Add `separated`, `separatedList` and `separate` to iterables and lists. (#919)
diff --git a/pkgs/collection/CHANGELOG.md b/pkgs/collection/CHANGELOG.md
index 743fddf..6517421 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 d9516e6..b3da5ea 100644
--- a/pkgs/collection/lib/src/iterable_extensions.dart
+++ b/pkgs/collection/lib/src/iterable_extensions.dart
@@ -4,6 +4,7 @@
import 'dart:math' show Random;
+import '../collection.dart' show DelegatingIterable;
import 'algorithms.dart';
import 'functions.dart' as functions;
import 'utils.dart';
@@ -56,6 +57,99 @@
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] as Iterable<int>).separated(-1)); // (1, -1, 2, -1, 3)
+ /// print(([1] as Iterable<int>).separated(-1)); // (1)
+ /// print(([] as Iterable<int>).separated(-1)); // ()
+ ///
+ /// print(([1, 2, 3] as Iterable<int>).separated(
+ /// -1,
+ /// before: true,
+ /// )); // (-1, 1, -1, 2, -1, 3)
+ ///
+ /// print(([1] as Iterable<int>).separated(
+ /// -1,
+ /// before: true,
+ /// after: true,
+ /// )); // (-1, 1, -1)
+ ///
+ /// print(([] as Iterable<int>).separated(
+ /// -1,
+ /// before: true,
+ /// after: true,
+ /// )); // ()
+ /// ```
+ Iterable<T> separated(T separator,
+ {bool before = false, bool after = false}) =>
+ _SeparatedIterable<T>(this, separator, before, after);
+
+ /// Creates list with the elements of this iterable separated by [separator].
+ ///
+ /// Returns a new list which contains the same elements as this iterable,
+ /// 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 iterable 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<T> separatedList(T separator,
+ {bool before = false, bool after = false}) {
+ var result = <T>[];
+ 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<T> whereNot(bool Function(T element) test) =>
where((element) => !test(element));
@@ -1056,3 +1150,206 @@
return result;
};
}
+
+/// Implementation of [IterableExtension.separated].
+///
+/// Optimizes direct accesses.
+class _SeparatedIterable<T> extends Iterable<T> {
+ final T _separator;
+ final Iterable<T> _elements;
+
+ static const int _afterFlag = 1 << 0;
+ static const int _beforeFlag = 1 << 1;
+
+ /// Two bit-flags, for whether the `before` and `after` arguments were `true`.
+ final int _flags;
+
+ _SeparatedIterable(this._elements, this._separator, bool before, bool after)
+ : _flags = (before ? _beforeFlag : 0) + (after ? _afterFlag : 0);
+
+ @override
+ bool get isEmpty => _elements.isEmpty;
+ @override
+ bool get isNotEmpty => _elements.isNotEmpty;
+ @override
+ int get length {
+ var length = _elements.length;
+ if (length != 0) {
+ length = length * 2 - 1 + (_flags & 1) + (_flags >> 1);
+ }
+ return length;
+ }
+
+ @override
+ T elementAt(int index) {
+ RangeError.checkNotNegative(index, 'index');
+ // Figure out which element must exist in [_elements] for this index
+ // to exist in the separated output.
+ var indexWithoutBefore = index - (_flags >> 1);
+ var elementIndex = indexWithoutBefore ~/ 2; // Rounds both -1 and 1 to 0.
+ if (indexWithoutBefore.isEven) {
+ // It's an element.
+ return _elements.elementAt(elementIndex);
+ }
+ // It's a separator after that element (or before the first element).
+ // Check if that element exists, unless the `_afterFlag` is set,
+ // in which case to check if the next element exists by adding 1
+ // to elementIndex.
+ // (But if `index` is zero, it's the before separator, so it should
+ // check that a first element exists.)
+ if (index != 0) {
+ assert(_afterFlag == 1);
+ elementIndex += (_flags ^ _afterFlag) & _afterFlag;
+ }
+ _elements.elementAt(elementIndex); // If throws, not an element.
+ return _separator;
+ }
+
+ @override
+ T get first {
+ if (_flags & _beforeFlag == 0) return _elements.first;
+ if (_elements.isNotEmpty) return _separator;
+ throw StateError('No element');
+ }
+
+ @override
+ T get last {
+ if (_flags & _afterFlag == 0) return _elements.last;
+ if (_elements.isNotEmpty) return _separator;
+ throw StateError('No element');
+ }
+
+ @override
+ Iterable<T> take(int count) {
+ if (count == 0) return Iterable<T>.empty();
+ var beforeCount = _flags >> 1;
+ if (count == 1) {
+ if (beforeCount == 0) {
+ return _elements.take(1);
+ }
+ // return Iterable<T>.value(_separator); // Why you no exist?!
+ return DelegatingIterable<T>([_separator]);
+ }
+ var countWithoutBefore = count - beforeCount;
+ var elementCount = (countWithoutBefore + 1) >> 1;
+ return _SeparatedIterable<T>(
+ _elements.take(elementCount),
+ _separator,
+ beforeCount != 0,
+ countWithoutBefore.isEven,
+ );
+ }
+
+ @override
+ Iterable<T> skip(int count) {
+ if (count == 0) return this;
+ var beforeCount = _flags >> 1;
+ var countWithoutBefore = count - beforeCount;
+ var hasAfter = _flags & _afterFlag != 0;
+ if (countWithoutBefore.isOdd && hasAfter) {
+ // Remainder could be just the final separator, which cannot
+ // be created by a `_SeparatedIterable`.
+ // (Unlike `take`, cannot see that without iterating.)
+ return super.skip(count);
+ }
+ // Starts or ends on an element, not a separator,
+ // so remainder cannot be a single separator.
+ var elementCount = (countWithoutBefore + 1) >> 1;
+ return _SeparatedIterable<T>(
+ elementCount == 0 ? _elements : _elements.skip(elementCount),
+ _separator,
+ countWithoutBefore.isOdd,
+ hasAfter,
+ );
+ }
+
+ @override
+ Iterator<T> get iterator =>
+ _SeparatedIterator<T>(_elements.iterator, _separator, _flags);
+}
+
+/// Iterator for [_SeparatedIterable].
+class _SeparatedIterator<T> implements Iterator<T> {
+ final T _separator;
+ final Iterator<T> _elements;
+
+ // Flags set in [_state].
+
+ /// Set if adding a separator after the last element.
+ ///
+ /// State never changes, just storing a boolean as a bit.
+ static const _noAddAfterFlag = 1 << 0;
+
+ // Set when the next element to emit is a separator.
+ //
+ // Otherwise the element to emit is [_elements.current].
+ static const _separatorFlag = 1 << 1;
+
+ // Set when next step should check if there is a next element.
+ //
+ // If there is no next element, iteration ends.
+ static const _ifHasNextFlag = 1 << 2;
+
+ /// Current state.
+ ///
+ /// A combination of the [_noAddAfterFlag], [_separatorFlag]
+ /// and [_ifHasNextFlag].
+ ///
+ /// Transitions:
+ /// * If `_ifHasNextFlag`:
+ /// - if `!_elements.moveNext()`, then end.
+ /// (No state change, next call will do the same).
+ /// - otherwise continue.
+ /// * If `_separatorFlag`:
+ /// - emit `_separator`,
+ /// - clear `_separatorFlag` (next is an element),
+ /// - toggle `_ifHasNextFlag`.
+ /// * else:
+ /// - emit `_elements.current`,
+ /// - set `_separatorFlag` (next will be a separator),
+ /// - set `_ifHasNextFlag` if `_noAddAfterFlag` is set.
+ ///
+ /// Starts with `ifHasNextFlag` set,
+ /// with `_separatorFlag` set if the `before` parameter of the iterable
+ /// was `true`, and with `noAddAfterFlag` set if the `after` parameter
+ /// of the iterable was `false`.
+ int _state;
+
+ T? _current;
+
+ _SeparatedIterator(this._elements, this._separator, int flags)
+ : assert(_noAddAfterFlag == _SeparatedIterable._afterFlag),
+ assert(_separatorFlag == _SeparatedIterable._beforeFlag),
+ // `_separatorFlag` set if `_beforeFlag` was set.
+ // `_noAddAfterFlag` set if `_afterFlag` was not.
+ // `_ifHasNextFlag` always set at the start.
+ _state = (flags ^ _noAddAfterFlag) | _ifHasNextFlag;
+
+ @override
+ T get current => _current as T;
+
+ @override
+ bool moveNext() {
+ var state = _state;
+ if (state & _ifHasNextFlag == 0 || _elements.moveNext()) {
+ if (state & _separatorFlag != 0) {
+ _current = _separator;
+ // Next is not separator.
+ // Check if there is a next if this call didn't.
+ state ^= _separatorFlag | _ifHasNextFlag;
+ } else {
+ _current = _elements.current;
+ // Next is separator.
+ // Check if there is a next if not adding separator after last element.
+ state = (state & _noAddAfterFlag) * (_noAddAfterFlag | _ifHasNextFlag) +
+ _separatorFlag;
+ }
+ _state = state;
+ return true;
+ }
+ // Next call will check `_elements.moveNext()` again.
+ assert(state & _ifHasNextFlag != 0);
+ _current = null;
+ return false;
+ }
+}
diff --git a/pkgs/collection/lib/src/list_extensions.dart b/pkgs/collection/lib/src/list_extensions.dart
index a301a1e..85b5a28 100644
--- a/pkgs/collection/lib/src/list_extensions.dart
+++ b/pkgs/collection/lib/src/list_extensions.dart
@@ -3,12 +3,15 @@
// BSD-style license that can be found in the LICENSE file.
// Extension methods on common collection types.
+
import 'dart:collection';
import 'dart:math';
import 'algorithms.dart';
import 'algorithms.dart' as algorithms;
import 'equality.dart';
+// For documentation only. Use docImport when reaching language version 3.8.
+import 'iterable_extensions.dart';
import 'utils.dart';
/// Various extensions on lists of arbitrary elements.
@@ -327,6 +330,169 @@
yield slice(i, min(i + length, this.length));
}
}
+
+ /// The elements of this list separated by [separator] as a lazy list.
+ ///
+ /// Creates an unmodifiable list backed by this list, which has [separator]s
+ /// between, and optionally before and or/after, the elements of this list.
+ /// Changes to this list will be reflected in the returned list.
+ ///
+ /// If [before] is set to `true`, the returned list has a separator
+ /// before each element of this list.
+ /// If [after] is set to `true`, the returned list has a separator
+ /// after each element of this list.
+ /// The returned list always has a separator between two elements
+ /// of this list.
+ ///
+ /// If this iterable is empty, [before] and [after] have no effect.
+ ///
+ /// Compared to [IterableExtension.separatedList], this function
+ /// doesn't copy the elements into a new list, it creates a view
+ /// of the existing list with separators between the original elements.
+ /// To do that efficiently, the source must be a list, not just
+ /// an iterable.
+ ///
+ /// 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,
+ /// )); // []
+ /// ```
+ List<E> separated(E separator, {bool before = false, bool after = false}) =>
+ _SeparatedList<E>(this, separator, before, after);
+
+ /// 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<E> 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.
@@ -570,3 +736,116 @@
throw UnsupportedError('Cannot remove from a fixed-length list');
}
}
+
+/// A lazy separator mapper around a `List`.
+class _SeparatedList<E> extends ListBase<E> {
+ final E _separator;
+ final List<E> _elements;
+
+ static const int _lengthDeltaShift = 1;
+ static const int _lengthDeltaUnit = 1 << _lengthDeltaShift;
+ static const int _beforeFlag = 1 << 0;
+
+ /// Bit flags for before/after separator behavior.
+ ///
+ /// Bit 0: Have separator before first element.
+ /// Bit 1+: Number of separators before and after.
+ /// One of 0, 1 or 2 depending on how many of `before` and `after`
+ /// were true.
+ final int _flags;
+
+ _SeparatedList(this._elements, this._separator, bool before, bool after)
+ : _flags = (before ? (_beforeFlag + _lengthDeltaUnit) : 0) +
+ (after ? _lengthDeltaUnit : 0);
+
+ @override
+ bool get isEmpty => _elements.isEmpty;
+
+ @override
+ bool get isNotEmpty => _elements.isNotEmpty;
+
+ @override
+ int get length {
+ var length = _elements.length;
+ if (length != 0) length = length * 2 - 1 + (_flags >> _lengthDeltaShift);
+ return length;
+ }
+
+ @override
+ E operator [](int index) {
+ IndexError.check(index, length, indexable: this);
+ var indexWithoutBefore = index - (_flags & _beforeFlag);
+ return indexWithoutBefore.isEven
+ ? _elements[indexWithoutBefore >> 1]
+ : _separator;
+ }
+
+ @override
+ void operator []=(int index, E value) => _unmodifiable();
+
+ @override
+ set length(int newLength) => _unmodifiable();
+
+ @override
+ set first(E element) => _unmodifiable();
+
+ @override
+ set last(E element) => _unmodifiable();
+
+ @override
+ void setAll(int at, Iterable<E> iterable) => _unmodifiable();
+
+ @override
+ void add(E value) => _unmodifiable();
+
+ @override
+ void insert(int index, E element) => _unmodifiable();
+
+ @override
+ void insertAll(int at, Iterable<E> iterable) => _unmodifiable();
+
+ @override
+ void addAll(Iterable<E> iterable) => _unmodifiable();
+
+ @override
+ bool remove(Object? element) => _unmodifiable();
+
+ @override
+ void removeWhere(bool Function(E element) test) => _unmodifiable();
+
+ @override
+ void retainWhere(bool Function(E element) test) => _unmodifiable();
+
+ @override
+ void sort([Comparator<E>? compare]) => _unmodifiable();
+
+ @override
+ void shuffle([Random? random]) => _unmodifiable();
+
+ @override
+ void clear() => _unmodifiable();
+
+ @override
+ E removeAt(int index) => _unmodifiable();
+
+ @override
+ E removeLast() => _unmodifiable();
+
+ @override
+ void setRange(int start, int end, Iterable<E> iterable,
+ [int skipCount = 0]) =>
+ _unmodifiable();
+
+ @override
+ void removeRange(int start, int end) => _unmodifiable();
+
+ @override
+ void replaceRange(int start, int end, Iterable<E> iterable) =>
+ _unmodifiable();
+
+ @override
+ void fillRange(int start, int end, [E? fillValue]) => _unmodifiable();
+
+ static Never _unmodifiable() =>
+ throw UnsupportedError('Cannot modify lazily separated list');
+}
diff --git a/pkgs/collection/test/separate_extensions_test.dart b/pkgs/collection/test/separate_extensions_test.dart
new file mode 100644
index 0000000..55d2a12
--- /dev/null
+++ b/pkgs/collection/test/separate_extensions_test.dart
@@ -0,0 +1,424 @@
+// 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('', () {
+ expectIterable(iterable(<int>[]).separated(42), <int>[]);
+ });
+ test('before', () {
+ expectIterable(iterable(<int>[]).separated(42, before: true), <int>[]);
+ });
+ test('after', () {
+ expectIterable(iterable(<int>[]).separated(42, after: true), <int>[]);
+ });
+ test('before and after', () {
+ expectIterable(
+ iterable(<int>[]).separated(42, before: true, after: true),
+ <int>[]);
+ });
+ test('List', () {
+ var separatedList = iterable(<int>[]).separated(42);
+ expect(separatedList, isNot(isA<List>()));
+ expectIterable(separatedList, <int>[]);
+ });
+ });
+ group('Singleton', () {
+ test('', () {
+ expectIterable(iterable(<int>[1]).separated(42), [1]);
+ });
+ test('before', () {
+ expectIterable(iterable(<int>[1]).separated(42, before: true), [42, 1]);
+ });
+ test('after', () {
+ expectIterable(iterable(<int>[1]).separated(42, after: true), [1, 42]);
+ });
+ test('before and after', () {
+ expectIterable(
+ iterable(<int>[1]).separated(42, before: true, after: true),
+ [42, 1, 42]);
+ });
+ test('List', () {
+ var separatedList = iterable(<int>[1]).separated(42);
+ expect(separatedList, isNot(isA<List>()));
+ expectIterable(separatedList, [1]);
+ });
+ });
+ group('Multiple', () {
+ test('', () {
+ expectIterable(
+ iterable(<int>[1, 2, 3]).separated(42), [1, 42, 2, 42, 3]);
+ });
+ test('before', () {
+ expectIterable(iterable(<int>[1, 2, 3]).separated(42, before: true),
+ [42, 1, 42, 2, 42, 3]);
+ });
+ test('after', () {
+ expectIterable(iterable(<int>[1, 2, 3]).separated(42, after: true),
+ [1, 42, 2, 42, 3, 42]);
+ });
+ test('before and after', () {
+ expectIterable(
+ iterable(<int>[1, 2, 3]).separated(42, before: true, after: true),
+ [42, 1, 42, 2, 42, 3, 42]);
+ });
+ });
+ test('nulls', () {
+ expectIterable(iterable<int?>([null, 2, null]).separated(null),
+ [null, null, 2, null, null]);
+ });
+ test('upcast receiver', () {
+ var source = <int>[1, 2, 3];
+ expectIterable(
+ iterable<num>(source).separated(3.14), [1, 3.14, 2, 3.14, 3]);
+ });
+ });
+
+ // ------------------------------------------------------------------
+ group('List.separated', () {
+ group('empty', () {
+ test('', () {
+ expectUnmodifiableList(<int>[].separated(42), <int>[], 0);
+ });
+ test('before', () {
+ expectUnmodifiableList(<int>[].separated(42, before: true), <int>[], 0);
+ });
+ test('after', () {
+ expectUnmodifiableList(<int>[].separated(42, after: true), <int>[], 0);
+ });
+ test('before and after', () {
+ expectUnmodifiableList(
+ <int>[].separated(42, before: true, after: true), <int>[], 0);
+ });
+ test('List', () {
+ var separatedList = <int>[].separated(42);
+ expect(separatedList, isA<List>());
+ expectUnmodifiableList(separatedList, <int>[], 0);
+ });
+ });
+ group('Singleton', () {
+ test('', () {
+ expectUnmodifiableList(<int>[1].separated(42), [1], 0);
+ });
+ test('before', () {
+ expectUnmodifiableList(
+ <int>[1].separated(42, before: true), [42, 1], 0);
+ });
+ test('after', () {
+ expectUnmodifiableList(<int>[1].separated(42, after: true), [1, 42], 0);
+ });
+ test('before and after', () {
+ expectUnmodifiableList(
+ <int>[1].separated(42, before: true, after: true), [42, 1, 42], 0);
+ });
+ test('List', () {
+ var separatedList = <int>[1].separated(42);
+ expect(separatedList, isA<List>());
+ expectUnmodifiableList(separatedList, [1], 0);
+ });
+ });
+ group('Multiple', () {
+ test('', () {
+ expectUnmodifiableList(
+ <int>[1, 2, 3].separated(42), [1, 42, 2, 42, 3], 0);
+ });
+ test('before', () {
+ expectUnmodifiableList(<int>[1, 2, 3].separated(42, before: true),
+ [42, 1, 42, 2, 42, 3], 0);
+ });
+ test('after', () {
+ expectUnmodifiableList(<int>[1, 2, 3].separated(42, after: true),
+ [1, 42, 2, 42, 3, 42], 0);
+ });
+ test('before and after', () {
+ expectUnmodifiableList(
+ <int>[1, 2, 3].separated(42, before: true, after: true),
+ [42, 1, 42, 2, 42, 3, 42],
+ 0);
+ });
+ test('List', () {
+ var separatedList = <int>[1, 2, 3].separated(42);
+ expect(separatedList, isA<List>());
+ expectUnmodifiableList(separatedList, [1, 42, 2, 42, 3], 0);
+ });
+ });
+ test('nulls', () {
+ expectUnmodifiableList(
+ [null, 2, null].separated(null), [null, null, 2, null, null], 0);
+ });
+ test('upcast receiver', () {
+ var source = <int>[1, 2, 3];
+ expectUnmodifiableList(
+ (source as List<num>).separated(3.14), [1, 3.14, 2, 3.14, 3], 0);
+ });
+ });
+
+ // ------------------------------------------------------------------
+ group('Iterable.separatedList', () {
+ group('empty', () {
+ test('', () {
+ expect(iterable(<int>[]).separatedList(42), <int>[]);
+ });
+ test('before', () {
+ expect(iterable(<int>[]).separatedList(42, before: true), <int>[]);
+ });
+ test('after', () {
+ expect(iterable(<int>[]).separatedList(42, after: true), <int>[]);
+ });
+ test('before and after', () {
+ expect(iterable(<int>[]).separatedList(42, before: true, after: true),
+ <int>[]);
+ });
+ });
+ group('Singleton', () {
+ test('', () {
+ expect(iterable(<int>[1]).separatedList(42), [1]);
+ });
+ test('before', () {
+ expect(iterable(<int>[1]).separatedList(42, before: true), [42, 1]);
+ });
+ test('after', () {
+ expect(iterable(<int>[1]).separatedList(42, after: true), [1, 42]);
+ });
+ test('before and after', () {
+ expect(iterable(<int>[1]).separatedList(42, before: true, after: true),
+ [42, 1, 42]);
+ });
+ });
+ group('Multiple', () {
+ test('', () {
+ expect(iterable(<int>[1, 2, 3]).separatedList(42), [1, 42, 2, 42, 3]);
+ });
+ test('before', () {
+ expect(iterable(<int>[1, 2, 3]).separatedList(42, before: true),
+ [42, 1, 42, 2, 42, 3]);
+ });
+ test('after', () {
+ expect(iterable(<int>[1, 2, 3]).separatedList(42, after: true),
+ [1, 42, 2, 42, 3, 42]);
+ });
+ test('before and after', () {
+ expect(
+ iterable(<int>[1, 2, 3])
+ .separatedList(42, before: true, after: true),
+ [42, 1, 42, 2, 42, 3, 42]);
+ });
+ });
+ test('nulls', () {
+ expect(iterable<int?>([null, 2, null]).separatedList(null),
+ [null, null, 2, null, null]);
+ });
+ test('upcast receiver', () {
+ var source = <int>[1, 2, 3];
+ expect(iterable<num>(source).separatedList(3.14), [1, 3.14, 2, 3.14, 3]);
+ });
+ });
+
+ // ------------------------------------------------------------------
+ group('List.separatedList', () {
+ group('empty', () {
+ test('', () {
+ expect(<int>[].separatedList(42), <int>[]);
+ });
+ test('before', () {
+ expect(<int>[].separatedList(42, before: true), <int>[]);
+ });
+ test('after', () {
+ expect(<int>[].separatedList(42, after: true), <int>[]);
+ });
+ test('before and after', () {
+ expect(<int>[].separatedList(42, before: true, after: true), <int>[]);
+ });
+ });
+ group('Singleton', () {
+ test('', () {
+ expect(<int>[1].separatedList(42), [1]);
+ });
+ test('before', () {
+ expect(<int>[1].separatedList(42, before: true), [42, 1]);
+ });
+ test('after', () {
+ expect(<int>[1].separatedList(42, after: true), [1, 42]);
+ });
+ test('before and after', () {
+ expect(
+ <int>[1].separatedList(42, before: true, after: true), [42, 1, 42]);
+ });
+ });
+ group('Multiple', () {
+ test('', () {
+ expect(<int>[1, 2, 3].separatedList(42), [1, 42, 2, 42, 3]);
+ });
+ test('before', () {
+ expect(<int>[1, 2, 3].separatedList(42, before: true),
+ [42, 1, 42, 2, 42, 3]);
+ });
+ test('after', () {
+ expect(<int>[1, 2, 3].separatedList(42, after: true),
+ [1, 42, 2, 42, 3, 42]);
+ });
+ test('before and after', () {
+ expect(<int>[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 = <int>[1, 2, 3];
+ expect((source as List<num>).separatedList(3.14), [1, 3.14, 2, 3.14, 3]);
+ });
+ });
+
+ // ------------------------------------------------------------------
+ group('List.separate', () {
+ group('empty', () {
+ test('', () {
+ expectList(<int>[]..separate(42), <int>[]);
+ });
+ test('before', () {
+ expectList(<int>[]..separate(42, before: true), <int>[]);
+ });
+ test('after', () {
+ expectList(<int>[]..separate(42, after: true), <int>[]);
+ });
+ test('before and after', () {
+ expectList(<int>[]..separate(42, before: true, after: true), <int>[]);
+ });
+ });
+ group('Singleton', () {
+ test('', () {
+ expectList(<int>[1]..separate(42), [1]);
+ });
+ test('before', () {
+ expectList(<int>[1]..separate(42, before: true), [42, 1]);
+ });
+ test('after', () {
+ expectList(<int>[1]..separate(42, after: true), [1, 42]);
+ });
+ test('before and after', () {
+ expectList(
+ <int>[1]..separate(42, before: true, after: true), [42, 1, 42]);
+ });
+ });
+ group('Multiple', () {
+ group('odd length', () {
+ test('', () {
+ expectList(<int>[1, 2, 3]..separate(42), [1, 42, 2, 42, 3]);
+ });
+ test('before', () {
+ expectList(<int>[1, 2, 3]..separate(42, before: true),
+ [42, 1, 42, 2, 42, 3]);
+ });
+ test('after', () {
+ expectList(
+ <int>[1, 2, 3]..separate(42, after: true), [1, 42, 2, 42, 3, 42]);
+ });
+ test('before and after', () {
+ expectList(<int>[1, 2, 3]..separate(42, before: true, after: true),
+ [42, 1, 42, 2, 42, 3, 42]);
+ });
+ });
+ group('even length', () {
+ test('', () {
+ expectList(<int>[1, 2, 3, 4]..separate(42), [1, 42, 2, 42, 3, 42, 4]);
+ });
+ test('before', () {
+ expectList(<int>[1, 2, 3, 4]..separate(42, before: true),
+ [42, 1, 42, 2, 42, 3, 42, 4]);
+ });
+ test('after', () {
+ expectList(<int>[1, 2, 3, 4]..separate(42, after: true),
+ [1, 42, 2, 42, 3, 42, 4, 42]);
+ });
+ test('before and after', () {
+ expectList(<int>[1, 2, 3, 4]..separate(42, before: true, after: true),
+ [42, 1, 42, 2, 42, 3, 42, 4, 42]);
+ });
+ });
+ });
+ test('nulls', () {
+ expectList([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 = <int>[1, 2, 3];
+ expect(() => (source as List<num>).separate(3.14),
+ throwsA(isA<TypeError>()));
+ });
+ 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 = <int>[1, 2, 3];
+ expectList((source as List<num>)..separate(42), [1, 42, 2, 42, 3]);
+ });
+ });
+}
+
+/// Creates a plain iterable not implementing any other class.
+Iterable<T> iterable<T>(Iterable<T> values) sync* {
+ yield* values;
+}
+
+void expectIterable<T>(Iterable<T> actual, List<T> expected) {
+ expect(actual, expected); // Elements are correct, uses `iterator`.
+
+ // Specialized members should work.
+ expect(actual.length, expected.length);
+ for (var i = 0; i < expected.length; i++) {
+ expect(actual.isEmpty, expected.isEmpty);
+ expect(actual.isNotEmpty, expected.isNotEmpty);
+ expect(actual.elementAt(i), expected[i]);
+ expect(actual.skip(i), expected.sublist(i));
+ expect(actual.take(i), expected.sublist(0, i));
+ }
+ expect(() => actual.elementAt(actual.length), throwsRangeError);
+ expect(() => actual.elementAt(-1), throwsRangeError);
+
+ if (expected.isNotEmpty) {
+ expect(actual.first, expected.first, reason: 'first');
+ expect(actual.last, expected.last, reason: 'last');
+ } else {
+ expect(() => actual.first, throwsStateError, reason: 'first');
+ expect(() => actual.last, throwsStateError, reason: 'last');
+ }
+}
+
+void expectList<T>(List<T> actual, List<T> expected) {
+ expectIterable(actual, expected);
+
+ for (var i = 0; i < expected.length; i++) {
+ expect(actual[i], expected[i]);
+ }
+ expect(() => actual[actual.length], throwsRangeError);
+ expect(() => actual[-1], throwsRangeError);
+}
+
+void expectUnmodifiableList<T>(List<T> actual, List<T> expected, T value) {
+ expectList(actual, expected);
+
+ expect(() => actual.length = 0, throwsUnsupportedError);
+ expect(() => actual.add(value), throwsUnsupportedError);
+ expect(() => actual.addAll([]), throwsUnsupportedError);
+ expect(() => actual.clear(), throwsUnsupportedError);
+ expect(() => actual.fillRange(0, 0, value), throwsUnsupportedError);
+ expect(() => actual.remove(0), throwsUnsupportedError);
+ expect(() => actual.removeAt(0), throwsUnsupportedError);
+ expect(() => actual.removeLast(), throwsUnsupportedError);
+ expect(() => actual.removeRange(0, 0), throwsUnsupportedError);
+ expect(() => actual.removeWhere((_) => false), throwsUnsupportedError);
+ expect(() => actual.replaceRange(0, 0, []), throwsUnsupportedError);
+ expect(() => actual.retainWhere((_) => true), throwsUnsupportedError);
+ expect(() => actual.setAll(0, []), throwsUnsupportedError);
+ expect(() => actual.setRange(0, 0, []), throwsUnsupportedError);
+ expect(() => actual.sort((a, b) => 0), throwsUnsupportedError);
+}