Skip to content

Commit

Permalink
Add CombinedMapView (dart-archive/collection#53)
Browse files Browse the repository at this point in the history
* Add CombinedMapView.

* Mistype.

* Address feedback.

* Address feedback.
  • Loading branch information
matanlurey authored Mar 23, 2017
1 parent 8d1a887 commit d81d3d8
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 2 deletions.
1 change: 1 addition & 0 deletions pkgs/collection/lib/collection.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ class CombinedIterableView<T> extends IterableBase<T> {
Iterator<T> get iterator =>
new _CombinedIterator<T>(_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);

Expand Down
52 changes: 52 additions & 0 deletions pkgs/collection/lib/src/combined_wrappers/combined_map.dart
Original file line number Diff line number Diff line change
@@ -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<K, V> extends UnmodifiableMapBase<K, V> {
final Iterable<Map<K, V>> _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<K> get keys => new CombinedIterableView<K>(_maps.map((m) => m.keys));
}
55 changes: 55 additions & 0 deletions pkgs/collection/test/combined_wrapper/map_test.dart
Original file line number Diff line number Diff line change
@@ -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 = <int, int>{};
var backing2 = <int, int>{};
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 = <int, int>{};
var combined = new CombinedMapView([backing1]);
expect(combined, isEmpty);
backing1.addAll(map1);
expect(combined, map1);
});
}

0 comments on commit d81d3d8

Please sign in to comment.