Skip to content

Commit 32f22a5

Browse files
authored
CombinedMapView.keys fix (flutter#110)
Fixes https://github.com/dart-lang/collection/issues/109 Adds a custom iterable/iterator that can filter out duplicates and use that for the `CombineMapView.keys` getter. Updates tests to contain duplicates in maps, and ensure the keys/values from the earlier maps are the ones that are returned. Updates the changelog and docs to no longer claim `O(maps)` for the length getter. This now requires iteration of all items and is `O(total map entries)`. Prepare to publish as 1.14.12
1 parent b43f26c commit 32f22a5

File tree

4 files changed

+84
-12
lines changed

4 files changed

+84
-12
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
## 1.14.12
2+
3+
* Fix `CombinedMapView.keys`, `CombinedMapView.length`,
4+
`CombinedMapView.forEach`, and `CombinedMapView.values` to work as specified
5+
and not repeat duplicate items from the maps.
6+
* As a result of this fix the `length` getter now must iterate all maps in
7+
order to remove duplicates and return an accurate length, so it is no
8+
longer `O(maps)`.
9+
110
## 1.14.11
211

312
* Set max SDK version to `<3.0.0`.

lib/src/combined_wrappers/combined_map.dart

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,16 @@ import 'combined_iterable.dart';
1313
/// accessing individual map instances. In the occasion where a key occurs in
1414
/// multiple maps the first value is returned.
1515
///
16-
/// The resulting map has an index operator (`[]`) and `length` property that
17-
/// are both `O(maps)`, rather than `O(1)`, and the map is unmodifiable - but
18-
/// underlying changes to these maps are still accessible from the resulting
19-
/// map.
16+
/// The resulting map has an index operator (`[]`) that is `O(maps)`, rather
17+
/// than `O(1)`, and the map is unmodifiable, but underlying changes to these
18+
/// maps are still accessible from the resulting map.
19+
///
20+
/// The `length` getter is `O(M)` where M is the total number of entries in
21+
/// all maps, since it has to remove duplicate entries.
2022
class CombinedMapView<K, V> extends UnmodifiableMapBase<K, V> {
2123
final Iterable<Map<K, V>> _maps;
2224

23-
/// Create a new combined view into multiple maps.
25+
/// Create a new combined view of multiple maps.
2426
///
2527
/// The iterable is accessed lazily so it should be collection type like
2628
/// [List] or [Set] rather than a lazy iterable produced by `map()` et al.
@@ -39,14 +41,57 @@ class CombinedMapView<K, V> extends UnmodifiableMapBase<K, V> {
3941

4042
/// The keys of [this].
4143
///
42-
/// The returned iterable has efficient `length` and `contains` operations,
43-
/// based on [length] and [containsKey] of the individual maps.
44+
/// The returned iterable has efficient `contains` operations, assuming the
45+
/// iterables returned by the wrapped maps have efficient `contains` operations
46+
/// for their `keys` iterables.
47+
///
48+
/// The `length` must do deduplication and thus is not optimized.
4449
///
4550
/// The order of iteration is defined by the individual `Map` implementations,
4651
/// but must be consistent between changes to the maps.
4752
///
4853
/// Unlike most [Map] implementations, modifying an individual map while
4954
/// iterating the keys will _sometimes_ throw. This behavior may change in
5055
/// the future.
51-
Iterable<K> get keys => CombinedIterableView<K>(_maps.map((m) => m.keys));
56+
Iterable<K> get keys => _DeduplicatingIterableView(
57+
CombinedIterableView(_maps.map((m) => m.keys)));
58+
}
59+
60+
/// A view of an iterable that skips any duplicate entries.
61+
class _DeduplicatingIterableView<T> extends IterableBase<T> {
62+
final Iterable<T> _iterable;
63+
64+
const _DeduplicatingIterableView(this._iterable);
65+
66+
Iterator<T> get iterator => _DeduplicatingIterator(_iterable.iterator);
67+
68+
// Special cased contains/isEmpty since many iterables have an efficient
69+
// implementation instead of running through the entire iterator.
70+
//
71+
// Note: We do not do this for `length` because we have to remove the
72+
// duplicates.
73+
74+
bool contains(Object element) => _iterable.contains(element);
75+
76+
bool get isEmpty => _iterable.isEmpty;
77+
}
78+
79+
/// An iterator that wraps another iterator and skips duplicate values.
80+
class _DeduplicatingIterator<T> implements Iterator<T> {
81+
final Iterator<T> _iterator;
82+
83+
final _emitted = HashSet<T>();
84+
85+
_DeduplicatingIterator(this._iterator);
86+
87+
T get current => _iterator.current;
88+
89+
bool moveNext() {
90+
while (_iterator.moveNext()) {
91+
if (_emitted.add(current)) {
92+
return true;
93+
}
94+
}
95+
return false;
96+
}
5297
}

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: collection
2-
version: 1.14.12-dev
2+
version: 1.14.12
33

44
description: Collections and utilities functions and classes related to collections.
55
author: Dart Team <[email protected]>

test/combined_wrapper/map_test.dart

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5+
import 'dart:collection';
6+
57
import 'package:collection/collection.dart';
68
import 'package:test/test.dart';
79

@@ -11,13 +13,23 @@ void main() {
1113
var map1 = const {1: 1, 2: 2, 3: 3};
1214
var map2 = const {4: 4, 5: 5, 6: 6};
1315
var map3 = const {7: 7, 8: 8, 9: 9};
14-
var concat = <int, int>{}..addAll(map1)..addAll(map2)..addAll(map3);
16+
var map4 = const {1: -1, 2: -2, 3: -3};
17+
var concat = SplayTreeMap<int, int>()
18+
// The duplicates map appears first here but last in the CombinedMapView
19+
// which has the opposite semantics of `concat`. Keys/values should be
20+
// returned from the first map that contains them.
21+
..addAll(map4)
22+
..addAll(map1)
23+
..addAll(map2)
24+
..addAll(map3);
1525

1626
// In every way possible this should test the same as an UnmodifiableMapView.
1727
common.testReadMap(
18-
concat, CombinedMapView([map1, map2, map3]), 'CombinedMapView');
28+
concat, CombinedMapView([map1, map2, map3, map4]), 'CombinedMapView');
1929

20-
common.testReadMap(concat, CombinedMapView([map1, {}, map2, {}, map3, {}]),
30+
common.testReadMap(
31+
concat,
32+
CombinedMapView([map1, {}, map2, {}, map3, {}, map4, {}]),
2133
'CombinedMapView (some empty)');
2234

2335
test('should function as an empty map when no maps are passed', () {
@@ -50,4 +62,10 @@ void main() {
5062
backing1.addAll(map1);
5163
expect(combined, map1);
5264
});
65+
66+
test('re-iterating keys produces same result', () {
67+
var combined = CombinedMapView([map1, map2, map3, map4]);
68+
var keys = combined.keys;
69+
expect(keys.toList(), keys.toList());
70+
});
5371
}

0 commit comments

Comments
 (0)