Add GroupSet and SetGroup classes.
GroupSet provides a view of the union of a set of sets. SetGroup
provides an easy way to manage a GroupSet in a class context.
R=floitsch@google.com, lrn@google.com
Review URL: https://codereview.chromium.org//1873373002 .
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 01da16b..559e810 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,9 @@
-## 1.5.2
+## 1.6.0
+
+* Add a `UnionSet` class that provides a view of the union of a set of sets.
+
+* Add a `UnionSetController` class that provides a convenient way to manage the
+ contents of a `UnionSet`.
* Fix another incorrectly-declared generic type.
diff --git a/lib/collection.dart b/lib/collection.dart
index b2c7e73..95b8479 100644
--- a/lib/collection.dart
+++ b/lib/collection.dart
@@ -9,5 +9,7 @@
export "src/iterable_zip.dart";
export "src/priority_queue.dart";
export "src/queue_list.dart";
+export "src/union_set.dart";
+export "src/union_set_controller.dart";
export "src/unmodifiable_wrappers.dart";
export "src/wrappers.dart";
diff --git a/lib/src/union_set.dart b/lib/src/union_set.dart
new file mode 100644
index 0000000..5d88b6b
--- /dev/null
+++ b/lib/src/union_set.dart
@@ -0,0 +1,88 @@
+// Copyright (c) 2016, 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 'unmodifiable_wrappers.dart';
+
+/// A single set that provides a view of the union over a set of sets.
+///
+/// Since this is just a view, it reflects all changes in the underlying sets.
+///
+/// If an element is in multiple sets and the outer set is ordered, the version
+/// in the earliest inner set is preferred. Component sets are assumed to use
+/// `==` and `hashCode` for equality.
+class UnionSet<E> extends SetBase<E> with UnmodifiableSetMixin<E> {
+ /// The set of sets that this provides a view of.
+ final Set<Set<E>> _sets;
+
+ /// Whether the sets in [_sets] are guaranteed to be disjoint.
+ final bool _disjoint;
+
+ /// Creates a new set that's a view of the union of all sets in [sets].
+ ///
+ /// If any sets in [sets] change, this [UnionSet] reflects that change. If a
+ /// new set is added to [sets], this [UnionSet] reflects that as well.
+ ///
+ /// If [disjoint] is `true`, then all component sets must be disjoint. That
+ /// is, that they contain no elements in common. This makes many operations
+ /// including [length] more efficient. If the component sets turn out not to
+ /// be disjoint, some operations may behave inconsistently.
+ UnionSet(this._sets, {bool disjoint: false}) : _disjoint = disjoint;
+
+ /// Creates a new set that's a view of the union of all sets in [sets].
+ ///
+ /// If any sets in [sets] change, this [UnionSet] reflects that change.
+ /// However, unlike [new UnionSet], this creates a copy of its parameter, so
+ /// changes in [sets] aren't reflected in this [UnionSet].
+ ///
+ /// If [disjoint] is `true`, then all component sets must be disjoint. That
+ /// is, that they contain no elements in common. This makes many operations
+ /// including [length] more efficient. If the component sets turn out not to
+ /// be disjoint, some operations may behave inconsistently.
+ UnionSet.from(Iterable<Set<E>> sets, {bool disjoint: false})
+ : this(sets.toSet(), disjoint: disjoint);
+
+ int get length => _disjoint
+ ? _sets.fold(0, (length, set) => length + set.length)
+ : _iterable.length;
+
+ Iterator<E> get iterator => _iterable.iterator;
+
+ /// Returns an iterable over the contents of all the sets in [this].
+ Iterable<E> get _iterable =>
+ _disjoint ? _sets.expand((set) => set) : _dedupIterable;
+
+ /// Returns an iterable over the contents of all the sets in [this] that
+ /// de-duplicates elements.
+ ///
+ /// If the sets aren't guaranteed to be disjoint, this keeps track of the
+ /// elements we've already emitted so that we can de-duplicate them.
+ Iterable<E> get _dedupIterable {
+ var seen = new Set<E>();
+ return _sets.expand((set) => set).where((element) {
+ if (seen.contains(element)) return false;
+ seen.add(element);
+ return true;
+ });
+ }
+
+ bool contains(Object element) => _sets.any((set) => set.contains(element));
+
+ E lookup(Object element) {
+ if (element == null) return null;
+
+ return _sets
+ .map((set) => set.lookup(element))
+ .firstWhere((result) => result != null, orElse: () => null);
+ }
+
+ Set<E> toSet() {
+ var result = new Set<E>();
+ for (var set in _sets) {
+ result.addAll(set);
+ }
+ return result;
+ }
+}
diff --git a/lib/src/union_set_controller.dart b/lib/src/union_set_controller.dart
new file mode 100644
index 0000000..1d0eb74
--- /dev/null
+++ b/lib/src/union_set_controller.dart
@@ -0,0 +1,54 @@
+// Copyright (c) 2016, 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 'union_set.dart';
+
+/// A controller that exposes a view of the union of a collection of sets.
+///
+/// This is a convenience class for creating a [UnionSet] whose contents change
+/// over the lifetime of a class. For example:
+///
+/// ```dart
+/// class Engine {
+/// Set<Test> get activeTests => _activeTestsGroup.set;
+/// final _activeTestsGroup = new UnionSetController<Test>();
+///
+/// void addSuite(Suite suite) {
+/// _activeTestsGroup.add(suite.tests);
+/// _runSuite(suite);
+/// _activeTestsGroup.remove(suite.tests);
+/// }
+/// }
+/// ```
+class UnionSetController<E> {
+ /// The [UnionSet] that provides a view of the union of sets in [this].
+ UnionSet<E> get set => _set;
+ UnionSet<E> _set;
+
+ /// The sets whose union is exposed through [set].
+ final _sets = new Set<Set<E>>();
+
+ /// Creates a set of sets that provides a view of the union of those sets.
+ ///
+ /// If [disjoint] is `true`, this assumes that all component sets are
+ /// disjoint—that is, that they contain no elements in common. This makes
+ /// many operations including [length] more efficient.
+ UnionSetController({bool disjoint: false}) {
+ _set = new UnionSet<E>(_sets, disjoint: disjoint);
+ }
+
+ /// Adds the contents of [component] to [set].
+ ///
+ /// If the contents of [component] change over time, [set] will change
+ /// accordingly.
+ void add(Set<E> component) {
+ _sets.add(component);
+ }
+
+ /// Removes the contents of [component] to [set].
+ ///
+ /// If another set in [this] has overlapping elements with [component], those
+ /// elements will remain in [set].
+ bool remove(Set<E> component) => _sets.remove(component);
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index 00a3f7c..992e7ae 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
name: collection
-version: 1.5.2-dev
+version: 1.6.0
author: Dart Team <misc@dartlang.org>
description: Collections and utilities functions and classes related to collections.
homepage: https://www.github.com/dart-lang/collection
diff --git a/test/union_set_controller_test.dart b/test/union_set_controller_test.dart
new file mode 100644
index 0000000..aa8c1c9
--- /dev/null
+++ b/test/union_set_controller_test.dart
@@ -0,0 +1,36 @@
+// Copyright (c) 2016, 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:test/test.dart";
+
+import "package:collection/collection.dart";
+
+void main() {
+ var controller;
+ var innerSet;
+ setUp(() {
+ innerSet = new Set.from([1, 2, 3]);
+ controller = new UnionSetController<int>()..add(innerSet);
+ });
+
+ test("exposes a union set", () {
+ expect(controller.set, unorderedEquals([1, 2, 3]));
+
+ controller.add(new Set.from([3, 4, 5]));
+ expect(controller.set, unorderedEquals([1, 2, 3, 4, 5]));
+
+ controller.remove(innerSet);
+ expect(controller.set, unorderedEquals([3, 4, 5]));
+ });
+
+ test("exposes a disjoint union set", () {
+ expect(controller.set, unorderedEquals([1, 2, 3]));
+
+ controller.add(new Set.from([4, 5, 6]));
+ expect(controller.set, unorderedEquals([1, 2, 3, 4, 5, 6]));
+
+ controller.remove(innerSet);
+ expect(controller.set, unorderedEquals([4, 5, 6]));
+ });
+}
diff --git a/test/union_set_test.dart b/test/union_set_test.dart
new file mode 100644
index 0000000..15d9d49
--- /dev/null
+++ b/test/union_set_test.dart
@@ -0,0 +1,222 @@
+// Copyright (c) 2016, 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:test/test.dart";
+
+import "package:collection/collection.dart";
+
+void main() {
+ group("with an empty outer set", () {
+ var set;
+ setUp(() {
+ set = new UnionSet<int>(new Set());
+ });
+
+ test("length returns 0", () {
+ expect(set.length, equals(0));
+ });
+
+ test("contains() returns false", () {
+ expect(set.contains(0), isFalse);
+ expect(set.contains(null), isFalse);
+ expect(set.contains("foo"), isFalse);
+ });
+
+ test("lookup() returns null", () {
+ expect(set.lookup(0), isNull);
+ expect(set.lookup(null), isNull);
+ expect(set.lookup("foo"), isNull);
+ });
+
+ test("toSet() returns an empty set", () {
+ expect(set.toSet(), isEmpty);
+ expect(set.toSet(), isNot(same(set)));
+ });
+
+ test("map() doesn't run on any elements", () {
+ expect(set.map(expectAsync((_) {}, count: 0)), isEmpty);
+ });
+ });
+
+ group("with multiple disjoint sets", () {
+ var set;
+ setUp(() {
+ set = new UnionSet<int>.from([
+ new Set.from([1, 2]),
+ new Set.from([3, 4]),
+ new Set.from([5]),
+ new Set()
+ ], disjoint: true);
+ });
+
+ test("length returns the total length", () {
+ expect(set.length, equals(5));
+ });
+
+ test("contains() returns whether any set contains the element", () {
+ expect(set.contains(1), isTrue);
+ expect(set.contains(4), isTrue);
+ expect(set.contains(5), isTrue);
+ expect(set.contains(6), isFalse);
+ });
+
+ test("lookup() returns elements that are in any set", () {
+ expect(set.lookup(1), equals(1));
+ expect(set.lookup(4), equals(4));
+ expect(set.lookup(5), equals(5));
+ expect(set.lookup(6), isNull);
+ });
+
+ test("toSet() returns the union of all the sets", () {
+ expect(set.toSet(), unorderedEquals([1, 2, 3, 4, 5]));
+ expect(set.toSet(), isNot(same(set)));
+ });
+
+ test("map() maps the elements", () {
+ expect(set.map((i) => i * 2), unorderedEquals([2, 4, 6, 8, 10]));
+ });
+ });
+
+ group("with multiple overlapping sets", () {
+ var set;
+ setUp(() {
+ set = new UnionSet<int>.from([
+ new Set.from([1, 2, 3]),
+ new Set.from([3, 4]),
+ new Set.from([5, 1]),
+ new Set()
+ ]);
+ });
+
+ test("length returns the total length", () {
+ expect(set.length, equals(5));
+ });
+
+ test("contains() returns whether any set contains the element", () {
+ expect(set.contains(1), isTrue);
+ expect(set.contains(4), isTrue);
+ expect(set.contains(5), isTrue);
+ expect(set.contains(6), isFalse);
+ });
+
+ test("lookup() returns elements that are in any set", () {
+ expect(set.lookup(1), equals(1));
+ expect(set.lookup(4), equals(4));
+ expect(set.lookup(5), equals(5));
+ expect(set.lookup(6), isNull);
+ });
+
+ test("lookup() returns the first element in an ordered context", () {
+ var duration1 = new Duration(seconds: 0);
+ var duration2 = new Duration(seconds: 0);
+ expect(duration1, equals(duration2));
+ expect(duration1, isNot(same(duration2)));
+
+ var set = new UnionSet.from([
+ new Set.from([duration1]),
+ new Set.from([duration2])
+ ]);
+
+ expect(set.lookup(new Duration(seconds: 0)), same(duration1));
+ });
+
+ test("toSet() returns the union of all the sets", () {
+ expect(set.toSet(), unorderedEquals([1, 2, 3, 4, 5]));
+ expect(set.toSet(), isNot(same(set)));
+ });
+
+ test("map() maps the elements", () {
+ expect(set.map((i) => i * 2), unorderedEquals([2, 4, 6, 8, 10]));
+ });
+ });
+
+ group("after an inner set was modified", () {
+ var set;
+ setUp(() {
+ var innerSet = new Set.from([3, 7]);
+ set = new UnionSet<int>.from([
+ new Set.from([1, 2]),
+ new Set.from([5]),
+ innerSet
+ ]);
+
+ innerSet.add(4);
+ innerSet.remove(7);
+ });
+
+ test("length returns the total length", () {
+ expect(set.length, equals(5));
+ });
+
+ test("contains() returns true for a new element", () {
+ expect(set.contains(4), isTrue);
+ });
+
+ test("contains() returns false for a removed element", () {
+ expect(set.contains(7), isFalse);
+ });
+
+ test("lookup() returns a new element", () {
+ expect(set.lookup(4), equals(4));
+ });
+
+ test("lookup() doesn't returns a removed element", () {
+ expect(set.lookup(7), isNull);
+ });
+
+ test("toSet() returns the union of all the sets", () {
+ expect(set.toSet(), unorderedEquals([1, 2, 3, 4, 5]));
+ expect(set.toSet(), isNot(same(set)));
+ });
+
+ test("map() maps the elements", () {
+ expect(set.map((i) => i * 2), unorderedEquals([2, 4, 6, 8, 10]));
+ });
+ });
+
+ group("after the outer set was modified", () {
+ var set;
+ setUp(() {
+ var innerSet = new Set.from([6]);
+ var outerSet = new Set.from([
+ new Set.from([1, 2]),
+ new Set.from([5]),
+ innerSet
+ ]);
+
+ set = new UnionSet<int>(outerSet);
+ outerSet.remove(innerSet);
+ outerSet.add(new Set.from([3, 4]));
+ });
+
+ test("length returns the total length", () {
+ expect(set.length, equals(5));
+ });
+
+ test("contains() returns true for a new element", () {
+ expect(set.contains(4), isTrue);
+ });
+
+ test("contains() returns false for a removed element", () {
+ expect(set.contains(6), isFalse);
+ });
+
+ test("lookup() returns a new element", () {
+ expect(set.lookup(4), equals(4));
+ });
+
+ test("lookup() doesn't returns a removed element", () {
+ expect(set.lookup(6), isNull);
+ });
+
+ test("toSet() returns the union of all the sets", () {
+ expect(set.toSet(), unorderedEquals([1, 2, 3, 4, 5]));
+ expect(set.toSet(), isNot(same(set)));
+ });
+
+ test("map() maps the elements", () {
+ expect(set.map((i) => i * 2), unorderedEquals([2, 4, 6, 8, 10]));
+ });
+ });
+}