Add MapKeySet and MapValueSet to the collection package.
These classes provide Set views that are implemented as Maps under the
covers.
BUG=18705
R=floitsch@google.com, lrn@google.com
Review URL: https://codereview.chromium.org//277563007
git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart/pkg/collection@36447 260f80e4-7a28-3924-810f-c04153c831b5
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..e5a6381
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,7 @@
+## 0.9.3
+
+* Add a `MapKeySet` class that exposes an unmodifiable `Set` view of a `Map`'s
+ keys.
+
+* Add a `MapValueSet` class that takes a function from values to keys and uses
+ it to expose a `Set` view of a `Map`'s values.
diff --git a/lib/wrappers.dart b/lib/wrappers.dart
index 0fcc8c2..c9e919d 100644
--- a/lib/wrappers.dart
+++ b/lib/wrappers.dart
@@ -19,19 +19,15 @@
part "src/unmodifiable_wrappers.dart";
/**
- * Creates an [Iterable] that delegates all operations to a base iterable.
+ * A base class for delegating iterables.
*
- * This class can be used hide non-`Iterable` methods of an iterable object,
- * or it can be extended to add extra functionality on top of an existing
- * iterable object.
+ * Subclasses can provide a [_base] that should be delegated to. Unlike
+ * [DelegatingIterable], this allows the base to be created on demand.
*/
-class DelegatingIterable<E> implements Iterable<E> {
- final Iterable<E> _base;
+abstract class _DelegatingIterableBase<E> implements Iterable<E> {
+ Iterable<E> get _base;
- /**
- * Create a wrapper that forwards operations to [base].
- */
- const DelegatingIterable(Iterable<E> base) : _base = base;
+ const _DelegatingIterableBase();
bool any(bool test(E element)) => _base.any(test);
@@ -93,6 +89,22 @@
String toString() => _base.toString();
}
+/**
+ * Creates an [Iterable] that delegates all operations to a base iterable.
+ *
+ * This class can be used hide non-`Iterable` methods of an iterable object,
+ * or it can be extended to add extra functionality on top of an existing
+ * iterable object.
+ */
+class DelegatingIterable<E> extends _DelegatingIterableBase<E> {
+ final Iterable<E> _base;
+
+ /**
+ * Create a wrapper that forwards operations to [base].
+ */
+ const DelegatingIterable(Iterable<E> base) : _base = base;
+}
+
/**
* Creates a [List] that delegates all operations to a base list.
@@ -337,3 +349,219 @@
String toString() => _base.toString();
}
+
+/**
+ * An unmodifiable [Set] view of the keys of a [Map].
+ *
+ * The set delegates all operations to the underlying map.
+ *
+ * A `Map` can only contain each key once, so its keys can always
+ * be viewed as a `Set` without any loss, even if the [Map.keys]
+ * getter only shows an [Iterable] view of the keys.
+ *
+ * Note that [lookup] is not supported for this set.
+ */
+class MapKeySet<E> extends _DelegatingIterableBase<E>
+ with UnmodifiableSetMixin<E> {
+ final Map<E, dynamic> _baseMap;
+
+ MapKeySet(Map<E, dynamic> base) : _baseMap = base;
+
+ Iterable<E> get _base => _baseMap.keys;
+
+ bool contains(Object element) => _baseMap.containsKey(element);
+
+ bool get isEmpty => _baseMap.isEmpty;
+
+ bool get isNotEmpty => _baseMap.isNotEmpty;
+
+ int get length => _baseMap.length;
+
+ String toString() => "{${_base.join(', ')}}";
+
+ bool containsAll(Iterable<Object> other) => other.every(contains);
+
+ /**
+ * Returns a new set with the the elements of [this] that are not in [other].
+ *
+ * That is, the returned set contains all the elements of this [Set] that are
+ * not elements of [other] according to `other.contains`.
+ *
+ * Note that the returned set will use the default equality operation, which
+ * may be different than the equality operation [this] uses.
+ */
+ Set<E> difference(Set<E> other) =>
+ where((element) => !other.contains(element)).toSet();
+
+ /**
+ * Returns a new set which is the intersection between [this] and [other].
+ *
+ * That is, the returned set contains all the elements of this [Set] that are
+ * also elements of [other] according to `other.contains`.
+ *
+ * Note that the returned set will use the default equality operation, which
+ * may be different than the equality operation [this] uses.
+ */
+ Set<E> intersection(Set<Object> other) => where(other.contains).toSet();
+
+ /**
+ * Throws an [UnsupportedError] since there's no corresponding method for
+ * [Map]s.
+ */
+ E lookup(E element) => throw new UnsupportedError(
+ "MapKeySet doesn't support lookup().");
+
+ /**
+ * Returns a new set which contains all the elements of [this] and [other].
+ *
+ * That is, the returned set contains all the elements of this [Set] and all
+ * the elements of [other].
+ *
+ * Note that the returned set will use the default equality operation, which
+ * may be different than the equality operation [this] uses.
+ */
+ Set<E> union(Set<E> other) => toSet()..addAll(other);
+}
+
+/**
+ * Creates a modifiable [Set] view of the values of a [Map].
+ *
+ * The `Set` view assumes that the keys of the `Map` can be uniquely determined
+ * from the values. The `keyForValue` function passed to the constructor finds
+ * the key for a single value. The `keyForValue` function should be consistent
+ * with equality. If `value1 == value2` then `keyForValue(value1)` and
+ * `keyForValue(value2)` should be considered equal keys by the underlying map,
+ * and vice versa.
+ *
+ * Modifying the set will modify the underlying map based on the key returned by
+ * `keyForValue`.
+ *
+ * If the `Map` contents are not compatible with the `keyForValue` function, the
+ * set will not work consistently, and may give meaningless responses or do
+ * inconsistent updates.
+ *
+ * This set can, for example, be used on a map from database record IDs to the
+ * records. It exposes the records as a set, and allows for writing both
+ * `recordSet.add(databaseRecord)` and `recordMap[id]`.
+ *
+ * Effectively, the map will act as a kind of index for the set.
+ */
+class MapValueSet<K, V> extends _DelegatingIterableBase<V> implements Set<V> {
+ final Map<K, V> _baseMap;
+ final Function _keyForValue;
+
+ /**
+ * Creates a new [MapValueSet] based on [base].
+ *
+ * [keyForValue] returns the key in the map that should be associated with the
+ * given value. The set's notion of equality is identical to the equality of
+ * the return values of [keyForValue].
+ */
+ MapValueSet(Map<K, V> base, K keyForValue(V value))
+ : _baseMap = base,
+ _keyForValue = keyForValue;
+
+ Iterable<V> get _base => _baseMap.values;
+
+ bool contains(Object element) {
+ if (element != null && element is! V) return false;
+ return _baseMap.containsKey(_keyForValue(element));
+ }
+
+ bool get isEmpty => _baseMap.isEmpty;
+
+ bool get isNotEmpty => _baseMap.isNotEmpty;
+
+ int get length => _baseMap.length;
+
+ String toString() => toSet().toString();
+
+ bool add(V value) {
+ K key = _keyForValue(value);
+ bool result = false;
+ _baseMap.putIfAbsent(key, () {
+ result = true;
+ return value;
+ });
+ return result;
+ }
+
+ void addAll(Iterable<V> elements) => elements.forEach(add);
+
+ void clear() => _baseMap.clear();
+
+ bool containsAll(Iterable<Object> other) => other.every(contains);
+
+ /**
+ * Returns a new set with the the elements of [this] that are not in [other].
+ *
+ * That is, the returned set contains all the elements of this [Set] that are
+ * not elements of [other] according to `other.contains`.
+ *
+ * Note that the returned set will use the default equality operation, which
+ * may be different than the equality operation [this] uses.
+ */
+ Set<V> difference(Set<V> other) =>
+ where((element) => !other.contains(element)).toSet();
+
+ /**
+ * Returns a new set which is the intersection between [this] and [other].
+ *
+ * That is, the returned set contains all the elements of this [Set] that are
+ * also elements of [other] according to `other.contains`.
+ *
+ * Note that the returned set will use the default equality operation, which
+ * may be different than the equality operation [this] uses.
+ */
+ Set<V> intersection(Set<Object> other) => where(other.contains).toSet();
+
+ V lookup(V element) => _baseMap[_keyForValue(element)];
+
+ bool remove(Object value) {
+ if (value != null && value is! V) return false;
+ var key = _keyForValue(value);
+ if (!_baseMap.containsKey(key)) return false;
+ _baseMap.remove(key);
+ return true;
+ }
+
+ void removeAll(Iterable<Object> elements) => elements.forEach(remove);
+
+ void removeWhere(bool test(V element)) {
+ var toRemove = [];
+ _baseMap.forEach((key, value) {
+ if (test(value)) toRemove.add(key);
+ });
+ toRemove.forEach(_baseMap.remove);
+ }
+
+ void retainAll(Iterable<Object> elements) {
+ var valuesToRetain = new Set<V>.identity();
+ for (var element in elements) {
+ if (element != null && element is! V) continue;
+ var key = _keyForValue(element);
+ if (!_baseMap.containsKey(key)) continue;
+ valuesToRetain.add(_baseMap[key]);
+ }
+
+ var keysToRemove = [];
+ _baseMap.forEach((k, v) {
+ if (!valuesToRetain.contains(v)) keysToRemove.add(k);
+ });
+ keysToRemove.forEach(_baseMap.remove);
+ }
+
+ void retainWhere(bool test(V element)) =>
+ removeWhere((element) => !test(element));
+
+ /**
+ * Returns a new set which contains all the elements of [this] and [other].
+ *
+ * That is, the returned set contains all the elements of this [Set] and all
+ * the elements of [other].
+ *
+ * Note that the returned set will use the default equality operation, which
+ * may be different than the equality operation [this] uses.
+ */
+ Set<V> union(Set<V> other) => toSet()..addAll(other);
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index 58482ce..45cf9b7 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
name: collection
-version: 0.9.2
+version: 0.9.3-dev
author: Dart Team <misc@dartlang.org>
description: Collections and utilities functions and classes related to collections.
homepage: http://www.dartlang.org
diff --git a/test/wrapper_test.dart b/test/wrapper_test.dart
index 86a0ddd..5858aaf 100644
--- a/test/wrapper_test.dart
+++ b/test/wrapper_test.dart
@@ -265,6 +265,178 @@
expect.toString().equals.toString();
}
+ // Runs tests of Set behavior.
+ //
+ // [setUpSet] should return a set with two elements: "foo" and "bar".
+ void testTwoElementSet(Set<String> setUpSet()) {
+ group("with two elements", () {
+ var set;
+ setUp(() => set = setUpSet());
+
+ test(".any", () {
+ expect(set.any((element) => element == "foo"), isTrue);
+ expect(set.any((element) => element == "baz"), isFalse);
+ });
+
+ test(".elementAt", () {
+ expect(set.elementAt(0), equals("foo"));
+ expect(set.elementAt(1), equals("bar"));
+ expect(() => set.elementAt(2), throwsRangeError);
+ });
+
+ test(".every", () {
+ expect(set.every((element) => element == "foo"), isFalse);
+ expect(set.every((element) => element is String), isTrue);
+ });
+
+ test(".expand", () {
+ expect(set.expand((element) {
+ return [element.substring(0, 1), element.substring(1)];
+ }), equals(["f", "oo", "b", "ar"]));
+ });
+
+ test(".first", () {
+ expect(set.first, equals("foo"));
+ });
+
+ test(".firstWhere", () {
+ expect(set.firstWhere((element) => element is String), equals("foo"));
+ expect(set.firstWhere((element) => element.startsWith("b")),
+ equals("bar"));
+ expect(() => set.firstWhere((element) => element is int),
+ throwsStateError);
+ expect(set.firstWhere((element) => element is int, orElse: () => "baz"),
+ equals("baz"));
+ });
+
+ test(".fold", () {
+ expect(set.fold("start", (previous, element) => previous + element),
+ equals("startfoobar"));
+ });
+
+ test(".forEach", () {
+ var values = [];
+ set.forEach(values.add);
+ expect(values, equals(["foo", "bar"]));
+ });
+
+ test(".iterator", () {
+ var values = [];
+ for (var element in set) {
+ values.add(element);
+ }
+ expect(values, equals(["foo", "bar"]));
+ });
+
+ test(".join", () {
+ expect(set.join(", "), equals("foo, bar"));
+ });
+
+ test(".last", () {
+ expect(set.last, equals("bar"));
+ });
+
+ test(".lastWhere", () {
+ expect(set.lastWhere((element) => element is String), equals("bar"));
+ expect(set.lastWhere((element) => element.startsWith("f")),
+ equals("foo"));
+ expect(() => set.lastWhere((element) => element is int),
+ throwsStateError);
+ expect(set.lastWhere((element) => element is int, orElse: () => "baz"),
+ equals("baz"));
+ });
+
+ test(".map", () {
+ expect(set.map((element) => element.substring(1)),
+ equals(["oo", "ar"]));
+ });
+
+ test(".reduce", () {
+ expect(set.reduce((previous, element) => previous + element),
+ equals("foobar"));
+ });
+
+ test(".singleWhere", () {
+ expect(() => set.singleWhere((element) => element == "baz"),
+ throwsStateError);
+ expect(set.singleWhere((element) => element == "foo"),
+ "foo");
+ expect(() => set.singleWhere((element) => element is String),
+ throwsStateError);
+ });
+
+ test(".skip", () {
+ expect(set.skip(0), equals(["foo", "bar"]));
+ expect(set.skip(1), equals(["bar"]));
+ expect(set.skip(2), equals([]));
+ });
+
+ test(".skipWhile", () {
+ expect(set.skipWhile((element) => element.startsWith("f")),
+ equals(["bar"]));
+ expect(set.skipWhile((element) => element.startsWith("z")),
+ equals(["foo", "bar"]));
+ expect(set.skipWhile((element) => element is String),
+ equals([]));
+ });
+
+ test(".take", () {
+ expect(set.take(0), equals([]));
+ expect(set.take(1), equals(["foo"]));
+ expect(set.take(2), equals(["foo", "bar"]));
+ });
+
+ test(".takeWhile", () {
+ expect(set.takeWhile((element) => element.startsWith("f")),
+ equals(["foo"]));
+ expect(set.takeWhile((element) => element.startsWith("z")),
+ equals([]));
+ expect(set.takeWhile((element) => element is String),
+ equals(["foo", "bar"]));
+ });
+
+ test(".toList", () {
+ expect(set.toList(), equals(["foo", "bar"]));
+ expect(() => set.toList(growable: false).add("baz"),
+ throwsUnsupportedError);
+ expect(set.toList()..add("baz"), equals(["foo", "bar", "baz"]));
+ });
+
+ test(".toSet", () {
+ expect(set.toSet(), equals(new Set.from(["foo", "bar"])));
+ });
+
+ test(".where", () {
+ expect(set.where((element) => element.startsWith("f")),
+ equals(["foo"]));
+ expect(set.where((element) => element.startsWith("z")), equals([]));
+ expect(set.where((element) => element is String),
+ equals(["foo", "bar"]));
+ });
+
+ test(".containsAll", () {
+ expect(set.containsAll(["foo", "bar"]), isTrue);
+ expect(set.containsAll(["foo"]), isTrue);
+ expect(set.containsAll(["foo", "bar", "qux"]), isFalse);
+ });
+
+ test(".difference", () {
+ expect(set.difference(new Set.from(["foo", "baz"])),
+ equals(new Set.from(["bar"])));
+ });
+
+ test(".intersection", () {
+ expect(set.intersection(new Set.from(["foo", "baz"])),
+ equals(new Set.from(["foo"])));
+ });
+
+ test(".union", () {
+ expect(set.union(new Set.from(["foo", "baz"])),
+ equals(new Set.from(["foo", "bar", "baz"])));
+ });
+ });
+ }
+
test("Iterable", () {
testIterable(new IterableExpector());
});
@@ -284,4 +456,209 @@
test("Map", () {
testMap(new MapExpector());
});
+
+ group("MapKeySet", () {
+ var map;
+ var set;
+
+ setUp(() {
+ map = new Map<String, int>();
+ set = new MapKeySet<String>(map);
+ });
+
+ testTwoElementSet(() {
+ map["foo"] = 1;
+ map["bar"] = 2;
+ return set;
+ });
+
+ test(".single", () {
+ expect(() => set.single, throwsStateError);
+ map["foo"] = 1;
+ expect(set.single, equals("foo"));
+ map["bar"] = 1;
+ expect(() => set.single, throwsStateError);
+ });
+
+ test(".toString", () {
+ expect(set.toString(), equals("{}"));
+ map["foo"] = 1;
+ map["bar"] = 2;
+ expect(set.toString(), equals("{foo, bar}"));
+ });
+
+ test(".contains", () {
+ expect(set.contains("foo"), isFalse);
+ map["foo"] = 1;
+ expect(set.contains("foo"), isTrue);
+ });
+
+ test(".isEmpty", () {
+ expect(set.isEmpty, isTrue);
+ map["foo"] = 1;
+ expect(set.isEmpty, isFalse);
+ });
+
+ test(".isNotEmpty", () {
+ expect(set.isNotEmpty, isFalse);
+ map["foo"] = 1;
+ expect(set.isNotEmpty, isTrue);
+ });
+
+ test(".length", () {
+ expect(set, hasLength(0));
+ map["foo"] = 1;
+ expect(set, hasLength(1));
+ map["bar"] = 2;
+ expect(set, hasLength(2));
+ });
+
+ test("is unmodifiable", () {
+ expect(() => set.add("baz"), throwsUnsupportedError);
+ expect(() => set.addAll(["baz", "bang"]), throwsUnsupportedError);
+ expect(() => set.remove("foo"), throwsUnsupportedError);
+ expect(() => set.removeAll(["baz", "bang"]), throwsUnsupportedError);
+ expect(() => set.retainAll(["foo"]), throwsUnsupportedError);
+ expect(() => set.removeWhere((_) => true), throwsUnsupportedError);
+ expect(() => set.retainWhere((_) => true), throwsUnsupportedError);
+ expect(() => set.clear(), throwsUnsupportedError);
+ });
+ });
+
+ group("MapValueSet", () {
+ var map;
+ var set;
+
+ setUp(() {
+ map = new Map<String, String>();
+ set = new MapValueSet<String, String>(map,
+ (string) => string.substring(0, 1));
+ });
+
+ testTwoElementSet(() {
+ map["f"] = "foo";
+ map["b"] = "bar";
+ return set;
+ });
+
+ test(".single", () {
+ expect(() => set.single, throwsStateError);
+ map["f"] = "foo";
+ expect(set.single, equals("foo"));
+ map["b"] = "bar";
+ expect(() => set.single, throwsStateError);
+ });
+
+ test(".toString", () {
+ expect(set.toString(), equals("{}"));
+ map["f"] = "foo";
+ map["b"] = "bar";
+ expect(set.toString(), equals("{foo, bar}"));
+ });
+
+ test(".contains", () {
+ expect(set.contains("foo"), isFalse);
+ map["f"] = "foo";
+ expect(set.contains("foo"), isTrue);
+ expect(set.contains("fblthp"), isTrue);
+ });
+
+ test(".isEmpty", () {
+ expect(set.isEmpty, isTrue);
+ map["f"] = "foo";
+ expect(set.isEmpty, isFalse);
+ });
+
+ test(".isNotEmpty", () {
+ expect(set.isNotEmpty, isFalse);
+ map["f"] = "foo";
+ expect(set.isNotEmpty, isTrue);
+ });
+
+ test(".length", () {
+ expect(set, hasLength(0));
+ map["f"] = "foo";
+ expect(set, hasLength(1));
+ map["b"] = "bar";
+ expect(set, hasLength(2));
+ });
+
+ test(".lookup", () {
+ map["f"] = "foo";
+ expect(set.lookup("fblthp"), equals("foo"));
+ expect(set.lookup("bar"), isNull);
+ });
+
+ test(".add", () {
+ set.add("foo");
+ set.add("bar");
+ expect(map, equals({"f": "foo", "b": "bar"}));
+ });
+
+ test(".addAll", () {
+ set.addAll(["foo", "bar"]);
+ expect(map, equals({"f": "foo", "b": "bar"}));
+ });
+
+ test(".clear", () {
+ map["f"] = "foo";
+ map["b"] = "bar";
+ set.clear();
+ expect(map, isEmpty);
+ });
+
+ test(".remove", () {
+ map["f"] = "foo";
+ map["b"] = "bar";
+ set.remove("fblthp");
+ expect(map, equals({"b": "bar"}));
+ });
+
+ test(".removeAll", () {
+ map["f"] = "foo";
+ map["b"] = "bar";
+ map["q"] = "qux";
+ set.removeAll(["fblthp", "qux"]);
+ expect(map, equals({"b": "bar"}));
+ });
+
+ test(".removeWhere", () {
+ map["f"] = "foo";
+ map["b"] = "bar";
+ map["q"] = "qoo";
+ set.removeWhere((element) => element.endsWith("o"));
+ expect(map, equals({"b": "bar"}));
+ });
+
+ test(".retainAll", () {
+ map["f"] = "foo";
+ map["b"] = "bar";
+ map["q"] = "qux";
+ set.retainAll(["fblthp", "qux"]);
+ expect(map, equals({"f": "foo", "q": "qux"}));
+ });
+
+ test(".retainAll respects an unusual notion of equality", () {
+ map = new HashMap<String, String>(
+ equals: (value1, value2) =>
+ value1.toLowerCase() == value2.toLowerCase(),
+ hashCode: (value) => value.toLowerCase().hashCode);
+ set = new MapValueSet<String, String>(map,
+ (string) => string.substring(0, 1));
+
+ map["f"] = "foo";
+ map["B"] = "bar";
+ map["Q"] = "qux";
+ set.retainAll(["fblthp", "qux"]);
+ expect(map, equals({"f": "foo", "Q": "qux"}));
+ });
+
+ test(".retainWhere", () {
+ map["f"] = "foo";
+ map["b"] = "bar";
+ map["q"] = "qoo";
+ set.retainWhere((element) => element.endsWith("o"));
+ expect(map, equals({"f": "foo", "q": "qoo"}));
+ });
+ });
}