Add CombinedListView (#50)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7cca956..97792cd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 1.14.0
+
+* Add `CombinedListView`, a view of several lists concatenated together.
+
## 1.13.0
* Add `EqualityBy`
diff --git a/lib/collection.dart b/lib/collection.dart
index 612508b..2ea73a7 100644
--- a/lib/collection.dart
+++ b/lib/collection.dart
@@ -4,6 +4,7 @@
export "src/algorithms.dart";
export "src/canonicalized_map.dart";
+export "src/combined_wrappers/combined_list.dart";
export "src/comparators.dart";
export "src/equality.dart";
export "src/equality_map.dart";
diff --git a/lib/src/combined_wrappers/combined_list.dart b/lib/src/combined_wrappers/combined_list.dart
new file mode 100644
index 0000000..d2e0349
--- /dev/null
+++ b/lib/src/combined_wrappers/combined_list.dart
@@ -0,0 +1,66 @@
+// 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';
+
+/// A view of several lists combined into a single list.
+///
+/// All methods and accessors treat the [CombinedListView] list as if it were a
+/// single concatenated list, but the underlying implementation is based on
+/// lazily accessing individual list instances. This means that if the
+/// underlying lists change, the [CombinedListView] will reflect those changes.
+///
+/// The index operator (`[]`) and [length] property of a [CombinedListView] are
+/// both `O(lists)` rather than `O(1)`. A [CombinedListView] is unmodifiable.
+class CombinedListView<T> extends ListBase<T>
+ implements UnmodifiableListView<T> {
+ static void _throw() {
+ throw new UnsupportedError('Cannot modify an unmodifiable List');
+ }
+
+ /// The lists that this combines.
+ final List<List<T>> _lists;
+
+ /// Creates a combined view of [lists].
+ CombinedListView(this._lists);
+
+ set length(int length) {
+ _throw();
+ }
+
+ int get length => _lists.fold(0, (length, list) => length + list.length);
+
+ T operator [](int index) {
+ var initialIndex = index;
+ for (var i = 0; i < _lists.length; i++) {
+ var list = _lists[i];
+ if (index < list.length) {
+ return list[index];
+ }
+ index -= list.length;
+ }
+ throw new RangeError.index(initialIndex, this, 'index', null, length);
+ }
+
+ void operator []=(int index, T value) {
+ _throw();
+ }
+
+ void clear() {
+ _throw();
+ }
+
+ bool remove(Object element) {
+ _throw();
+ return null;
+ }
+
+ void removeWhere(bool filter(T element)) {
+ _throw();
+ }
+
+ void retainWhere(bool filter(T element)) {
+ _throw();
+ }
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index 587f9f0..5060112 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
name: collection
-version: 1.13.0
+version: 1.14.0-dev
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/combined_list_view_test.dart b/test/combined_list_view_test.dart
new file mode 100644
index 0000000..d61e51a
--- /dev/null
+++ b/test/combined_list_view_test.dart
@@ -0,0 +1,69 @@
+// 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 list1 = [1, 2, 3];
+ var list2 = [4, 5, 6];
+ var list3 = [7, 8, 9];
+ var concat = []..addAll(list1)..addAll(list2)..addAll(list3);
+
+ // In every way possible this should test the same as an UnmodifiableListView.
+ common.testUnmodifiableList(concat, new CombinedListView(
+ [list1, list2, list3]
+ ), 'combineLists');
+
+ common.testUnmodifiableList(concat, new CombinedListView(
+ [list1, [], list2, [], list3, []]
+ ), 'combineLists');
+
+ test('should function as an empty list when no lists are passed', () {
+ var empty = new CombinedListView([]);
+ expect(empty, isEmpty);
+ expect(empty.length, 0);
+ expect(() => empty[0], throwsRangeError);
+ });
+
+ test('should function as an empty list when only empty lists are passed', () {
+ var empty = new CombinedListView([[], [], []]);
+ expect(empty, isEmpty);
+ expect(empty.length, 0);
+ expect(() => empty[0], throwsRangeError);
+ });
+
+ test('should reflect underlying changes back to the combined list', () {
+ var backing1 = <int>[];
+ var backing2 = <int>[];
+ var combined = new CombinedListView([backing1, backing2]);
+ expect(combined, isEmpty);
+ backing1.addAll(list1);
+ expect(combined, list1);
+ backing2.addAll(list2);
+ expect(combined, backing1.toList()..addAll(backing2));
+ });
+
+ test('should reflect underlying changes from the list of lists', () {
+ var listOfLists = <List<int>>[];
+ var combined = new CombinedListView(listOfLists);
+ expect(combined, isEmpty);
+ listOfLists.add(list1);
+ expect(combined, list1);
+ listOfLists.add(list2);
+ expect(combined, []..addAll(list1)..addAll(list2));
+ listOfLists.clear();
+ expect(combined, isEmpty);
+ });
+
+ test('should reflect underlying changes with a single list', () {
+ var backing1 = <int>[];
+ var combined = new CombinedListView([backing1]);
+ expect(combined, isEmpty);
+ backing1.addAll(list1);
+ expect(combined, list1);
+ });
+}