diff --git a/lib/collection.dart b/lib/collection.dart index 7012432..70f9fbc 100644 --- a/lib/collection.dart +++ b/lib/collection.dart @@ -6,6 +6,7 @@ export "src/algorithms.dart"; export "src/canonicalized_map.dart"; export "src/combined_wrappers/combined_iterable.dart"; export "src/combined_wrappers/combined_list.dart"; +export "src/combined_wrappers/combined_map.dart"; export "src/comparators.dart"; export "src/equality.dart"; export "src/equality_map.dart"; diff --git a/lib/src/combined_wrappers/combined_iterable.dart b/lib/src/combined_wrappers/combined_iterable.dart index 62e02be..511876e 100644 --- a/lib/src/combined_wrappers/combined_iterable.dart +++ b/lib/src/combined_wrappers/combined_iterable.dart @@ -21,8 +21,10 @@ class CombinedIterableView extends IterableBase { Iterator get iterator => new _CombinedIterator(_iterables.map((i) => i.iterator).iterator); - // Special cased isEmpty/length since many iterables have an efficient - // implementation instead of running through the entire iterator. + // Special cased contains/isEmpty/length since many iterables have an + // efficient implementation instead of running through the entire iterator. + + bool contains(Object element) => _iterables.any((i) => i.contains(element)); bool get isEmpty => _iterables.every((i) => i.isEmpty); diff --git a/lib/src/combined_wrappers/combined_map.dart b/lib/src/combined_wrappers/combined_map.dart new file mode 100644 index 0000000..8c2760b --- /dev/null +++ b/lib/src/combined_wrappers/combined_map.dart @@ -0,0 +1,52 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:collection'; + +import 'combined_iterable.dart'; + +/// Returns a new map that represents maps flattened into a single map. +/// +/// All methods and accessors treat the new map as-if it were a single +/// concatenated map, but the underlying implementation is based on lazily +/// accessing individual map instances. In the occasion where a key occurs in +/// multiple maps the first value is returned. +/// +/// The resulting map has an index operator (`[]`) and `length` property that +/// are both `O(maps)`, rather than `O(1)`, and the map is unmodifiable - but +/// underlying changes to these maps are still accessible from the resulting +/// map. +class CombinedMapView extends UnmodifiableMapBase { + final Iterable> _maps; + + /// Create a new combined view into multiple maps. + /// + /// The iterable is accessed lazily so it should be collection type like + /// [List] or [Set] rather than a lazy iterable produced by `map()` et al. + CombinedMapView(this._maps); + + V operator [](Object key) { + for (var map in _maps) { + // Avoid two hash lookups on a positive hit. + var value = map[key]; + if (value != null || map.containsKey(value)) { + return value; + } + } + return null; + } + + /// The keys of [this]. + /// + /// The returned iterable has efficient `length` and `contains` operations, + /// based on [length] and [containsKey] of the individual maps. + /// + /// The order of iteration is defined by the individual `Map` implementations, + /// but must be consistent between changes to the maps. + /// + /// Unlike most [Map] implementations, modifying an individual map while + /// iterating the keys will _sometimes_ throw. This behavior may change in + /// the future. + Iterable get keys => new CombinedIterableView(_maps.map((m) => m.keys)); +} diff --git a/test/combined_wrapper/map_test.dart b/test/combined_wrapper/map_test.dart new file mode 100644 index 0000000..c447373 --- /dev/null +++ b/test/combined_wrapper/map_test.dart @@ -0,0 +1,55 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:collection/collection.dart'; +import 'package:test/test.dart'; + +import '../unmodifiable_collection_test.dart' as common; + +void main() { + var map1 = const {1: 1, 2: 2, 3: 3}; + var map2 = const {4: 4, 5: 5, 6: 6}; + var map3 = const {7: 7, 8: 8, 9: 9}; + var concat = {}..addAll(map1)..addAll(map2)..addAll(map3); + + // In every way possible this should test the same as an UnmodifiableMapView. + common.testReadMap(concat, new CombinedMapView( + [map1, map2, map3] + ), 'CombinedMapView'); + + common.testReadMap(concat, new CombinedMapView( + [map1, {}, map2, {}, map3, {}] + ), 'CombinedMapView (some empty)'); + + test('should function as an empty map when no maps are passed', () { + var empty = new CombinedMapView([]); + expect(empty, isEmpty); + expect(empty.length, 0); + }); + + test('should function as an empty map when only empty maps are passed', () { + var empty = new CombinedMapView([{}, {}, {}]); + expect(empty, isEmpty); + expect(empty.length, 0); + }); + + test('should reflect underlying changes back to the combined map', () { + var backing1 = {}; + var backing2 = {}; + var combined = new CombinedMapView([backing1, backing2]); + expect(combined, isEmpty); + backing1.addAll(map1); + expect(combined, map1); + backing2.addAll(map2); + expect(combined, new Map.from(backing1)..addAll(backing2)); + }); + + test('should reflect underlying changes with a single map', () { + var backing1 = {}; + var combined = new CombinedMapView([backing1]); + expect(combined, isEmpty); + backing1.addAll(map1); + expect(combined, map1); + }); +}