From 672e51afbae282983ed0c6a56b4b6cb10ef1f878 Mon Sep 17 00:00:00 2001 From: Jonas Finnemann Jensen Date: Fri, 1 Nov 2024 14:31:56 +0100 Subject: [PATCH 1/2] Introduce `IterableMapEntryExtension` for use with `Map.entries`. **Example**: ```dart final myMap = { 'foo': 42, 'bar': -1, 'foobar': 21, }; // myMap without negative values myMap.entries.whereValue((v) => v >= 0).toMap(); // myMap, but only keys that start with 'foo' myMap.entries.whereKey((k) => k.startsWith('foo')).toMap(); ``` --- pkgs/collection/CHANGELOG.md | 4 + .../lib/src/iterable_extensions.dart | 27 +++++++ pkgs/collection/pubspec.yaml | 2 +- pkgs/collection/test/extensions_test.dart | 81 +++++++++++++++++++ 4 files changed, 113 insertions(+), 1 deletion(-) diff --git a/pkgs/collection/CHANGELOG.md b/pkgs/collection/CHANGELOG.md index 021dd937..239efd57 100644 --- a/pkgs/collection/CHANGELOG.md +++ b/pkgs/collection/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.19.1-wip +- Add `IterableMapEntryExtension` for working on `Map` as a list of pairs, using + `Map.entries`. + ## 1.19.1 - Move to `dart-lang/core` monorepo. diff --git a/pkgs/collection/lib/src/iterable_extensions.dart b/pkgs/collection/lib/src/iterable_extensions.dart index e2050062..b05e3fa7 100644 --- a/pkgs/collection/lib/src/iterable_extensions.dart +++ b/pkgs/collection/lib/src/iterable_extensions.dart @@ -914,6 +914,33 @@ extension IterableIterableExtension on Iterable> { }; } +/// Extension on iterables of [MapEntry]. +/// +/// An [Iterable] is obtained using [Map.entries], these extensions +/// make it easy to work on a [Map] as a list of pairs. +extension IterableMapEntryExtension on Iterable> { + /// Creates a new lazy [Iterable] with all elements whose [MapEntry.key] + /// satisfy the predicate [test]. + Iterable> whereKey(bool Function(K) test) => + where((e) => test(e.key)); + + /// Creates a new lazy [Iterable] with all elements whose [MapEntry.value] + /// satisfy the predicate [test]. + Iterable> whereValue(bool Function(V) test) => + where((e) => test(e.value)); + + /// Create an new lazy [Iterable] with [MapEntry.key] from all elements. + Iterable get keys => map((e) => e.key); + + /// Create an new lazy [Iterable] with [MapEntry.value] from all elements. + Iterable get values => map((e) => e.value); + + /// Create a [Map] from all elements. + /// + /// This is a short-hand for [Map.fromEntries]. + Map toMap() => Map.fromEntries(this); +} + /// Extensions that apply to iterables of [Comparable] elements. /// /// These operations can assume that the elements have a natural ordering, diff --git a/pkgs/collection/pubspec.yaml b/pkgs/collection/pubspec.yaml index c1b16334..4e74ac7a 100644 --- a/pkgs/collection/pubspec.yaml +++ b/pkgs/collection/pubspec.yaml @@ -1,5 +1,5 @@ name: collection -version: 1.19.1 +version: 1.19.1-wip description: >- Collections and utilities functions and classes related to collections. repository: https://github.com/dart-lang/core/tree/main/pkgs/collection diff --git a/pkgs/collection/test/extensions_test.dart b/pkgs/collection/test/extensions_test.dart index 9940e1d4..1d7a0ec0 100644 --- a/pkgs/collection/test/extensions_test.dart +++ b/pkgs/collection/test/extensions_test.dart @@ -1122,6 +1122,87 @@ void main() { }); }); }); + group('of MapEntry', () { + group('.whereKey', () { + test('empty', () { + expect({}.entries.whereKey(unreachable), isEmpty); + }); + test('single', () { + expect({'a': 1}.entries.whereKey((k) => k == 'a').toMap(), {'a': 1}); + expect({'a': 1}.entries.whereKey((k) => k == 'b').toMap(), isEmpty); + }); + test('multiple', () { + expect( + {'a': 1, 'b': 2}.entries.whereKey((k) => k == 'a').toMap(), + {'a': 1}, + ); + expect( + {'a': 1, 'b': 2}.entries.whereKey((k) => k == 'b').toMap(), + {'b': 2}, + ); + expect( + {'a': 1, 'b': 2}.entries.whereKey((k) => k != 'c').toMap(), + {'a': 1, 'b': 2}, + ); + }); + }); + group('.whereValue', () { + test('empty', () { + expect({}.entries.whereValue(unreachable), isEmpty); + }); + test('single', () { + expect({'a': 1}.entries.whereValue((v) => v == 1).toMap(), {'a': 1}); + expect({'a': 1}.entries.whereValue((v) => v == 2).toMap(), isEmpty); + }); + test('multiple', () { + expect( + {'a': 1, 'b': 2}.entries.whereValue((v) => v == 1).toMap(), + {'a': 1}, + ); + expect( + {'a': 1, 'b': 2}.entries.whereValue((v) => v == 2).toMap(), + {'b': 2}, + ); + expect( + {'a': 1, 'b': 2}.entries.whereValue((v) => v != 3).toMap(), + {'a': 1, 'b': 2}, + ); + }); + }); + group('.keys', () { + test('empty', () { + expect({}.entries.keys, isEmpty); + }); + test('single', () { + expect({'a': 1}.entries.keys, ['a']); + }); + test('multiple', () { + expect({'a': 1, 'b': 2}.entries.keys, ['a', 'b']); + }); + }); + group('.values', () { + test('empty', () { + expect({}.entries.values, isEmpty); + }); + test('single', () { + expect({'a': 1}.entries.values, [1]); + }); + test('multiple', () { + expect({'a': 1, 'b': 2}.entries.values, [1, 2]); + }); + }); + group('.toMap', () { + test('empty', () { + expect({}.entries.toMap(), {}); + }); + test('single', () { + expect({'a': 1}.entries.toMap(), {'a': 1}); + }); + test('multiple', () { + expect({'a': 1, 'b': 2}.entries.toMap(), {'a': 1, 'b': 2}); + }); + }); + }); group('of comparable', () { group('.min', () { test('empty', () { From b9dc19ca3a3940ec48f78333c8cd9ebf947cf48c Mon Sep 17 00:00:00 2001 From: Jonas Finnemann Jensen Date: Tue, 5 Nov 2024 16:25:52 +0100 Subject: [PATCH 2/2] Fix review comments --- .../lib/src/iterable_extensions.dart | 30 ++-- pkgs/collection/test/extensions_test.dart | 147 ++++++++++++++---- 2 files changed, 140 insertions(+), 37 deletions(-) diff --git a/pkgs/collection/lib/src/iterable_extensions.dart b/pkgs/collection/lib/src/iterable_extensions.dart index b05e3fa7..10f46760 100644 --- a/pkgs/collection/lib/src/iterable_extensions.dart +++ b/pkgs/collection/lib/src/iterable_extensions.dart @@ -916,29 +916,39 @@ extension IterableIterableExtension on Iterable> { /// Extension on iterables of [MapEntry]. /// -/// An [Iterable] is obtained using [Map.entries], these extensions -/// make it easy to work on a [Map] as a list of pairs. +/// An [Iterable] is obtained using [Map.entries]. These extensions +/// facilitates working directly on the entries of a [Map]. extension IterableMapEntryExtension on Iterable> { - /// Creates a new lazy [Iterable] with all elements whose [MapEntry.key] - /// satisfy the predicate [test]. + /// The elements whose [MapEntry.key] values satisfy [test]. + /// + /// The resulting iterable is lazily computing its elements + /// based on the elements this iterable. Iterable> whereKey(bool Function(K) test) => where((e) => test(e.key)); - /// Creates a new lazy [Iterable] with all elements whose [MapEntry.value] - /// satisfy the predicate [test]. + /// The elements whose [MapEntry.value] values satisfy [test]. + /// + /// The resulting iterable is lazily computing its elements + /// based on the elements this iterable. Iterable> whereValue(bool Function(V) test) => where((e) => test(e.value)); - /// Create an new lazy [Iterable] with [MapEntry.key] from all elements. + /// A new lazy [Iterable] of the [MapEntry.key]s of these entries. + /// + /// Do not use this getter as `map.entries.keys`, just use `map.keys` + /// directly. Iterable get keys => map((e) => e.key); - /// Create an new lazy [Iterable] with [MapEntry.value] from all elements. + /// A new lazy [Iterable] of the [MapEntry.value]s of these entries. + /// + /// Do not use this getter as `map.entries.values`, just use `map.values` + /// directly. Iterable get values => map((e) => e.value); - /// Create a [Map] from all elements. + /// Create a [Map] from all elements. /// /// This is a short-hand for [Map.fromEntries]. - Map toMap() => Map.fromEntries(this); + Map toMap() => Map.fromEntries(this); } /// Extensions that apply to iterables of [Comparable] elements. diff --git a/pkgs/collection/test/extensions_test.dart b/pkgs/collection/test/extensions_test.dart index 1d7a0ec0..6c5b45f1 100644 --- a/pkgs/collection/test/extensions_test.dart +++ b/pkgs/collection/test/extensions_test.dart @@ -1125,81 +1125,174 @@ void main() { group('of MapEntry', () { group('.whereKey', () { test('empty', () { - expect({}.entries.whereKey(unreachable), isEmpty); + expect( + iterable(>[]).whereKey(unreachable), + isEmpty, + ); }); test('single', () { - expect({'a': 1}.entries.whereKey((k) => k == 'a').toMap(), {'a': 1}); - expect({'a': 1}.entries.whereKey((k) => k == 'b').toMap(), isEmpty); + expect( + iterable([const MapEntry('a', 1)]).whereKey((k) => k == 'a'), + [const MapEntry('a', 1)], + ); + expect( + iterable([const MapEntry('a', 1)]).whereKey((k) => k == 'b'), + isEmpty, + ); }); test('multiple', () { expect( - {'a': 1, 'b': 2}.entries.whereKey((k) => k == 'a').toMap(), - {'a': 1}, + iterable([ + const MapEntry('a', 1), + const MapEntry('b', 2), + ]).whereKey((k) => k == 'a'), + [const MapEntry('a', 1)], ); expect( - {'a': 1, 'b': 2}.entries.whereKey((k) => k == 'b').toMap(), - {'b': 2}, + iterable([ + const MapEntry('a', 1), + const MapEntry('b', 2), + ]).whereKey((k) => k == 'b'), + [const MapEntry('b', 2)], ); expect( - {'a': 1, 'b': 2}.entries.whereKey((k) => k != 'c').toMap(), - {'a': 1, 'b': 2}, + iterable([ + const MapEntry('a', 1), + const MapEntry('b', 2), + ]).whereKey((k) => k != 'c'), + [const MapEntry('a', 1), const MapEntry('b', 2)], + ); + expect( + iterable([ + const MapEntry('a', 1), + const MapEntry('b', 2), + const MapEntry('a', 3), + ]).whereKey((k) => k == 'a'), + [const MapEntry('a', 1), const MapEntry('a', 3)], ); }); }); group('.whereValue', () { test('empty', () { - expect({}.entries.whereValue(unreachable), isEmpty); + expect( + iterable(>[]).whereValue(unreachable), + isEmpty, + ); }); test('single', () { - expect({'a': 1}.entries.whereValue((v) => v == 1).toMap(), {'a': 1}); - expect({'a': 1}.entries.whereValue((v) => v == 2).toMap(), isEmpty); + expect( + iterable([const MapEntry('a', 1)]).whereValue((v) => v == 1), + [const MapEntry('a', 1)], + ); + expect( + iterable([const MapEntry('a', 1)]).whereValue((v) => v == 2), + isEmpty, + ); }); test('multiple', () { expect( - {'a': 1, 'b': 2}.entries.whereValue((v) => v == 1).toMap(), - {'a': 1}, + iterable([ + const MapEntry('a', 1), + const MapEntry('b', 2), + ]).whereValue((v) => v == 1), + [const MapEntry('a', 1)], ); expect( - {'a': 1, 'b': 2}.entries.whereValue((v) => v == 2).toMap(), - {'b': 2}, + iterable([ + const MapEntry('a', 1), + const MapEntry('b', 2), + ]).whereValue((v) => v == 2), + [const MapEntry('b', 2)], ); expect( - {'a': 1, 'b': 2}.entries.whereValue((v) => v != 3).toMap(), - {'a': 1, 'b': 2}, + iterable([ + const MapEntry('a', 1), + const MapEntry('b', 2), + ]).whereValue((v) => v != 3), + [const MapEntry('a', 1), const MapEntry('b', 2)], + ); + expect( + iterable([ + const MapEntry('a', 1), + const MapEntry('b', 2), + const MapEntry('c', 1), + ]).whereValue((v) => v == 1), + [const MapEntry('a', 1), const MapEntry('c', 1)], + ); + expect( + iterable([ + const MapEntry('a', 1), + const MapEntry('b', 2), + const MapEntry('a', 1), + ]).whereValue((v) => v == 1), + [const MapEntry('a', 1), const MapEntry('a', 1)], ); }); }); group('.keys', () { test('empty', () { - expect({}.entries.keys, isEmpty); + expect(iterable(>[]).keys, isEmpty); }); test('single', () { - expect({'a': 1}.entries.keys, ['a']); + expect(iterable([const MapEntry('a', 1)]).keys, ['a']); }); test('multiple', () { - expect({'a': 1, 'b': 2}.entries.keys, ['a', 'b']); + expect( + iterable([const MapEntry('a', 1), const MapEntry('b', 2)]).keys, + ['a', 'b'], + ); + expect( + iterable([ + const MapEntry('a', 1), + const MapEntry('b', 2), + const MapEntry('a', 3), + ]).keys, + ['a', 'b', 'a'], + ); }); }); group('.values', () { test('empty', () { - expect({}.entries.values, isEmpty); + expect(iterable(>[]).values, isEmpty); }); test('single', () { - expect({'a': 1}.entries.values, [1]); + expect(iterable([const MapEntry('a', 1)]).values, [1]); }); test('multiple', () { - expect({'a': 1, 'b': 2}.entries.values, [1, 2]); + expect( + iterable([const MapEntry('a', 1), const MapEntry('b', 2)]).values, + [1, 2], + ); + expect( + iterable([ + const MapEntry('a', 1), + const MapEntry('b', 2), + const MapEntry('a', 3), + ]).values, + [1, 2, 3], + ); }); }); group('.toMap', () { test('empty', () { - expect({}.entries.toMap(), {}); + expect(iterable(>[]).toMap(), {}); }); test('single', () { - expect({'a': 1}.entries.toMap(), {'a': 1}); + expect(iterable([const MapEntry('a', 1)]).toMap(), {'a': 1}); }); test('multiple', () { - expect({'a': 1, 'b': 2}.entries.toMap(), {'a': 1, 'b': 2}); + expect( + iterable([const MapEntry('a', 1), const MapEntry('b', 2)]).toMap(), + {'a': 1, 'b': 2}, + ); + expect( + iterable([ + const MapEntry('a', 1), + const MapEntry('b', 2), + const MapEntry('a', 3), + ]).toMap(), + {'b': 2, 'a': 3}, + ); }); }); });