diff --git a/pkgs/collection/CHANGELOG.md b/pkgs/collection/CHANGELOG.md index 743fddf2..6517421e 100644 --- a/pkgs/collection/CHANGELOG.md +++ b/pkgs/collection/CHANGELOG.md @@ -1,5 +1,7 @@ ## 1.20.0-wip +- Adds `separated` and `separatedList` extension methods to `Iterable`. +- Adds `separate` extension method to `List` - Add `IterableMapEntryExtension` for working on `Map` as a list of pairs, using `Map.entries`. - Explicitly mark `BoolList` as `abstract interface` diff --git a/pkgs/collection/lib/src/iterable_extensions.dart b/pkgs/collection/lib/src/iterable_extensions.dart index d9516e67..f3449f9e 100644 --- a/pkgs/collection/lib/src/iterable_extensions.dart +++ b/pkgs/collection/lib/src/iterable_extensions.dart @@ -56,6 +56,112 @@ extension IterableExtension on Iterable { return chosen; } + /// The elements of this iterable separated by [separator]. + /// + /// Emits the same elements as this iterable, and also emits + /// a [separator] between any two of those elements. + /// + /// If [before] is set to `true`, a [separator] is also + /// emitted before the first element. + /// If [after] is set to `true`, a [separator] is also + /// emitted after the last element. + /// + /// If this iterable is empty, [before] and [after] have no effect. + /// + /// Example: + /// ```dart + /// print([1, 2, 3].separated(-1)); // (1, -1, 2, -1, 3) + /// print([1].separated(-1)); // (1) + /// print([].separated(-1)); // () + /// + /// print([1, 2, 3].separated( + /// -1, + /// before: true, + /// )); // (-1, 1, -1, 2, -1, 3) + /// + /// print([1].separated( + /// -1, + /// before: true, + /// after: true, + /// )); // (-1, 1, -1) + /// + /// print([].separated( + /// -1, + /// before: true, + /// after: true, + /// )); // () + /// ``` + Iterable separated(T separator, + {bool before = false, bool after = false}) sync* { + var iterator = this.iterator; + if (iterator.moveNext()) { + if (before) yield separator; + while (true) { + yield iterator.current; + if (iterator.moveNext()) { + yield separator; + } else { + break; + } + } + if (after) yield separator; + } + } + + /// Creates new list with the elements of this list separated by [separator]. + /// + /// Returns a new list which contains the same elements as this list, + /// with a [separator] between any two of those elements. + /// + /// If [before] is set to `true`, a [separator] is also + /// added before the first element. + /// If [after] is set to `true`, a [separator] is also + /// added after the last element. + /// + /// If this list is empty, [before] and [after] have no effect. + /// + /// Example: + /// ```dart + /// print([1, 2, 3].separatedList(-1)); // [1, -1, 2, -1, 3] + /// print([1].separatedList(-1)); // [1] + /// print([].separatedList(-1)); // [] + /// + /// print([1, 2, 3].separatedList( + /// -1, + /// before: true, + /// )); // [-1, 1, -1, 2, -1, 3] + /// + /// print([1].separatedList( + /// -1, + /// before: true, + /// after: true, + /// )); // [-1, 1, -1] + /// + /// print([].separatedList( + /// -1, + /// before: true, + /// after: true, + /// )); // [] + /// ``` + List separatedList(T separator, + {bool before = false, bool after = false}) { + var result = []; + var iterator = this.iterator; + if (iterator.moveNext()) { + if (before) result.add(separator); + while (true) { + result.add(iterator.current); + if (iterator.moveNext()) { + result.add(separator); + } else { + break; + } + } + if (after) result.add(separator); + } + return result; + } + /// The elements that do not satisfy [test]. Iterable whereNot(bool Function(T element) test) => where((element) => !test(element)); diff --git a/pkgs/collection/lib/src/list_extensions.dart b/pkgs/collection/lib/src/list_extensions.dart index a301a1ec..51dde7bb 100644 --- a/pkgs/collection/lib/src/list_extensions.dart +++ b/pkgs/collection/lib/src/list_extensions.dart @@ -327,6 +327,122 @@ extension ListExtensions on List { yield slice(i, min(i + length, this.length)); } } + + /// Creates new list with the elements of this list separated by [separator]. + /// + /// Returns a new list which contains the same elements as this list, + /// with a [separator] between any two of those elements. + /// + /// If [before] is set to `true`, a [separator] is also + /// added before the first element. + /// If [after] is set to `true`, a [separator] is also + /// added after the last element. + /// + /// If this list is empty, [before] and [after] have no effect. + /// + /// Example: + /// ```dart + /// print([1, 2, 3].separatedList(-1)); // [1, -1, 2, -1, 3] + /// print([1].separatedList(-1)); // [1] + /// print([].separatedList(-1)); // [] + /// + /// print([1, 2, 3].separatedList( + /// -1 + /// before: true, + /// )); // [-1, 1, -1, 2, -1, 3] + /// + /// print([1].separatedList( + /// -1 + /// before: true, + /// after: true, + /// )); // [-1, 1, -1] + /// + /// print([].separatedList( + /// -1 + /// before: true, + /// after: true, + /// )); // [] + /// ``` + List separatedList(E separator, + {bool before = false, bool after = false}) => + isEmpty + ? [] + : [ + if (!before) this[0], + for (var i = before ? 0 : 1; i < length; i++) ...[ + separator, + this[i], + ], + if (after) separator + ]; + + /// Inserts [separator] between elements of this list. + /// + /// Afterwards, the list will contains all the original elements, + /// with a [separator] between any two of those elements. + /// + /// If [before] is set to `true`, a [separator] is also + /// inserted before the first element. + /// If [after] is set to `true`, a [separator] is also + /// added after the last element. + /// + /// If this list is empty, [before] and [after] have no effect. + /// + /// Example: + /// ```dart + /// print([1, 2, 3]..separate(-1)); // [1, -1, 2, -1, 3] + /// print([1]..separate(-1)); // [1] + /// print([]..separate(-1)); // [] + /// + /// print([1, 2, 3]..separate( + /// -1, + /// before: true, + /// )); // [-1, 1, -1, 2, -1, 3] + /// + /// print([1]..separate( + /// -1, + /// before: true, + /// after: true, + /// )); // [-1, 1, -1] + /// + /// print([]..separate( + /// -1, + /// before: true, + /// after: true, + /// )); // [] + /// ``` + void separate(E separator, {bool before = false, bool after = false}) { + var length = this.length; + if (length == 0) return; + // New position of first element. + var newFirst = before ? 1 : 0; + // New position of last element. + var newLast = length * 2 - (newFirst ^ 1); + + var splitIndex = length - newFirst; + var cursor = splitIndex >> 1; + if (splitIndex.isEven) { + add(this[cursor]); + } + cursor++; + while (this.length < newLast) { + add(separator); + add(this[cursor++]); + } + assert(cursor == length); + if (after) add(separator); + + cursor = splitIndex >> 1; + if (splitIndex.isOdd) { + this[--length] = this[cursor]; + } + cursor--; + for (var i = length; i > 1;) { + this[--i] = separator; + this[--i] = this[cursor--]; + } + if (newFirst != 0) this[0] = separator; + } } /// Various extensions on lists of comparable elements. diff --git a/pkgs/collection/test/separate_extensions_test.dart b/pkgs/collection/test/separate_extensions_test.dart new file mode 100644 index 00000000..cccf8b5c --- /dev/null +++ b/pkgs/collection/test/separate_extensions_test.dart @@ -0,0 +1,291 @@ +// Copyright (c) 2025, 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'; + +void main() { + group('Iterable.separated', () { + group('empty', () { + test('', () { + expect(iterable([]).separated(42), isEmpty); + }); + test('before', () { + expect(iterable([]).separated(42, before: true), isEmpty); + }); + test('after', () { + expect(iterable([]).separated(42, after: true), isEmpty); + }); + test('before and after', () { + expect(iterable([]).separated(42, before: true, after: true), + isEmpty); + }); + test('List', () { + var separatedList = [].separated(42); + expect(separatedList, isNot(isA())); + expect(separatedList, isEmpty); + }); + }); + group('Singleton', () { + test('', () { + expect(iterable([1]).separated(42), [1]); + }); + test('before', () { + expect(iterable([1]).separated(42, before: true), [42, 1]); + }); + test('after', () { + expect(iterable([1]).separated(42, after: true), [1, 42]); + }); + test('before and after', () { + expect(iterable([1]).separated(42, before: true, after: true), + [42, 1, 42]); + }); + test('List', () { + var separatedList = [1].separated(42); + expect(separatedList, isNot(isA())); + expect(separatedList, [1]); + }); + }); + group('Multiple', () { + test('', () { + expect(iterable([1, 2, 3]).separated(42), [1, 42, 2, 42, 3]); + }); + test('before', () { + expect(iterable([1, 2, 3]).separated(42, before: true), + [42, 1, 42, 2, 42, 3]); + }); + test('after', () { + expect(iterable([1, 2, 3]).separated(42, after: true), + [1, 42, 2, 42, 3, 42]); + }); + test('before and after', () { + expect( + iterable([1, 2, 3]).separated(42, before: true, after: true), + [42, 1, 42, 2, 42, 3, 42]); + }); + test('List', () { + var separatedList = [1, 2, 3].separated(42); + expect(separatedList, isNot(isA())); + expect(separatedList, [1, 42, 2, 42, 3]); + }); + }); + test('nulls', () { + expect(iterable([null, 2, null]).separated(null), + [null, null, 2, null, null]); + }); + test('upcast receiver', () { + var source = [1, 2, 3]; + expect(iterable(source).separated(3.14), [1, 3.14, 2, 3.14, 3]); + }); + }); + + // ------------------------------------------------------------------ + group('Iterable.separatedList', () { + group('empty', () { + test('', () { + expect(iterable([]).separatedList(42), isEmpty); + }); + test('before', () { + expect(iterable([]).separatedList(42, before: true), isEmpty); + }); + test('after', () { + expect(iterable([]).separatedList(42, after: true), isEmpty); + }); + test('before and after', () { + expect(iterable([]).separatedList(42, before: true, after: true), + isEmpty); + }); + }); + group('Singleton', () { + test('', () { + expect(iterable([1]).separatedList(42), [1]); + }); + test('before', () { + expect(iterable([1]).separatedList(42, before: true), [42, 1]); + }); + test('after', () { + expect(iterable([1]).separatedList(42, after: true), [1, 42]); + }); + test('before and after', () { + expect(iterable([1]).separatedList(42, before: true, after: true), + [42, 1, 42]); + }); + }); + group('Multiple', () { + test('', () { + expect(iterable([1, 2, 3]).separatedList(42), [1, 42, 2, 42, 3]); + }); + test('before', () { + expect(iterable([1, 2, 3]).separatedList(42, before: true), + [42, 1, 42, 2, 42, 3]); + }); + test('after', () { + expect(iterable([1, 2, 3]).separatedList(42, after: true), + [1, 42, 2, 42, 3, 42]); + }); + test('before and after', () { + expect( + iterable([1, 2, 3]) + .separatedList(42, before: true, after: true), + [42, 1, 42, 2, 42, 3, 42]); + }); + }); + test('nulls', () { + expect(iterable([null, 2, null]).separatedList(null), + [null, null, 2, null, null]); + }); + test('upcast receiver', () { + var source = [1, 2, 3]; + expect(iterable(source).separatedList(3.14), [1, 3.14, 2, 3.14, 3]); + }); + }); + + // ------------------------------------------------------------------ + group('List.separatedList', () { + group('empty', () { + test('', () { + expect([].separatedList(42), isEmpty); + }); + test('before', () { + expect([].separatedList(42, before: true), isEmpty); + }); + test('after', () { + expect([].separatedList(42, after: true), isEmpty); + }); + test('before and after', () { + expect([].separatedList(42, before: true, after: true), isEmpty); + }); + }); + group('Singleton', () { + test('', () { + expect([1].separatedList(42), [1]); + }); + test('before', () { + expect([1].separatedList(42, before: true), [42, 1]); + }); + test('after', () { + expect([1].separatedList(42, after: true), [1, 42]); + }); + test('before and after', () { + expect( + [1].separatedList(42, before: true, after: true), [42, 1, 42]); + }); + }); + group('Multiple', () { + test('', () { + expect([1, 2, 3].separatedList(42), [1, 42, 2, 42, 3]); + }); + test('before', () { + expect([1, 2, 3].separatedList(42, before: true), + [42, 1, 42, 2, 42, 3]); + }); + test('after', () { + expect([1, 2, 3].separatedList(42, after: true), + [1, 42, 2, 42, 3, 42]); + }); + test('before and after', () { + expect([1, 2, 3].separatedList(42, before: true, after: true), + [42, 1, 42, 2, 42, 3, 42]); + }); + }); + test('nulls', () { + expect([null, 2, null].separatedList(null), [null, null, 2, null, null]); + }); + test('upcast receiver', () { + var source = [1, 2, 3]; + expect((source as List).separatedList(3.14), [1, 3.14, 2, 3.14, 3]); + }); + }); + + // ------------------------------------------------------------------ + group('List.separate', () { + group('empty', () { + test('', () { + expect([]..separate(42), isEmpty); + }); + test('before', () { + expect([]..separate(42, before: true), isEmpty); + }); + test('after', () { + expect([]..separate(42, after: true), isEmpty); + }); + test('before and after', () { + expect([]..separate(42, before: true, after: true), isEmpty); + }); + }); + group('Singleton', () { + test('', () { + expect([1]..separate(42), [1]); + }); + test('before', () { + expect([1]..separate(42, before: true), [42, 1]); + }); + test('after', () { + expect([1]..separate(42, after: true), [1, 42]); + }); + test('before and after', () { + expect([1]..separate(42, before: true, after: true), [42, 1, 42]); + }); + }); + group('Multiple', () { + group('odd length', () { + test('', () { + expect([1, 2, 3]..separate(42), [1, 42, 2, 42, 3]); + }); + test('before', () { + expect([1, 2, 3]..separate(42, before: true), + [42, 1, 42, 2, 42, 3]); + }); + test('after', () { + expect( + [1, 2, 3]..separate(42, after: true), [1, 42, 2, 42, 3, 42]); + }); + test('before and after', () { + expect([1, 2, 3]..separate(42, before: true, after: true), + [42, 1, 42, 2, 42, 3, 42]); + }); + }); + group('even length', () { + test('', () { + expect([1, 2, 3, 4]..separate(42), [1, 42, 2, 42, 3, 42, 4]); + }); + test('before', () { + expect([1, 2, 3, 4]..separate(42, before: true), + [42, 1, 42, 2, 42, 3, 42, 4]); + }); + test('after', () { + expect([1, 2, 3, 4]..separate(42, after: true), + [1, 42, 2, 42, 3, 42, 4, 42]); + }); + test('before and after', () { + expect([1, 2, 3, 4]..separate(42, before: true, after: true), + [42, 1, 42, 2, 42, 3, 42, 4, 42]); + }); + }); + }); + test('nulls', () { + expect([null, 2, null]..separate(null), [null, null, 2, null, null]); + }); + test('upcast receiver throws', () { + // Modifying the list is a contravariant operation, + // throws if separator is not valid. + var source = [1, 2, 3]; + expect(() => (source as List).separate(3.14), + throwsA(isA())); + }); + test('upcast receiver succeeds if separator valid', () { + // Modifying the list is a contravariant operation, + // succeeds if the separator is a valid value. + // (All operations are read/write with existing elements + // or the separator, which works when upcast if values are valid.) + var source = [1, 2, 3]; + expect((source as List)..separate(42), [1, 42, 2, 42, 3]); + }); + }); +} + +/// Creates a plain iterable not implementing any other class. +Iterable iterable(Iterable values) sync* { + yield* values; +}