diff --git a/.gitignore b/.gitignore index 9d0511bc..497436f4 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ packages pubspec.lock build/ +.dart_tool/ .pub .packages diff --git a/CHANGELOG.md b/CHANGELOG.md index 75afc06c..5cdbdc82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,30 @@ #### Master + * New: BiMap now includes a real implementation of `addEntries`, `get + entries`, `map`, `removeWhere`, `update`, and `updateAll`. * New: DelegatingIterable now includes a real implementation of `followedBy`, and accepts the `orElse` parameter on `singleWhere`. * New: DelegatingList now includes real implementations of `operator +`, `indexWhere`, and `lastIndexWhere`. * New: LruMap now includes a real implementation of `addEntries`, `get entries`, `removeWhere`, `update`, and `updateAll`. + * New: The map returned by `Multimap.asMap()` now includes real + implementations of `get entries` and `removeWhere`. This class also has + "real" implementations of `addEntries`, `map`, `update`, and `updateAll`, + which just throw an `UnsupportedError`, as inserts and updates are not + allowed on map views. + * New: The list keys of `ListMultimap` now include real implementations of + `operator +`, `indexWhere`, and `lastIndexWhere`. + * New: The iterable keys of `ListMultimap` and `SetMultimap` now include a + real implementation of `followedBy`, and accept the `orElse` parameter on + `singleWhere`. + * Fix: Eliminate a bug where `LruMap` linkage is incorrectly preserved when + items are removed. + +#### 0.28.0+1 - 2018-03-22 + + * Remove use of `Maps.mapToString` in `LruMap`. +   * Add `@visibleForTesting` annotation in `AvlTreeSet`. #### 0.28.0 - 2018-01-19 diff --git a/README.md b/README.md index 1b9fb262..0799a5ec 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Add Quiver to your project's pubspec.yaml file and run `pub get`. We recommend the following version constraint: dependencies: - quiver: '>=0.28.0 <0.29.0' + quiver: '>=0.28.1 <0.29.0' # Main Libraries diff --git a/lib/src/collection/bimap.dart b/lib/src/collection/bimap.dart index 8ff32c73..2a3f1958 100644 --- a/lib/src/collection/bimap.dart +++ b/lib/src/collection/bimap.dart @@ -70,13 +70,13 @@ class HashBiMap implements BiMap { int get length => _map.length; Iterable get values => _inverse.keys; + BiMap get inverse => _cached ??= new HashBiMap._from(_inverse, _map); + @override - // TODO: Dart 2.0 requires this method to be implemented. - // ignore: override_on_non_overriding_method - void addEntries(Iterable entries) { - // Change Iterable to Iterable> when - // the MapEntry class has been added. - throw new UnimplementedError("addEntries"); + void addEntries(Iterable> entries) { + for (var entry in entries) { + _add(entry.key, entry.value, false); + } } @override @@ -87,29 +87,11 @@ class HashBiMap implements BiMap { } @override - // TODO: Dart 2.0 requires this method to be implemented. - // ignore: override_on_non_overriding_getter - Iterable get entries { - // Change Iterable to Iterable> when - // the MapEntry class has been added. - throw new UnimplementedError("entries"); - } - - BiMap get inverse { - if (_cached == null) { - _cached = new HashBiMap._from(_inverse, _map); - } - return _cached; - } + Iterable> get entries => _map.entries; @override - // TODO: Dart 2.0 requires this method to be implemented. - // ignore: override_on_non_overriding_method - Map map(Object transform(K key, V value)) { - // Change Object to MapEntry when - // the MapEntry class has been added. - throw new UnimplementedError("map"); - } + Map map(MapEntry transform(K key, V value)) => + _map.map(transform); V putIfAbsent(K key, V ifAbsent()) { var value = _map[key]; @@ -124,10 +106,9 @@ class HashBiMap implements BiMap { } @override - // TODO: Dart 2.0 requires this method to be implemented. - // ignore: override_on_non_overriding_method void removeWhere(bool test(K key, V value)) { - throw new UnimplementedError("removeWhere"); + _inverse.removeWhere((v, k) => test(k, v)); + _map.removeWhere(test); } @override @@ -138,17 +119,22 @@ class HashBiMap implements BiMap { } @override - // TODO: Dart 2.0 requires this method to be implemented. - // ignore: override_on_non_overriding_method V update(K key, V update(V value), {V ifAbsent()}) { - throw new UnimplementedError("update"); + var value = _map[key]; + if (value != null) { + return _add(key, update(value), true); + } else { + if (ifAbsent == null) + throw new ArgumentError.value(key, 'key', 'Key not in map'); + return _add(key, ifAbsent(), false); + } } @override - // TODO: Dart 2.0 requires this method to be implemented. - // ignore: override_on_non_overriding_method void updateAll(V update(K key, V value)) { - throw new UnimplementedError("updateAll"); + for (var key in this.keys) { + _add(key, update(key, _map[key]), true); + } } void clear() { diff --git a/lib/src/collection/lru_map.dart b/lib/src/collection/lru_map.dart index e9bd758a..ddf01fc6 100644 --- a/lib/src/collection/lru_map.dart +++ b/lib/src/collection/lru_map.dart @@ -227,11 +227,13 @@ class LinkedLruHashMap implements LruMap { _head = _tail = null; } else if (entry == _head) { _head = _head.next; + _head?.previous = null; } else if (entry == _tail) { - _tail.previous.next = null; _tail = _tail.previous; + _tail?.next = null; } else { entry.previous.next = entry.next; + entry.next.previous = entry.previous; } return entry.value; } diff --git a/lib/src/collection/multimap.dart b/lib/src/collection/multimap.dart index 80c59930..c04f2a3a 100644 --- a/lib/src/collection/multimap.dart +++ b/lib/src/collection/multimap.dart @@ -309,51 +309,36 @@ class _WrappedMap> implements Map { } @override - // TODO: Dart 2.0 requires this method to be implemented. - // ignore: override_on_non_overriding_getter - Iterable get entries { - // Change Iterable to Iterable> when - // the MapEntry class has been added. - throw new UnimplementedError("entries"); - } + Iterable> get entries => _multimap._map.entries; @override - // TODO: Dart 2.0 requires this method to be implemented. - // ignore: override_on_non_overriding_method - void addEntries(Iterable entries) { - // Change Iterable to Iterable> when - // the MapEntry class has been added. - throw new UnimplementedError("addEntries"); + void addEntries(Iterable> entries) { + throw new UnsupportedError("Insert unsupported on map view"); } @override - // TODO: Dart 2.0 requires this method to be implemented. - // ignore: override_on_non_overriding_method - Map map(Object transform(K key, C value)) { - // Change Object to MapEntry when - // the MapEntry class has been added. - throw new UnimplementedError("map"); - } + Map map(MapEntry transform(K key, C value)) => + _multimap._map.map(transform); @override - // TODO: Dart 2.0 requires this method to be implemented. - // ignore: override_on_non_overriding_method C update(K key, C update(C value), {C ifAbsent()}) { - throw new UnimplementedError("update"); + throw new UnsupportedError("Update unsupported on map view"); } @override - // TODO: Dart 2.0 requires this method to be implemented. - // ignore: override_on_non_overriding_method void updateAll(C update(K key, C value)) { - throw new UnimplementedError("updateAll"); + throw new UnsupportedError("Update unsupported on map view"); } @override - // TODO: Dart 2.0 requires this method to be implemented. - // ignore: override_on_non_overriding_method void removeWhere(bool test(K key, C value)) { - throw new UnimplementedError("removeWhere"); + var keysToRemove = []; + for (var key in keys) { + if (test(key, this[key])) keysToRemove.add(key); + } + for (var key in keysToRemove) { + _multimap.removeAll(key); + } } } @@ -606,8 +591,6 @@ class _WrappedList extends _WrappedIterable> } @override - // TODO: Dart 2.0 requires this method to be implemented. - // ignore: override_on_non_overriding_setter void set first(V value) { if (this.isEmpty) throw new RangeError.index(0, this); this[0] = value; diff --git a/pubspec.yaml b/pubspec.yaml index 4346d76f..01afc809 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: quiver -version: 0.28.0 +version: 0.28.1 authors: - Justin Fagnani - Yegor Jbanov diff --git a/test/collection/bimap_test.dart b/test/collection/bimap_test.dart index 125bc69b..8e693b37 100644 --- a/test/collection/bimap_test.dart +++ b/test/collection/bimap_test.dart @@ -161,6 +161,14 @@ main() { map.remove(k1); expect(map.containsKey(k1), false); expect(map.inverse.containsKey(v1), false); + + map[k1] = v1; + map[k2] = v2; + map.removeWhere((k, v) => v.isOdd); + expect(map.containsKey(k1), false); + expect(map.containsKey(k2), true); + expect(map.inverse.containsKey(v1), false); + expect(map.inverse.containsKey(v2), true); }); test('should not contain mappings removed from its inverse', () { @@ -168,6 +176,48 @@ main() { map.inverse.remove(v1); expect(map.containsKey(k1), false); expect(map.inverse.containsKey(v1), false); + + map[k1] = v1; + map[k2] = v2; + map.inverse.removeWhere((v, k) => v.isOdd); + expect(map.containsKey(k1), false); + expect(map.containsKey(k2), true); + expect(map.inverse.containsKey(v1), false); + expect(map.inverse.containsKey(v2), true); + }); + + test('should update both sides', () { + map[k1] = v1; + map.update(k1, (v) => v + 1); + expect(map[k1], equals(v1 + 1)); + expect(map.inverse[v1 + 1], equals(k1)); + + map[k1] = v1; + map.inverse.update(v1, (k) => '_$k'); + expect(map['_$k1'], equals(v1)); + expect(map.inverse[v1], equals('_$k1')); + }); + + test('should update absent key values', () { + map[k1] = v1; + map.update(k2, (v) => v + 1, ifAbsent: () => 0); + expect(map[k2], equals(0)); + expect(map.inverse[0], equals(k2)); + + map[k1] = v1; + map.inverse.update(v2, (k) => '_$k', ifAbsent: () => '_X'); + expect(map['_X'], equals(v2)); + expect(map.inverse[v2], equals('_X')); + }); + + test('should update all values', () { + map[k1] = v1; + map[k2] = v2; + map.updateAll((k, v) => v + k.length); + expect(map[k1], equals(3)); + expect(map[k2], equals(4)); + expect(map.inverse[3], equals(k1)); + expect(map.inverse[4], equals(k2)); }); test('should be empty after clear', () { @@ -230,6 +280,49 @@ main() { expect(map.inverse.values, unorderedEquals([k1, k2])); }); + test('should add entries', () { + map.addEntries([new MapEntry(k1, v1)]); + expect(map[k1], equals(v1)); + expect(map.inverse[v1], equals(k1)); + + map.inverse.addEntries([new MapEntry(v2, k2)]); + expect(map[k2], equals(v2)); + expect(map.inverse[v2], equals(k2)); + }); + + test('should get entries', () { + map[k1] = v1; + map.inverse[v2] = k2; + + var mapEntries = map.entries; + expect(mapEntries, hasLength(2)); + // MapEntry objects are not equal to each other; cannot use `contains`. :( + expect(mapEntries.singleWhere((e) => e.key == k1).value, equals(v1)); + expect(mapEntries.singleWhere((e) => e.key == k2).value, equals(v2)); + + var inverseEntries = map.inverse.entries; + expect(inverseEntries, hasLength(2)); + expect(inverseEntries.singleWhere((e) => e.key == v1).value, equals(k1)); + expect(inverseEntries.singleWhere((e) => e.key == v2).value, equals(k2)); + }); + + test('should map mappings', () { + map[k1] = v1; + map[k2] = v2; + + var mapped = map.map((k, v) => new MapEntry(k.toUpperCase(), '$k / $v')); + expect(mapped, contains('K1')); + expect(mapped, contains('K2')); + expect(mapped['K1'], equals('k1 / 1')); + expect(mapped['K2'], equals('k2 / 2')); + + var mapped2 = map.inverse.map((v, k) => new MapEntry('$v$v', k.length)); + expect(mapped2, contains('11')); + expect(mapped2, contains('22')); + expect(mapped2['11'], equals(2)); + expect(mapped2['22'], equals(2)); + }); + test('should add mappings via putIfAbsent if absent', () { map.putIfAbsent(k1, () => v1); expect(map[k1], v1); diff --git a/test/collection/lru_map_test.dart b/test/collection/lru_map_test.dart index 5baa02a9..c9eb0a13 100644 --- a/test/collection/lru_map_test.dart +++ b/test/collection/lru_map_test.dart @@ -275,6 +275,15 @@ void main() { lruMap.removeWhere((k, v) => v.contains('A')); expect(lruMap.keys.toList(), ['C', 'B']); }); + + test('linkage correctly preserved on remove', () { + lruMap.remove('B'); + lruMap['A']; + + final keys = []; + lruMap.forEach((String k, String v) => keys.add(k)); + expect(keys, ['A', 'C']); + }); }); test('the linked list is mutated when promoting an item in the middle', () { diff --git a/test/collection/multimap_test.dart b/test/collection/multimap_test.dart index e0288bcc..7f139a8e 100644 --- a/test/collection/multimap_test.dart +++ b/test/collection/multimap_test.dart @@ -76,6 +76,21 @@ void main() { expect(() => map['k1'] = ['1', '2', '3'], throwsUnsupportedError); }); + test('addEntries should throw UnsupportedError', () { + expect(() => map.addEntries(>>[]), + throwsUnsupportedError); + }); + + test('update should throw UnsupportedError', () { + expect(() => map.update('k1', (_) => ['1', '2', '3']), + throwsUnsupportedError); + }); + + test('updateAll should throw UnsupportedError', () { + expect(() => map.updateAll((_, __) => ['1', '2', '3']), + throwsUnsupportedError); + }); + test('containsKey() should return false for missing key', () { expect(map.containsKey('k3'), isFalse); }); @@ -928,6 +943,27 @@ void main() { expect(mmap.containsKey('k1'), false); }); + test('should something about entries in returned map view', () { + var mmap = new SetMultimap() + ..add('k1', 'v1') + ..add('k1', 'v2'); + Map map = mmap.asMap(); + expect(map.entries, hasLength(1)); + expect(map.entries.single.key, equals('k1')); + expect(map.entries.single.value, unorderedEquals(['v1', 'v2'])); + }); + + test('should map from returned map view', () { + var mmap = new SetMultimap() + ..add('k1', 'v1') + ..add('k1', 'v2'); + Map map = mmap.asMap(); + var newMap = map.map((k, v) => new MapEntry(k, v.join(','))); + expect(newMap, hasLength(1)); + expect(newMap, contains('k1')); + expect(newMap, containsValue('v1,v2')); + }); + test('should reflect clearing of returned map view', () { var mmap = new SetMultimap() ..add('k1', 'v1')