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..35565aeb 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 @@ 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] as Iterable).separated(-1)); // (1, -1, 2, -1, 3) + /// print(([1] as Iterable).separated(-1)); // (1) + /// print(([] as Iterable).separated(-1)); // () + /// + /// print(([1, 2, 3] as Iterable).separated( + /// -1, + /// before: true, + /// )); // (-1, 1, -1, 2, -1, 3) + /// + /// print(([1] as Iterable).separated( + /// -1, + /// before: true, + /// after: true, + /// )); // (-1, 1, -1) + /// + /// print(([] as Iterable).separated( + /// -1, + /// before: true, + /// after: true, + /// )); // () + /// ``` + Iterable separated(T separator, + {bool before = false, bool after = false}) => + _SeparatedIterable(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 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)); @@ -1056,3 +1150,206 @@ extension ComparatorExtension on Comparator { return result; }; } + +/// Implementation of [IterableExtension.separated]. +/// +/// Optimizes direct accesses. +class _SeparatedIterable extends Iterable { + final T _separator; + final Iterable _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 take(int count) { + if (count == 0) return Iterable.empty(); + var beforeCount = _flags >> 1; + if (count == 1) { + if (beforeCount == 0) { + return _elements.take(1); + } + // return Iterable.value(_separator); // Why you no exist?! + return DelegatingIterable([_separator]); + } + var countWithoutBefore = count - beforeCount; + var elementCount = (countWithoutBefore + 1) >> 1; + return _SeparatedIterable( + _elements.take(elementCount), + _separator, + beforeCount != 0, + countWithoutBefore.isEven, + ); + } + + @override + Iterable 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( + elementCount == 0 ? _elements : _elements.skip(elementCount), + _separator, + countWithoutBefore.isOdd, + hasAfter, + ); + } + + @override + Iterator get iterator => + _SeparatedIterator(_elements.iterator, _separator, _flags); +} + +/// Iterator for [_SeparatedIterable]. +class _SeparatedIterator implements Iterator { + final T _separator; + final Iterator _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 a301a1ec..be81f227 100644 --- a/pkgs/collection/lib/src/list_extensions.dart +++ b/pkgs/collection/lib/src/list_extensions.dart @@ -327,6 +327,165 @@ extension ListExtensions on List { 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], + /// + /// 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 separated(E separator, {bool before = false, bool after = false}) => + _SeparatedList(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 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 +729,116 @@ class ListSlice extends ListBase { throw UnsupportedError('Cannot remove from a fixed-length list'); } } + +/// A lazy separator mapper around a `List`. +class _SeparatedList extends ListBase { + final E _separator; + final List _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 iterable) => _unmodifiable(); + + @override + void add(E value) => _unmodifiable(); + + @override + void insert(int index, E element) => _unmodifiable(); + + @override + void insertAll(int at, Iterable iterable) => _unmodifiable(); + + @override + void addAll(Iterable 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? 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 iterable, + [int skipCount = 0]) => + _unmodifiable(); + + @override + void removeRange(int start, int end) => _unmodifiable(); + + @override + void replaceRange(int start, int end, Iterable 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 00000000..55d2a125 --- /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([]).separated(42), []); + }); + test('before', () { + expectIterable(iterable([]).separated(42, before: true), []); + }); + test('after', () { + expectIterable(iterable([]).separated(42, after: true), []); + }); + test('before and after', () { + expectIterable( + iterable([]).separated(42, before: true, after: true), + []); + }); + test('List', () { + var separatedList = iterable([]).separated(42); + expect(separatedList, isNot(isA())); + expectIterable(separatedList, []); + }); + }); + group('Singleton', () { + test('', () { + expectIterable(iterable([1]).separated(42), [1]); + }); + test('before', () { + expectIterable(iterable([1]).separated(42, before: true), [42, 1]); + }); + test('after', () { + expectIterable(iterable([1]).separated(42, after: true), [1, 42]); + }); + test('before and after', () { + expectIterable( + iterable([1]).separated(42, before: true, after: true), + [42, 1, 42]); + }); + test('List', () { + var separatedList = iterable([1]).separated(42); + expect(separatedList, isNot(isA())); + expectIterable(separatedList, [1]); + }); + }); + group('Multiple', () { + test('', () { + expectIterable( + iterable([1, 2, 3]).separated(42), [1, 42, 2, 42, 3]); + }); + test('before', () { + expectIterable(iterable([1, 2, 3]).separated(42, before: true), + [42, 1, 42, 2, 42, 3]); + }); + test('after', () { + expectIterable(iterable([1, 2, 3]).separated(42, after: true), + [1, 42, 2, 42, 3, 42]); + }); + test('before and after', () { + expectIterable( + iterable([1, 2, 3]).separated(42, before: true, after: true), + [42, 1, 42, 2, 42, 3, 42]); + }); + }); + test('nulls', () { + expectIterable(iterable([null, 2, null]).separated(null), + [null, null, 2, null, null]); + }); + test('upcast receiver', () { + var source = [1, 2, 3]; + expectIterable( + iterable(source).separated(3.14), [1, 3.14, 2, 3.14, 3]); + }); + }); + + // ------------------------------------------------------------------ + group('List.separated', () { + group('empty', () { + test('', () { + expectUnmodifiableList([].separated(42), [], 0); + }); + test('before', () { + expectUnmodifiableList([].separated(42, before: true), [], 0); + }); + test('after', () { + expectUnmodifiableList([].separated(42, after: true), [], 0); + }); + test('before and after', () { + expectUnmodifiableList( + [].separated(42, before: true, after: true), [], 0); + }); + test('List', () { + var separatedList = [].separated(42); + expect(separatedList, isA()); + expectUnmodifiableList(separatedList, [], 0); + }); + }); + group('Singleton', () { + test('', () { + expectUnmodifiableList([1].separated(42), [1], 0); + }); + test('before', () { + expectUnmodifiableList( + [1].separated(42, before: true), [42, 1], 0); + }); + test('after', () { + expectUnmodifiableList([1].separated(42, after: true), [1, 42], 0); + }); + test('before and after', () { + expectUnmodifiableList( + [1].separated(42, before: true, after: true), [42, 1, 42], 0); + }); + test('List', () { + var separatedList = [1].separated(42); + expect(separatedList, isA()); + expectUnmodifiableList(separatedList, [1], 0); + }); + }); + group('Multiple', () { + test('', () { + expectUnmodifiableList( + [1, 2, 3].separated(42), [1, 42, 2, 42, 3], 0); + }); + test('before', () { + expectUnmodifiableList([1, 2, 3].separated(42, before: true), + [42, 1, 42, 2, 42, 3], 0); + }); + test('after', () { + expectUnmodifiableList([1, 2, 3].separated(42, after: true), + [1, 42, 2, 42, 3, 42], 0); + }); + test('before and after', () { + expectUnmodifiableList( + [1, 2, 3].separated(42, before: true, after: true), + [42, 1, 42, 2, 42, 3, 42], + 0); + }); + test('List', () { + var separatedList = [1, 2, 3].separated(42); + expect(separatedList, isA()); + 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 = [1, 2, 3]; + expectUnmodifiableList( + (source as List).separated(3.14), [1, 3.14, 2, 3.14, 3], 0); + }); + }); + + // ------------------------------------------------------------------ + group('Iterable.separatedList', () { + group('empty', () { + test('', () { + expect(iterable([]).separatedList(42), []); + }); + test('before', () { + expect(iterable([]).separatedList(42, before: true), []); + }); + test('after', () { + expect(iterable([]).separatedList(42, after: true), []); + }); + test('before and after', () { + expect(iterable([]).separatedList(42, before: true, after: true), + []); + }); + }); + 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), []); + }); + test('before', () { + expect([].separatedList(42, before: true), []); + }); + test('after', () { + expect([].separatedList(42, after: true), []); + }); + test('before and after', () { + expect([].separatedList(42, before: true, after: true), []); + }); + }); + 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('', () { + expectList([]..separate(42), []); + }); + test('before', () { + expectList([]..separate(42, before: true), []); + }); + test('after', () { + expectList([]..separate(42, after: true), []); + }); + test('before and after', () { + expectList([]..separate(42, before: true, after: true), []); + }); + }); + group('Singleton', () { + test('', () { + expectList([1]..separate(42), [1]); + }); + test('before', () { + expectList([1]..separate(42, before: true), [42, 1]); + }); + test('after', () { + expectList([1]..separate(42, after: true), [1, 42]); + }); + test('before and after', () { + expectList( + [1]..separate(42, before: true, after: true), [42, 1, 42]); + }); + }); + group('Multiple', () { + group('odd length', () { + test('', () { + expectList([1, 2, 3]..separate(42), [1, 42, 2, 42, 3]); + }); + test('before', () { + expectList([1, 2, 3]..separate(42, before: true), + [42, 1, 42, 2, 42, 3]); + }); + test('after', () { + expectList( + [1, 2, 3]..separate(42, after: true), [1, 42, 2, 42, 3, 42]); + }); + test('before and after', () { + expectList([1, 2, 3]..separate(42, before: true, after: true), + [42, 1, 42, 2, 42, 3, 42]); + }); + }); + group('even length', () { + test('', () { + expectList([1, 2, 3, 4]..separate(42), [1, 42, 2, 42, 3, 42, 4]); + }); + test('before', () { + expectList([1, 2, 3, 4]..separate(42, before: true), + [42, 1, 42, 2, 42, 3, 42, 4]); + }); + test('after', () { + expectList([1, 2, 3, 4]..separate(42, after: true), + [1, 42, 2, 42, 3, 42, 4, 42]); + }); + test('before and after', () { + expectList([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 = [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]; + expectList((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; +} + +void expectIterable(Iterable actual, List 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(List actual, List 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(List actual, List 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); +}