From 57adf311809eb17f00dd480255c166b7f8818e3a Mon Sep 17 00:00:00 2001 From: "Lasse R.H. Nielsen" Date: Fri, 31 Oct 2025 15:31:31 +0100 Subject: [PATCH 1/2] Add `separated`, `separatedList` and `separate` to iterables and lists. Each new function separates the elements of an iterable or list with a separator value of the same type. They allow optionally adding the separator before a first element and/or after a last element. * `separated`: Returns a (lazy) iterable backed by the original elements. * `separatedList`: Creates an (eager) list with the same elements that separated would have returned. (But more efficiently.) (Also has a version specialized for a `List` input.) * `separate`: Modifies a list in-place to add separators around the existing elements. --- pkgs/collection/CHANGELOG.md | 2 + .../lib/src/iterable_extensions.dart | 96 +++++++ pkgs/collection/lib/src/list_extensions.dart | 108 +++++++ .../test/separate_extensions_test.dart | 272 ++++++++++++++++++ 4 files changed, 478 insertions(+) create mode 100644 pkgs/collection/test/separate_extensions_test.dart 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..352f6863 100644 --- a/pkgs/collection/lib/src/iterable_extensions.dart +++ b/pkgs/collection/lib/src/iterable_extensions.dart @@ -56,6 +56,102 @@ 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* { + const emitBefore = 1; + const emitAfter = 2; + var state = before ? emitBefore : 0; + final afterState = emitBefore | (after ? emitAfter : 0); + for (var element in this) { + if (state & emitBefore != 0) yield separator; + state = afterState; + yield element; + } + if (state & emitAfter != 0) 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 hasElement = false; + return [ + for (var element in this) ...[ + if (hasElement || (hasElement = true) && before) separator, + element, + ], + if (hasElement && after) separator + ]; + } + /// 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..cd56118a 100644 --- a/pkgs/collection/lib/src/list_extensions.dart +++ b/pkgs/collection/lib/src/list_extensions.dart @@ -327,6 +327,114 @@ 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]..insertSeparator(-1)); // [1, -1, 2, -1, 3] + /// print([1]..insertSeparator(-1)); // [1] + /// print([]..insertSeparator(-1)); // [] + /// + /// print([1, 2, 3]..insertSeparator( + /// -1 + /// before: true, + /// )); // [-1, 1, -1, 2, -1, 3] + /// + /// print([1]..insertSeparator( + /// -1 + /// before: true, + /// after: true, + /// )); // [-1, 1, -1] + /// + /// print([]..insertSeparator( + /// -1 + /// before: true, + /// after: true, + /// )); // [] + /// ``` + void separate(E separator, {bool before = false, bool after = false}) { + var length = this.length; + if (length == 0) return; + var newLength = length * 2 - 1; + var offset = 0; + if (before) { + newLength++; + offset = 1; + } + if (after) newLength++; + E newElementAt(int index) { + index -= offset; + if (index.isOdd) return separator; + return this[index >> 1]; + } + + for (var i = length; i < newLength; i++) { + add(newElementAt(i)); + } + for (var i = length, firstChanged = offset ^ 1; i > firstChanged;) { + --i; + this[i] = newElementAt(i); + } + } } /// 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..2adccc44 --- /dev/null +++ b/pkgs/collection/test/separate_extensions_test.dart @@ -0,0 +1,272 @@ +// 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', () { + 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]); + }); + }); + 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; +} From fe35c566890e8ad0ac084849af3741cb21b92641 Mon Sep 17 00:00:00 2001 From: "Lasse R.H. Nielsen" Date: Mon, 10 Nov 2025 18:36:57 +0100 Subject: [PATCH 2/2] Use more efficient implementation. Fix comment typos. --- .../lib/src/iterable_extensions.dart | 56 +++++++++++-------- pkgs/collection/lib/src/list_extensions.dart | 56 +++++++++++-------- .../test/separate_extensions_test.dart | 47 +++++++++++----- 3 files changed, 98 insertions(+), 61 deletions(-) diff --git a/pkgs/collection/lib/src/iterable_extensions.dart b/pkgs/collection/lib/src/iterable_extensions.dart index 352f6863..f3449f9e 100644 --- a/pkgs/collection/lib/src/iterable_extensions.dart +++ b/pkgs/collection/lib/src/iterable_extensions.dart @@ -75,34 +75,37 @@ extension IterableExtension on Iterable { /// print([].separated(-1)); // () /// /// print([1, 2, 3].separated( - /// -1 + /// -1, /// before: true, /// )); // (-1, 1, -1, 2, -1, 3) /// /// print([1].separated( - /// -1 + /// -1, /// before: true, /// after: true, /// )); // (-1, 1, -1) /// /// print([].separated( - /// -1 + /// -1, /// before: true, /// after: true, /// )); // () /// ``` Iterable separated(T separator, {bool before = false, bool after = false}) sync* { - const emitBefore = 1; - const emitAfter = 2; - var state = before ? emitBefore : 0; - final afterState = emitBefore | (after ? emitAfter : 0); - for (var element in this) { - if (state & emitBefore != 0) yield separator; - state = afterState; - yield element; + 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; } - if (state & emitAfter != 0) yield separator; } /// Creates new list with the elements of this list separated by [separator]. @@ -124,32 +127,39 @@ extension IterableExtension on Iterable { /// print([].separatedList(-1)); // [] /// /// print([1, 2, 3].separatedList( - /// -1 + /// -1, /// before: true, /// )); // [-1, 1, -1, 2, -1, 3] /// /// print([1].separatedList( - /// -1 + /// -1, /// before: true, /// after: true, /// )); // [-1, 1, -1] /// /// print([].separatedList( - /// -1 + /// -1, /// before: true, /// after: true, /// )); // [] /// ``` List separatedList(T separator, {bool before = false, bool after = false}) { - var hasElement = false; - return [ - for (var element in this) ...[ - if (hasElement || (hasElement = true) && before) separator, - element, - ], - if (hasElement && after) separator - ]; + 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]. diff --git a/pkgs/collection/lib/src/list_extensions.dart b/pkgs/collection/lib/src/list_extensions.dart index cd56118a..51dde7bb 100644 --- a/pkgs/collection/lib/src/list_extensions.dart +++ b/pkgs/collection/lib/src/list_extensions.dart @@ -390,23 +390,23 @@ extension ListExtensions on List { /// /// Example: /// ```dart - /// print([1, 2, 3]..insertSeparator(-1)); // [1, -1, 2, -1, 3] - /// print([1]..insertSeparator(-1)); // [1] - /// print([]..insertSeparator(-1)); // [] + /// print([1, 2, 3]..separate(-1)); // [1, -1, 2, -1, 3] + /// print([1]..separate(-1)); // [1] + /// print([]..separate(-1)); // [] /// - /// print([1, 2, 3]..insertSeparator( - /// -1 + /// print([1, 2, 3]..separate( + /// -1, /// before: true, /// )); // [-1, 1, -1, 2, -1, 3] /// - /// print([1]..insertSeparator( - /// -1 + /// print([1]..separate( + /// -1, /// before: true, /// after: true, /// )); // [-1, 1, -1] /// - /// print([]..insertSeparator( - /// -1 + /// print([]..separate( + /// -1, /// before: true, /// after: true, /// )); // [] @@ -414,26 +414,34 @@ extension ListExtensions on List { void separate(E separator, {bool before = false, bool after = false}) { var length = this.length; if (length == 0) return; - var newLength = length * 2 - 1; - var offset = 0; - if (before) { - newLength++; - offset = 1; + // 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]); } - if (after) newLength++; - E newElementAt(int index) { - index -= offset; - if (index.isOdd) return separator; - return this[index >> 1]; + cursor++; + while (this.length < newLast) { + add(separator); + add(this[cursor++]); } + assert(cursor == length); + if (after) add(separator); - for (var i = length; i < newLength; i++) { - add(newElementAt(i)); + cursor = splitIndex >> 1; + if (splitIndex.isOdd) { + this[--length] = this[cursor]; } - for (var i = length, firstChanged = offset ^ 1; i > firstChanged;) { - --i; - this[i] = newElementAt(i); + cursor--; + for (var i = length; i > 1;) { + this[--i] = separator; + this[--i] = this[cursor--]; } + if (newFirst != 0) this[0] = separator; } } diff --git a/pkgs/collection/test/separate_extensions_test.dart b/pkgs/collection/test/separate_extensions_test.dart index 2adccc44..cccf8b5c 100644 --- a/pkgs/collection/test/separate_extensions_test.dart +++ b/pkgs/collection/test/separate_extensions_test.dart @@ -229,20 +229,39 @@ void main() { }); }); group('Multiple', () { - 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('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', () {