diff --git a/.github/ISSUE_TEMPLATE/collection.md b/.github/ISSUE_TEMPLATE/collection.md new file mode 100644 index 00000000..7880c7fd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/collection.md @@ -0,0 +1,5 @@ +--- +name: "package:collection" +about: "Create a bug or file a feature request against package:collection." +labels: "package:collection" +--- \ No newline at end of file diff --git a/.github/labeler.yml b/.github/labeler.yml index 9428f8f3..8dfe0c04 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -12,6 +12,10 @@ - changed-files: - any-glob-to-any-file: 'pkgs/characters/**' +"package:collection": + - changed-files: + - any-glob-to-any-file: 'pkgs/collection/**' + "package:convert": - changed-files: - any-glob-to-any-file: 'pkgs/convert/**' diff --git a/.github/workflows/collection.yaml b/.github/workflows/collection.yaml new file mode 100644 index 00000000..a42f72d5 --- /dev/null +++ b/.github/workflows/collection.yaml @@ -0,0 +1,77 @@ +name: package:collection + +on: + # Run CI on pushes to the main branch, and on PRs against main. + push: + branches: [ main ] + paths: + - '.github/workflows/collection.yaml' + - 'pkgs/collection/**' + pull_request: + branches: [ main ] + paths: + - '.github/workflows/collection.yaml' + - 'pkgs/collection/**' + schedule: + - cron: "0 0 * * 0" + +env: + PUB_ENVIRONMENT: bot.github + +defaults: + run: + working-directory: pkgs/collection/ + +jobs: + # Check code formatting and static analysis on a single OS (linux) + # against Dart dev. + analyze: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + sdk: [dev] + steps: + - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 + - uses: dart-lang/setup-dart@0a8a0fc875eb934c15d08629302413c671d3f672 + with: + sdk: ${{ matrix.sdk }} + - id: install + name: Install dependencies + run: dart pub get + - name: Check formatting + run: dart format --output=none --set-exit-if-changed . + if: always() && steps.install.outcome == 'success' + - name: Analyze code + run: dart analyze --fatal-infos + if: always() && steps.install.outcome == 'success' + + # Run tests on a matrix consisting of two dimensions: + # 1. OS: ubuntu-latest, (macos-latest, windows-latest) + # 2. release channel: dev + test: + needs: analyze + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + # Add macos-latest and/or windows-latest if relevant for this package. + os: [ubuntu-latest] + sdk: [3.4, dev] + steps: + - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 + - uses: dart-lang/setup-dart@0a8a0fc875eb934c15d08629302413c671d3f672 + with: + sdk: ${{ matrix.sdk }} + - id: install + name: Install dependencies + run: dart pub get + - name: Run VM tests + run: dart test --platform vm --test-randomize-ordering-seed=random + if: always() && steps.install.outcome == 'success' + - name: Run Chrome tests + run: dart test --platform chrome --test-randomize-ordering-seed=random + if: always() && steps.install.outcome == 'success' + - name: Run Chrome tests - wasm + run: dart test --platform chrome --compiler dart2wasm + if: always() && steps.install.outcome == 'success' diff --git a/README.md b/README.md index 634751fb..7542574f 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ This repository is home to various Dart packages under the [dart.dev](https://pu | [args](pkgs/args/) | Library for defining parsers for parsing raw command-line arguments into a set of options and values. | [![pub package](https://img.shields.io/pub/v/args.svg)](https://pub.dev/packages/args) | | [async](pkgs/async/) | Utility functions and classes related to the 'dart:async' library.| [![pub package](https://img.shields.io/pub/v/async.svg)](https://pub.dev/packages/async) | | [characters](pkgs/characters/) | String replacement with operations that are Unicode/grapheme cluster aware. | [![pub package](https://img.shields.io/pub/v/characters.svg)](https://pub.dev/packages/characters) | +| [collection](pkgs/collection/) | Collections and utilities functions and classes related to collections. | [![pub package](https://img.shields.io/pub/v/collection.svg)](https://pub.dev/packages/collection) | | [convert](pkgs/convert/) | Utilities for converting between data representations. | [![pub package](https://img.shields.io/pub/v/convert.svg)](https://pub.dev/packages/convert) | | [crypto](pkgs/crypto/) | Implementations of SHA, MD5, and HMAC cryptographic functions. | [![pub package](https://img.shields.io/pub/v/crypto.svg)](https://pub.dev/packages/crypto) | | [fixnum](pkgs/fixnum/) | Library for 32- and 64-bit signed fixed-width integers. | [![pub package](https://img.shields.io/pub/v/fixnum.svg)](https://pub.dev/packages/fixnum) | diff --git a/pkgs/collection/.gitignore b/pkgs/collection/.gitignore new file mode 100644 index 00000000..98d6d21f --- /dev/null +++ b/pkgs/collection/.gitignore @@ -0,0 +1,10 @@ +.buildlog +.DS_Store +.idea +.pub/ +.dart_tool/ +.settings/ +build/ +packages +.packages +pubspec.lock diff --git a/pkgs/collection/AUTHORS b/pkgs/collection/AUTHORS new file mode 100644 index 00000000..80712fdf --- /dev/null +++ b/pkgs/collection/AUTHORS @@ -0,0 +1,8 @@ +# Below is a list of people and organizations that have contributed +# to the project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +AAABramenko (https://github.com/AAAbramenko) +TimWhiting tim@whitings.org diff --git a/pkgs/collection/CHANGELOG.md b/pkgs/collection/CHANGELOG.md new file mode 100644 index 00000000..021dd937 --- /dev/null +++ b/pkgs/collection/CHANGELOG.md @@ -0,0 +1,326 @@ +## 1.19.1 + +- Move to `dart-lang/core` monorepo. + +## 1.19.0 + +- Adds `shuffled` to `IterableExtension`. +- Shuffle `IterableExtension.sample` results. +- Fix `mergeSort` when the runtime iterable generic is a subtype of the static + generic. +- `CanonicalizedMap`: added constructor `fromEntries`. +- Mark "mixin" classes as `mixin`. +- `extension IterableIterableExtension on Iterable>` + - Add `flattenedToList` as a performance improvement over `flattened.` + - Add `flattenedToSet` as new behavior for flattening to unique elements. +- Deprecate `transitiveClosure`. Consider using `package:graphs`. +- Deprecate `whereNotNull()` from `IterableNullableExtension`. Use `nonNulls` + instead - this is an equivalent extension available in Dart core since + version 3.0. +- Require Dart `^3.4.0` + +## 1.18.0 + +- `CanonicalizedMap`: + - Added methods: + - `copy`: copies an instance without recalculating the canonical values of the keys. + - `toMap`: creates a `Map` (with the original key values). + - `toMapOfCanonicalKeys`: creates a `Map` (with the canonicalized keys). +- Fixes bugs in `ListSlice.slice` and `ListExtensions.slice`. +- Update to `package:lints` 2.0.1. + +## 1.17.2 + +* Accept Dart SDK versions above 3.0. + +## 1.17.1 + +* Require Dart 2.18. +* Improve docs for `splitAfter` and `splitBefore`. + +## 1.17.0 + +* Add `Iterable.elementAtOrNull` and `List.elementAtOrNull` extension methods. +* Add a top-level `lastBy()` function that converts an `Iterable` to a `Map` by + grouping its elements using a function, keeping the last element for each + computed key. Also available as an extension method on `Iterable`. + +## 1.16.0 + +* Add an `Iterable.slices` extension method. +* Add `BoolList` class for space-efficient lists of boolean values. +* Use a stable sort algorithm in the `IterableExtension.sortedBy` method. +* Add `min`, `max`, `minOrNull` and `maxOrNull` getters to + `IterableDoubleExtension`, `IterableNumberExtension` and + `IterableIntegerExtension` +* Change `UnorderedIterableEquality` and `SetEquality` to implement `Equality` + with a non-nullable generic to allows assignment to variables with that type. + Assignment to `Equality` with a nullable type is still allowed because of + covariance. The `equals` and `hash` methods continue to accept nullable + arguments. +* Enable the `avoid_dynamic_calls` lint. + +## 1.15.0 + +* Stable release for null safety. + +## 1.15.0-nullsafety.5 + +* Fix typo in extension method `expandIndexed`. +* Update sdk constraints to `>=2.12.0-0 <3.0.0` based on beta release + guidelines. + +## 1.15.0-nullsafety.4 + +* Allow prerelease versions of the `2.12.x` sdk. + +* Remove the unusable setter `UnionSetController.set=`. This was mistakenly + added to the public API but could never be called. + +* Add extra optional `Random` argument to `shuffle`. + +* Add a large number of extension methods on `Iterable` and `List` types, + and on a few other types. + These either provide easy access to the operations from `algorithms.dart`, + or provide convenience variants of existing `Iterable` and `List` methods + like `singleWhereOrNull` or `forEachIndexed`. + +## 1.15.0-nullsafety.3 + +* Allow 2.10 stable and 2.11.0 dev SDK versions. +* Add `toUnorderedList` method on `PriorityQueue`. +* Make `HeapPriorityQueue`'s `remove` and `contains` methods + use `==` for equality checks. + Previously used `comparison(a, b) == 0` as criteria, but it's possible + to have multiple elements with the same priority in a queue, so that + could remove the wrong element. + Still requires that objects that are `==` also have the same priority. + +## 1.15.0-nullsafety.2 + +Update for the 2.10 dev sdk. + +## 1.15.0-nullsafety.1 + +* Allow the <=2.9.10 stable sdks. + +## 1.15.0-nullsafety + +Pre-release for the null safety migration of this package. + +Note that `1.15.0` may not be the final stable null safety release version, +we reserve the right to release it as a `2.0.0` breaking change. + +This release will be pinned to only allow pre-release sdk versions starting +from `2.9.0-dev.18.0`, which is the first version where this package will +appear in the null safety allow list. + +## 1.14.13 + +* Deprecate `mapMap`. The Map interface has a `map` call and map literals can + use for-loop elements which supersede this method. + +## 1.14.12 + +* Fix `CombinedMapView.keys`, `CombinedMapView.length`, + `CombinedMapView.forEach`, and `CombinedMapView.values` to work as specified + and not repeat duplicate items from the maps. + * As a result of this fix the `length` getter now must iterate all maps in + order to remove duplicates and return an accurate length, so it is no + longer `O(maps)`. + +## 1.14.11 + +* Set max SDK version to `<3.0.0`. + +## 1.14.10 + +* Fix the parameter names in overridden methods to match the source. +* Make tests Dart 2 type-safe. +* Stop depending on SDK `retype` and deprecate methods. + +## 1.14.9 + +* Fixed bugs where `QueueList`, `MapKeySet`, and `MapValueSet` did not adhere to + the contract laid out by `List.cast`, `Set.cast` and `Map.cast` respectively. + The returned instances of these methods now correctly forward to the existing + instance instead of always creating a new copy. + +## 1.14.8 + +* Deprecated `Delegating{Name}.typed` static methods in favor of the new Dart 2 + `cast` methods. For example, `DelegatingList.typed(list)` can now be + written as `list.cast()`. + +## 1.14.7 + +* Only the Dart 2 dev SDK (`>=2.0.0-dev.22.0`) is now supported. +* Added support for all Dart 2 SDK methods that threw `UnimplementedError`. + +## 1.14.6 + +* Make `DefaultEquality`'s `equals()` and `hash()` methods take any `Object` + rather than objects of type `E`. This makes `const DefaultEquality()` + usable as `Equality` for any `E`, which means it can be used in a const + context which expects `Equality`. + + This makes the default arguments of various other const equality constructors + work in strong mode. + +## 1.14.5 + +* Fix issue with `EmptyUnmodifiableSet`'s stubs that were introduced in 1.14.4. + +## 1.14.4 + +* Add implementation stubs of upcoming Dart 2.0 core library methods, namely + new methods for classes that implement `Iterable`, `List`, `Map`, `Queue`, + and `Set`. + +## 1.14.3 + +* Fix `MapKeySet.lookup` to be a valid override in strong mode. + +## 1.14.2 + +* Add type arguments to `SyntheticInvocation`. + +## 1.14.1 + +* Make `Equality` implementations accept `null` as argument to `hash`. + +## 1.14.0 + +* Add `CombinedListView`, a view of several lists concatenated together. +* Add `CombinedIterableView`, a view of several iterables concatenated together. +* Add `CombinedMapView`, a view of several maps concatenated together. + +## 1.13.0 + +* Add `EqualityBy` + +## 1.12.0 + +* Add `CaseInsensitiveEquality`. + +* Fix bug in `equalsIgnoreAsciiCase`. + +## 1.11.0 + +* Add `EqualityMap` and `EqualitySet` classes which use `Equality` objects for + key and element equality, respectively. + +## 1.10.1 + +* `Set.difference` now takes a `Set` as argument. + +## 1.9.1 + +* Fix some documentation bugs. + +## 1.9.0 + +* Add a top-level `stronglyConnectedComponents()` function that returns the + strongly connected components in a directed graph. + +## 1.8.0 + +* Add a top-level `mapMap()` function that works like `Iterable.map()` on a + `Map`. + +* Add a top-level `mergeMaps()` function that creates a new map with the + combined contents of two existing maps. + +* Add a top-level `groupBy()` function that converts an `Iterable` to a `Map` by + grouping its elements using a function. + +* Add top-level `minBy()` and `maxBy()` functions that return the minimum and + maximum values in an `Iterable`, respectively, ordered by a derived value. + +* Add a top-level `transitiveClosure()` function that returns the transitive + closure of a directed graph. + +## 1.7.0 + +* Add a `const UnmodifiableSetView.empty()` constructor. + +## 1.6.0 + +* Add a `UnionSet` class that provides a view of the union of a set of sets. + +* Add a `UnionSetController` class that provides a convenient way to manage the + contents of a `UnionSet`. + +* Fix another incorrectly-declared generic type. + +## 1.5.1 + +* Fix an incorrectly-declared generic type. + +## 1.5.0 + +* Add `DelegatingIterable.typed()`, `DelegatingList.typed()`, + `DelegatingSet.typed()`, `DelegatingMap.typed()`, and + `DelegatingQueue.typed()` static methods. These wrap untyped instances of + these classes with the correct type parameter, and assert the types of values + as they're accessed. + +* Fix the types for `binarySearch()` and `lowerBound()` so they no longer + require all arguments to be comparable. + +* Add generic annotations to `insertionSort()` and `mergeSort()`. + +## 1.4.1 + +* Fix all strong mode warnings. + +## 1.4.0 + +* Add a `new PriorityQueue()` constructor that forwards to `new + HeapPriorityQueue()`. + +* Deprecate top-level libraries other than `package:collection/collection.dart`, + which exports these libraries' interfaces. + +## 1.3.0 + +* Add `lowerBound` to binary search for values that might not be present. + +* Verify that the is valid for `CanonicalMap.[]`. + +## 1.2.0 + +* Add string comparators that ignore ASCII case and sort numbers numerically. + +## 1.1.3 + +* Fix type inconsistencies with `Map` and `Set`. + +## 1.1.2 + +* Export `UnmodifiableMapView` from the Dart core libraries. + +## 1.1.1 + +* Bug-fix for signatures of `isValidKey` arguments of `CanonicalizedMap`. + +## 1.1.0 + +* Add a `QueueList` class that implements both `Queue` and `List`. + +## 0.9.4 + +* Add a `CanonicalizedMap` class that canonicalizes its keys to provide a custom + equality relation. + +## 0.9.3+1 + +* Fix all analyzer hints. + +## 0.9.3 + +* Add a `MapKeySet` class that exposes an unmodifiable `Set` view of a `Map`'s + keys. + +* Add a `MapValueSet` class that takes a function from values to keys and uses + it to expose a `Set` view of a `Map`'s values. diff --git a/pkgs/collection/LICENSE b/pkgs/collection/LICENSE new file mode 100644 index 00000000..dbd2843a --- /dev/null +++ b/pkgs/collection/LICENSE @@ -0,0 +1,27 @@ +Copyright 2015, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/pkgs/collection/README.md b/pkgs/collection/README.md new file mode 100644 index 00000000..63f705c7 --- /dev/null +++ b/pkgs/collection/README.md @@ -0,0 +1,59 @@ +[![Dart CI](https://github.com/dart-lang/core/actions/workflows/collection.yaml/badge.svg)](https://github.com/dart-lang/core/actions/workflows/collection.yaml) +[![pub package](https://img.shields.io/pub/v/collection.svg)](https://pub.dev/packages/collection) +[![package publisher](https://img.shields.io/pub/publisher/collection.svg)](https://pub.dev/packages/collection/publisher) + +Contains utility functions and classes in the style of `dart:collection` to make +working with collections easier. + +## Algorithms + +The package contains functions that operate on lists. + +It contains ways to shuffle a `List`, do binary search on a sorted `List`, and +various sorting algorithms. + +## Equality + +The package provides a way to specify the equality of elements and collections. + +Collections in Dart have no inherent equality. Two sets are not equal, even +if they contain exactly the same objects as elements. + +The `Equality` interface provides a way to define such an equality. In this +case, for example, `const SetEquality(IdentityEquality())` is an equality +that considers two sets equal exactly if they contain identical elements. + +Equalities are provided for `Iterable`s, `List`s, `Set`s, and `Map`s, as well as +combinations of these, such as: + +```dart +const MapEquality(IdentityEquality(), ListEquality()); +``` + +This equality considers maps equal if they have identical keys, and the +corresponding values are lists with equal (`operator==`) values. + +## Iterable Zip + +Utilities for "zipping" a list of iterables into an iterable of lists. + +## Priority Queue + +An interface and implementation of a priority queue. + +## Wrappers + +The package contains classes that "wrap" a collection. + +A wrapper class contains an object of the same type, and it forwards all +methods to the wrapped object. + +Wrapper classes can be used in various ways, for example to restrict the type +of an object to that of a supertype, or to change the behavior of selected +functions on an existing object. + +## Features and bugs + +Please file feature requests and bugs at the [issue tracker][tracker]. + +[tracker]: https://github.com/dart-lang/collection/issues diff --git a/pkgs/collection/analysis_options.yaml b/pkgs/collection/analysis_options.yaml new file mode 100644 index 00000000..3321f3b1 --- /dev/null +++ b/pkgs/collection/analysis_options.yaml @@ -0,0 +1,16 @@ +include: package:dart_flutter_team_lints/analysis_options.yaml + +analyzer: + language: + strict-casts: true + +linter: + rules: + - avoid_unused_constructor_parameters + - cancel_subscriptions + - literal_only_boolean_expressions + - missing_whitespace_between_adjacent_strings + - no_adjacent_strings_in_list + - no_runtimeType_toString + - package_api_docs + - unnecessary_await_in_return diff --git a/pkgs/collection/lib/algorithms.dart b/pkgs/collection/lib/algorithms.dart new file mode 100644 index 00000000..ac432423 --- /dev/null +++ b/pkgs/collection/lib/algorithms.dart @@ -0,0 +1,10 @@ +// Copyright (c) 2013, 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 `collection.dart` instead. +@Deprecated('Will be removed in collection 2.0.0.') +library; + +export 'src/algorithms.dart' + show binarySearch, insertionSort, lowerBound, mergeSort, reverse, shuffle; diff --git a/pkgs/collection/lib/collection.dart b/pkgs/collection/lib/collection.dart new file mode 100644 index 00000000..73ec1797 --- /dev/null +++ b/pkgs/collection/lib/collection.dart @@ -0,0 +1,25 @@ +// Copyright (c) 2013, 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. + +export 'src/algorithms.dart' + show binarySearch, insertionSort, lowerBound, mergeSort, reverse, shuffle; +export 'src/boollist.dart'; +export 'src/canonicalized_map.dart'; +export 'src/combined_wrappers/combined_iterable.dart'; +export 'src/combined_wrappers/combined_list.dart'; +export 'src/combined_wrappers/combined_map.dart'; +export 'src/comparators.dart'; +export 'src/equality.dart'; +export 'src/equality_map.dart'; +export 'src/equality_set.dart'; +export 'src/functions.dart'; +export 'src/iterable_extensions.dart'; +export 'src/iterable_zip.dart'; +export 'src/list_extensions.dart'; +export 'src/priority_queue.dart'; +export 'src/queue_list.dart'; +export 'src/union_set.dart'; +export 'src/union_set_controller.dart'; +export 'src/unmodifiable_wrappers.dart'; +export 'src/wrappers.dart'; diff --git a/pkgs/collection/lib/equality.dart b/pkgs/collection/lib/equality.dart new file mode 100644 index 00000000..5dc158ca --- /dev/null +++ b/pkgs/collection/lib/equality.dart @@ -0,0 +1,9 @@ +// Copyright (c) 2013, 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 `collection.dart` instead. +@Deprecated('Will be removed in collection 2.0.0.') +library; + +export 'src/equality.dart'; diff --git a/pkgs/collection/lib/iterable_zip.dart b/pkgs/collection/lib/iterable_zip.dart new file mode 100644 index 00000000..bd0b1ef0 --- /dev/null +++ b/pkgs/collection/lib/iterable_zip.dart @@ -0,0 +1,9 @@ +// Copyright (c) 2013, 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 `collection.dart` instead. +@Deprecated('Will be removed in collection 2.0.0.') +library; + +export 'src/iterable_zip.dart'; diff --git a/pkgs/collection/lib/priority_queue.dart b/pkgs/collection/lib/priority_queue.dart new file mode 100644 index 00000000..7505ce47 --- /dev/null +++ b/pkgs/collection/lib/priority_queue.dart @@ -0,0 +1,9 @@ +// Copyright (c) 2014, 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 `collection.dart` instead. +@Deprecated('Will be removed in collection 2.0.0.') +library; + +export 'src/priority_queue.dart'; diff --git a/pkgs/collection/lib/src/algorithms.dart b/pkgs/collection/lib/src/algorithms.dart new file mode 100644 index 00000000..bb5843c8 --- /dev/null +++ b/pkgs/collection/lib/src/algorithms.dart @@ -0,0 +1,467 @@ +// Copyright (c) 2013, 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. + +/// A selection of data manipulation algorithms. +library; + +import 'dart:math' show Random; + +import 'utils.dart'; + +/// Returns a position of the [value] in [sortedList], if it is there. +/// +/// If the list isn't sorted according to the [compare] function, the result +/// is unpredictable. +/// +/// If [compare] is omitted, this defaults to calling [Comparable.compareTo] on +/// the objects. In this case, the objects must be [Comparable]. +/// +/// Returns -1 if [value] is not in the list. +int binarySearch(List sortedList, E value, + {int Function(E, E)? compare}) { + compare ??= defaultCompare; + return binarySearchBy(sortedList, identity, compare, value); +} + +/// Returns a position of the [value] in [sortedList], if it is there. +/// +/// If the list isn't sorted according to the [compare] function on the [keyOf] +/// property of the elements, the result is unpredictable. +/// +/// Returns -1 if [value] is not in the list by default. +/// +/// If [start] and [end] are supplied, only that range is searched, +/// and only that range need to be sorted. +int binarySearchBy(List sortedList, K Function(E element) keyOf, + int Function(K, K) compare, E value, + [int start = 0, int? end]) { + end = RangeError.checkValidRange(start, end, sortedList.length); + var min = start; + var max = end; + var key = keyOf(value); + while (min < max) { + var mid = min + ((max - min) >> 1); + var element = sortedList[mid]; + var comp = compare(keyOf(element), key); + if (comp == 0) return mid; + if (comp < 0) { + min = mid + 1; + } else { + max = mid; + } + } + return -1; +} + +/// Returns the first position in [sortedList] that does not compare less than +/// [value]. +/// +/// Uses binary search to find the location of [value]. +/// This takes on the order of `log(n)` comparisons. +/// If the list isn't sorted according to the [compare] function, the result +/// is unpredictable. +/// +/// If [compare] is omitted, this defaults to calling [Comparable.compareTo] on +/// the objects. In this case, the objects must be [Comparable]. +/// +/// Returns the length of [sortedList] if all the items in [sortedList] compare +/// less than [value]. +int lowerBound(List sortedList, E value, {int Function(E, E)? compare}) { + compare ??= defaultCompare; + return lowerBoundBy(sortedList, identity, compare, value); +} + +/// Returns the first position in [sortedList] that is not before [value]. +/// +/// Uses binary search to find the location of [value]. +/// This takes on the order of `log(n)` comparisons. +/// Elements are compared using the [compare] function of the [keyOf] property +/// of the elements. +/// If the list isn't sorted according to this order, the result is +/// unpredictable. +/// +/// Returns the length of [sortedList] if all the items in [sortedList] are +/// before [value]. +/// +/// If [start] and [end] are supplied, only that range is searched, +/// and only that range need to be sorted. +int lowerBoundBy(List sortedList, K Function(E element) keyOf, + int Function(K, K) compare, E value, + [int start = 0, int? end]) { + end = RangeError.checkValidRange(start, end, sortedList.length); + var min = start; + var max = end; + var key = keyOf(value); + while (min < max) { + var mid = min + ((max - min) >> 1); + var element = sortedList[mid]; + var comp = compare(keyOf(element), key); + if (comp < 0) { + min = mid + 1; + } else { + max = mid; + } + } + return min; +} + +/// Shuffles a list randomly. +/// +/// A sub-range of a list can be shuffled by providing [start] and [end]. +/// +/// If [start] or [end] are omitted, +/// they default to the start and end of the list. +/// +/// If [random] is omitted, it defaults to a new instance of [Random]. +void shuffle(List elements, [int start = 0, int? end, Random? random]) { + random ??= Random(); + end ??= elements.length; + var length = end - start; + while (length > 1) { + var pos = random.nextInt(length); + length--; + var tmp1 = elements[start + pos]; + elements[start + pos] = elements[start + length]; + elements[start + length] = tmp1; + } +} + +/// Reverses a list, or a part of a list, in-place. +void reverse(List elements, [int start = 0, int? end]) { + end = RangeError.checkValidRange(start, end, elements.length); + _reverse(elements, start, end); +} + +/// Internal helper function that assumes valid arguments. +void _reverse(List elements, int start, int end) { + for (var i = start, j = end - 1; i < j; i++, j--) { + var tmp = elements[i]; + elements[i] = elements[j]; + elements[j] = tmp; + } +} + +/// Sort a list between [start] (inclusive) and [end] (exclusive) using +/// insertion sort. +/// +/// If [compare] is omitted, this defaults to calling [Comparable.compareTo] on +/// the objects. In this case, the objects must be [Comparable]. +/// +/// Insertion sort is a simple sorting algorithm. For `n` elements it does on +/// the order of `n * log(n)` comparisons but up to `n` squared moves. The +/// sorting is performed in-place, without using extra memory. +/// +/// For short lists the many moves have less impact than the simple algorithm, +/// and it is often the favored sorting algorithm for short lists. +/// +/// This insertion sort is stable: Equal elements end up in the same order +/// as they started in. +void insertionSort(List elements, + {int Function(E, E)? compare, int start = 0, int? end}) { + // If the same method could have both positional and named optional + // parameters, this should be (list, [start, end], {compare}). + compare ??= defaultCompare; + end ??= elements.length; + + for (var pos = start + 1; pos < end; pos++) { + var min = start; + var max = pos; + var element = elements[pos]; + while (min < max) { + var mid = min + ((max - min) >> 1); + var comparison = compare(element, elements[mid]); + if (comparison < 0) { + max = mid; + } else { + min = mid + 1; + } + } + elements.setRange(min + 1, pos + 1, elements, min); + elements[min] = element; + } +} + +/// Generalized insertion sort. +/// +/// Performs insertion sort on the [elements] range from [start] to [end]. +/// Ordering is the [compare] of the [keyOf] of the elements. +void insertionSortBy(List elements, K Function(E element) keyOf, + int Function(K a, K b) compare, + [int start = 0, int? end]) { + end = RangeError.checkValidRange(start, end, elements.length); + _movingInsertionSort(elements, keyOf, compare, start, end, elements, start); +} + +/// Limit below which merge sort defaults to insertion sort. +const int _mergeSortLimit = 32; + +/// Sorts a list between [start] (inclusive) and [end] (exclusive) using the +/// merge sort algorithm. +/// +/// If [compare] is omitted, this defaults to calling [Comparable.compareTo] on +/// the objects. If any object is not [Comparable], that throws a [TypeError]. +/// +/// Merge-sorting works by splitting the job into two parts, sorting each +/// recursively, and then merging the two sorted parts. +/// +/// This takes on the order of `n * log(n)` comparisons and moves to sort +/// `n` elements, but requires extra space of about the same size as the list +/// being sorted. +/// +/// This merge sort is stable: Equal elements end up in the same order +/// as they started in. +void mergeSort(List elements, + {int start = 0, int? end, int Function(E, E)? compare}) { + end = RangeError.checkValidRange(start, end, elements.length); + compare ??= defaultCompare; + + var length = end - start; + if (length < 2) return; + if (length < _mergeSortLimit) { + insertionSort(elements, compare: compare, start: start, end: end); + return; + } + // Special case the first split instead of directly calling + // _mergeSort, because the _mergeSort requires its target to + // be different from its source, and it requires extra space + // of the same size as the list to sort. + // This split allows us to have only half as much extra space, + // and allows the sorted elements to end up in the original list. + var firstLength = (end - start) >> 1; + var middle = start + firstLength; + var secondLength = end - middle; + // secondLength is always the same as firstLength, or one greater. + var scratchSpace = elements.sublist(0, secondLength); + _mergeSort(elements, identity, compare, middle, end, scratchSpace, 0); + var firstTarget = end - firstLength; + _mergeSort( + elements, identity, compare, start, middle, elements, firstTarget); + _merge(identity, compare, elements, firstTarget, end, scratchSpace, 0, + secondLength, elements, start); +} + +/// Sort [elements] using a merge-sort algorithm. +/// +/// The elements are compared using [compare] on the value provided by [keyOf] +/// on the element. +/// If [start] and [end] are provided, only that range is sorted. +/// +/// Uses insertion sort for smaller sublists. +void mergeSortBy(List elements, K Function(E element) keyOf, + int Function(K a, K b) compare, + [int start = 0, int? end]) { + end = RangeError.checkValidRange(start, end, elements.length); + var length = end - start; + if (length < 2) return; + if (length < _mergeSortLimit) { + _movingInsertionSort(elements, keyOf, compare, start, end, elements, start); + return; + } + // Special case the first split instead of directly calling + // _mergeSort, because the _mergeSort requires its target to + // be different from its source, and it requires extra space + // of the same size as the list to sort. + // This split allows us to have only half as much extra space, + // and it ends up in the original place. + var middle = start + (length >> 1); + var firstLength = middle - start; + var secondLength = end - middle; + // secondLength is always the same as firstLength, or one greater. + var scratchSpace = elements.sublist(0, secondLength); + _mergeSort(elements, keyOf, compare, middle, end, scratchSpace, 0); + var firstTarget = end - firstLength; + _mergeSort(elements, keyOf, compare, start, middle, elements, firstTarget); + _merge(keyOf, compare, elements, firstTarget, end, scratchSpace, 0, + secondLength, elements, start); +} + +/// Performs an insertion sort into a potentially different list than the +/// one containing the original values. +/// +/// It will work in-place as well. +void _movingInsertionSort( + List list, + K Function(E element) keyOf, + int Function(K, K) compare, + int start, + int end, + List target, + int targetOffset) { + var length = end - start; + if (length == 0) return; + target[targetOffset] = list[start]; + for (var i = 1; i < length; i++) { + var element = list[start + i]; + var elementKey = keyOf(element); + var min = targetOffset; + var max = targetOffset + i; + while (min < max) { + var mid = min + ((max - min) >> 1); + if (compare(elementKey, keyOf(target[mid])) < 0) { + max = mid; + } else { + min = mid + 1; + } + } + target.setRange(min + 1, targetOffset + i + 1, target, min); + target[min] = element; + } +} + +/// Sorts [elements] from [start] to [end] into [target] at [targetOffset]. +/// +/// The `target` list must be able to contain the range from `start` to `end` +/// after `targetOffset`. +/// +/// Allows target to be the same list as [elements], as long as it's not +/// overlapping the `start..end` range. +void _mergeSort( + List elements, + K Function(E element) keyOf, + int Function(K, K) compare, + int start, + int end, + List target, + int targetOffset) { + var length = end - start; + if (length < _mergeSortLimit) { + _movingInsertionSort( + elements, keyOf, compare, start, end, target, targetOffset); + return; + } + var middle = start + (length >> 1); + var firstLength = middle - start; + var secondLength = end - middle; + // Here secondLength >= firstLength (differs by at most one). + var targetMiddle = targetOffset + firstLength; + // Sort the second half into the end of the target area. + _mergeSort(elements, keyOf, compare, middle, end, target, targetMiddle); + // Sort the first half into the end of the source area. + _mergeSort(elements, keyOf, compare, start, middle, elements, middle); + // Merge the two parts into the target area. + _merge(keyOf, compare, elements, middle, middle + firstLength, target, + targetMiddle, targetMiddle + secondLength, target, targetOffset); +} + +/// Merges two lists into a target list. +/// +/// One of the input lists may be positioned at the end of the target +/// list. +/// +/// For equal object, elements from [firstList] are always preferred. +/// This allows the merge to be stable if the first list contains elements +/// that started out earlier than the ones in [secondList] +void _merge( + K Function(E element) keyOf, + int Function(K, K) compare, + List firstList, + int firstStart, + int firstEnd, + List secondList, + int secondStart, + int secondEnd, + List target, + int targetOffset) { + // No empty lists reaches here. + assert(firstStart < firstEnd); + assert(secondStart < secondEnd); + var cursor1 = firstStart; + var cursor2 = secondStart; + var firstElement = firstList[cursor1++]; + var firstKey = keyOf(firstElement); + var secondElement = secondList[cursor2++]; + var secondKey = keyOf(secondElement); + while (true) { + if (compare(firstKey, secondKey) <= 0) { + target[targetOffset++] = firstElement; + if (cursor1 == firstEnd) break; // Flushing second list after loop. + firstElement = firstList[cursor1++]; + firstKey = keyOf(firstElement); + } else { + target[targetOffset++] = secondElement; + if (cursor2 != secondEnd) { + secondElement = secondList[cursor2++]; + secondKey = keyOf(secondElement); + continue; + } + // Second list empties first. Flushing first list here. + target[targetOffset++] = firstElement; + target.setRange(targetOffset, targetOffset + (firstEnd - cursor1), + firstList, cursor1); + return; + } + } + // First list empties first. Reached by break above. + target[targetOffset++] = secondElement; + target.setRange( + targetOffset, targetOffset + (secondEnd - cursor2), secondList, cursor2); +} + +/// Sort [elements] using a quick-sort algorithm. +/// +/// The elements are compared using [compare] on the elements. +/// If [start] and [end] are provided, only that range is sorted. +/// +/// Uses insertion sort for smaller sublists. +void quickSort(List elements, int Function(E a, E b) compare, + [int start = 0, int? end]) { + end = RangeError.checkValidRange(start, end, elements.length); + _quickSort(elements, identity, compare, Random(), start, end); +} + +/// Sort [list] using a quick-sort algorithm. +/// +/// The elements are compared using [compare] on the value provided by [keyOf] +/// on the element. +/// If [start] and [end] are provided, only that range is sorted. +/// +/// Uses insertion sort for smaller sublists. +void quickSortBy( + List list, K Function(E element) keyOf, int Function(K a, K b) compare, + [int start = 0, int? end]) { + end = RangeError.checkValidRange(start, end, list.length); + _quickSort(list, keyOf, compare, Random(), start, end); +} + +void _quickSort(List list, K Function(E element) keyOf, + int Function(K a, K b) compare, Random random, int start, int end) { + const minQuickSortLength = 24; + var length = end - start; + while (length >= minQuickSortLength) { + var pivotIndex = random.nextInt(length) + start; + var pivot = list[pivotIndex]; + var pivotKey = keyOf(pivot); + var endSmaller = start; + var startGreater = end; + var startPivots = end - 1; + list[pivotIndex] = list[startPivots]; + list[startPivots] = pivot; + while (endSmaller < startPivots) { + var current = list[endSmaller]; + var relation = compare(keyOf(current), pivotKey); + if (relation < 0) { + endSmaller++; + } else { + startPivots--; + var currentTarget = startPivots; + list[endSmaller] = list[startPivots]; + if (relation > 0) { + startGreater--; + currentTarget = startGreater; + list[startPivots] = list[startGreater]; + } + list[currentTarget] = current; + } + } + if (endSmaller - start < end - startGreater) { + _quickSort(list, keyOf, compare, random, start, endSmaller); + start = startGreater; + } else { + _quickSort(list, keyOf, compare, random, startGreater, end); + end = endSmaller; + } + length = end - start; + } + _movingInsertionSort(list, keyOf, compare, start, end, list, start); +} diff --git a/pkgs/collection/lib/src/boollist.dart b/pkgs/collection/lib/src/boollist.dart new file mode 100644 index 00000000..b026d858 --- /dev/null +++ b/pkgs/collection/lib/src/boollist.dart @@ -0,0 +1,273 @@ +// Copyright (c) 2021, 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 'dart:collection' show ListMixin; +import 'dart:typed_data' show Uint32List; + +import 'unmodifiable_wrappers.dart' show NonGrowableListMixin; + +/// A space-efficient list of boolean values. +/// +/// Uses list of integers as internal storage to reduce memory usage. +abstract /*mixin*/ class BoolList with ListMixin { + static const int _entryShift = 5; + + static const int _bitsPerEntry = 32; + + static const int _entrySignBitIndex = 31; + + /// The length of the list. + /// + /// Maybe be shorter than the capacity of the backing store. + int _length; + + /// Backing store for bits. + Uint32List _data; + + BoolList._(this._data, this._length); + + factory BoolList._selectType(int length, bool growable) { + if (growable) { + return _GrowableBoolList(length); + } else { + return _NonGrowableBoolList(length); + } + } + + /// Creates a list of booleans with the provided length. + /// + /// The list is initially filled with the [fill] value, and + /// the list is growable if [growable] is true. + factory BoolList(int length, {bool fill = false, bool growable = false}) { + RangeError.checkNotNegative(length, 'length'); + + BoolList boolList; + if (growable) { + boolList = _GrowableBoolList(length); + } else { + boolList = _NonGrowableBoolList(length); + } + + if (fill) { + boolList.fillRange(0, length, true); + } + + return boolList; + } + + /// Creates an empty list of booleans. + /// + /// The list defaults to being growable unless [growable] is `false`. + /// If [capacity] is provided, and [growable] is not `false`, + /// the implementation will attempt to make space for that + /// many elements before needing to grow its internal storage. + factory BoolList.empty({bool growable = true, int capacity = 0}) { + RangeError.checkNotNegative(capacity, 'length'); + + if (growable) { + return _GrowableBoolList._withCapacity(0, capacity); + } else { + return _NonGrowableBoolList._withCapacity(0, capacity); + } + } + + /// Generates a [BoolList] of values. + /// + /// Creates a [BoolList] with [length] positions and fills it with values + /// created by calling [generator] for each index in the range + /// `0` .. `length - 1` in increasing order. + /// + /// The created list is fixed-length unless [growable] is true. + factory BoolList.generate( + int length, + bool Function(int) generator, { + bool growable = true, + }) { + RangeError.checkNotNegative(length, 'length'); + + var instance = BoolList._selectType(length, growable); + for (var i = 0; i < length; i++) { + instance._setBit(i, generator(i)); + } + return instance; + } + + /// Creates a list containing all [elements]. + /// + /// The [Iterator] of [elements] provides the order of the elements. + /// + /// This constructor creates a growable [BoolList] when [growable] is true; + /// otherwise, it returns a fixed-length list. + factory BoolList.of(Iterable elements, {bool growable = false}) { + return BoolList._selectType(elements.length, growable)..setAll(0, elements); + } + + /// The number of boolean values in this list. + /// + /// The valid indices for a list are `0` through `length - 1`. + /// + /// If the list is growable, setting the length will change the + /// number of values. + /// Setting the length to a smaller number will remove all + /// values with indices greater than or equal to the new length. + /// Setting the length to a larger number will increase the number of + /// values, and all the new values will be `false`. + @override + int get length => _length; + + @override + bool operator [](int index) { + RangeError.checkValidIndex(index, this, 'index', _length); + return (_data[index >> _entryShift] & + (1 << (index & _entrySignBitIndex))) != + 0; + } + + @override + void operator []=(int index, bool value) { + RangeError.checkValidIndex(index, this, 'index', _length); + _setBit(index, value); + } + + @override + void fillRange(int start, int end, [bool? fill]) { + RangeError.checkValidRange(start, end, _length); + fill ??= false; + + var startWord = start >> _entryShift; + var endWord = (end - 1) >> _entryShift; + + var startBit = start & _entrySignBitIndex; + var endBit = (end - 1) & _entrySignBitIndex; + + if (startWord < endWord) { + if (fill) { + _data[startWord] |= -1 << startBit; + _data.fillRange(startWord + 1, endWord, -1); + _data[endWord] |= (1 << (endBit + 1)) - 1; + } else { + _data[startWord] &= (1 << startBit) - 1; + _data.fillRange(startWord + 1, endWord, 0); + _data[endWord] &= -1 << (endBit + 1); + } + } else { + if (fill) { + _data[startWord] |= ((1 << (endBit - startBit + 1)) - 1) << startBit; + } else { + _data[startWord] &= ((1 << startBit) - 1) | (-1 << (endBit + 1)); + } + } + } + + /// Creates an iterator for the elements of this [BoolList]. + /// + /// The [Iterator.current] getter of the returned iterator + /// is `false` when the iterator has no current element. + @override + Iterator get iterator => _BoolListIterator(this); + + void _setBit(int index, bool value) { + if (value) { + _data[index >> _entryShift] |= 1 << (index & _entrySignBitIndex); + } else { + _data[index >> _entryShift] &= ~(1 << (index & _entrySignBitIndex)); + } + } + + static int _lengthInWords(int bitLength) { + return (bitLength + (_bitsPerEntry - 1)) >> _entryShift; + } +} + +class _GrowableBoolList extends BoolList { + static const int _growthFactor = 2; + + _GrowableBoolList._withCapacity(int length, int capacity) + : super._( + Uint32List(BoolList._lengthInWords(capacity)), + length, + ); + + _GrowableBoolList(int length) + : super._( + Uint32List(BoolList._lengthInWords(length * _growthFactor)), + length, + ); + + @override + set length(int length) { + RangeError.checkNotNegative(length, 'length'); + if (length > _length) { + _expand(length); + } else if (length < _length) { + _shrink(length); + } + } + + void _expand(int length) { + if (length > _data.length * BoolList._bitsPerEntry) { + _data = Uint32List( + BoolList._lengthInWords(length * _growthFactor), + )..setRange(0, _data.length, _data); + } + _length = length; + } + + void _shrink(int length) { + if (length < _length ~/ _growthFactor) { + var newDataLength = BoolList._lengthInWords(length); + _data = Uint32List(newDataLength)..setRange(0, newDataLength, _data); + } + + for (var i = length; i < _data.length * BoolList._bitsPerEntry; i++) { + _setBit(i, false); + } + + _length = length; + } +} + +class _NonGrowableBoolList extends BoolList with NonGrowableListMixin { + _NonGrowableBoolList._withCapacity(int length, int capacity) + : super._( + Uint32List(BoolList._lengthInWords(capacity)), + length, + ); + + _NonGrowableBoolList(int length) + : super._( + Uint32List(BoolList._lengthInWords(length)), + length, + ); +} + +class _BoolListIterator implements Iterator { + bool _current = false; + int _pos = 0; + final int _length; + + final BoolList _boolList; + + _BoolListIterator(this._boolList) : _length = _boolList._length; + + @override + bool get current => _current; + + @override + bool moveNext() { + if (_boolList._length != _length) { + throw ConcurrentModificationError(_boolList); + } + + if (_pos < _boolList.length) { + var pos = _pos++; + _current = _boolList._data[pos >> BoolList._entryShift] & + (1 << (pos & BoolList._entrySignBitIndex)) != + 0; + return true; + } + _current = false; + return false; + } +} diff --git a/pkgs/collection/lib/src/canonicalized_map.dart b/pkgs/collection/lib/src/canonicalized_map.dart new file mode 100644 index 00000000..3dc6e37c --- /dev/null +++ b/pkgs/collection/lib/src/canonicalized_map.dart @@ -0,0 +1,200 @@ +// Copyright (c) 2014, 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 'dart:collection'; + +/// A map whose keys are converted to canonical values of type `C`. +/// +/// This is useful for using case-insensitive String keys, for example. It's +/// more efficient than a [LinkedHashMap] with a custom equality operator +/// because it only canonicalizes each key once, rather than doing so for each +/// comparison. +class CanonicalizedMap implements Map { + final C Function(K) _canonicalize; + + final bool Function(K)? _isValidKeyFn; + + final _base = >{}; + + /// Creates an empty canonicalized map. + /// + /// The [canonicalize] function should return the canonical value for the + /// given key. Keys with the same canonical value are considered equivalent. + /// + /// The [isValidKey] function is called before calling [canonicalize] for + /// methods that take arbitrary objects. It can be used to filter out keys + /// that can't be canonicalized. + CanonicalizedMap(C Function(K key) canonicalize, + {bool Function(K key)? isValidKey}) + : _canonicalize = canonicalize, + _isValidKeyFn = isValidKey; + + /// Creates a canonicalized map that is initialized with the key/value pairs + /// of [other]. + /// + /// The [canonicalize] function should return the canonical value for the + /// given key. Keys with the same canonical value are considered equivalent. + /// + /// The [isValidKey] function is called before calling [canonicalize] for + /// methods that take arbitrary objects. It can be used to filter out keys + /// that can't be canonicalized. + CanonicalizedMap.from(Map other, C Function(K key) canonicalize, + {bool Function(K key)? isValidKey}) + : _canonicalize = canonicalize, + _isValidKeyFn = isValidKey { + addAll(other); + } + + /// Creates a canonicalized map that is initialized with the key/value pairs + /// of [entries]. + /// + /// The [canonicalize] function should return the canonical value for the + /// given key. Keys with the same canonical value are considered equivalent. + /// + /// The [isValidKey] function is called before calling [canonicalize] for + /// methods that take arbitrary objects. It can be used to filter out keys + /// that can't be canonicalized. + CanonicalizedMap.fromEntries( + Iterable> entries, C Function(K key) canonicalize, + {bool Function(K key)? isValidKey}) + : _canonicalize = canonicalize, + _isValidKeyFn = isValidKey { + addEntries(entries); + } + + CanonicalizedMap._( + this._canonicalize, this._isValidKeyFn, Map> base) { + _base.addAll(base); + } + + /// Copies this [CanonicalizedMap] instance without recalculating the + /// canonical values of the keys. + CanonicalizedMap copy() => + CanonicalizedMap._(_canonicalize, _isValidKeyFn, _base); + + @override + V? operator [](Object? key) { + if (!_isValidKey(key)) return null; + var pair = _base[_canonicalize(key as K)]; + return pair?.value; + } + + @override + void operator []=(K key, V value) { + if (!_isValidKey(key)) return; + _base[_canonicalize(key)] = MapEntry(key, value); + } + + @override + void addAll(Map other) { + other.forEach((key, value) => this[key] = value); + } + + @override + void addEntries(Iterable> entries) => _base.addEntries(entries + .map((e) => MapEntry(_canonicalize(e.key), MapEntry(e.key, e.value)))); + + @override + Map cast() => _base.cast(); + + @override + void clear() { + _base.clear(); + } + + @override + bool containsKey(Object? key) { + if (!_isValidKey(key)) return false; + return _base.containsKey(_canonicalize(key as K)); + } + + @override + bool containsValue(Object? value) => + _base.values.any((pair) => pair.value == value); + + @override + Iterable> get entries => + _base.entries.map((e) => MapEntry(e.value.key, e.value.value)); + + @override + void forEach(void Function(K, V) f) { + _base.forEach((key, pair) => f(pair.key, pair.value)); + } + + @override + bool get isEmpty => _base.isEmpty; + + @override + bool get isNotEmpty => _base.isNotEmpty; + + @override + Iterable get keys => _base.values.map((pair) => pair.key); + + @override + int get length => _base.length; + + @override + Map map(MapEntry Function(K, V) transform) => + _base.map((_, pair) => transform(pair.key, pair.value)); + + @override + V putIfAbsent(K key, V Function() ifAbsent) { + return _base + .putIfAbsent(_canonicalize(key), () => MapEntry(key, ifAbsent())) + .value; + } + + @override + V? remove(Object? key) { + if (!_isValidKey(key)) return null; + var pair = _base.remove(_canonicalize(key as K)); + return pair?.value; + } + + @override + void removeWhere(bool Function(K key, V value) test) => + _base.removeWhere((_, pair) => test(pair.key, pair.value)); + + @Deprecated('Use cast instead') + Map retype() => cast(); + + @override + V update(K key, V Function(V) update, {V Function()? ifAbsent}) => + _base.update(_canonicalize(key), (pair) { + var value = pair.value; + var newValue = update(value); + if (identical(newValue, value)) return pair; + return MapEntry(key, newValue); + }, + ifAbsent: + ifAbsent == null ? null : () => MapEntry(key, ifAbsent())).value; + + @override + void updateAll(V Function(K key, V value) update) => + _base.updateAll((_, pair) { + var value = pair.value; + var key = pair.key; + var newValue = update(key, value); + if (identical(value, newValue)) return pair; + return MapEntry(key, newValue); + }); + + @override + Iterable get values => _base.values.map((pair) => pair.value); + + @override + String toString() => MapBase.mapToString(this); + + bool _isValidKey(Object? key) => + (key is K) && (_isValidKeyFn == null || _isValidKeyFn(key)); + + /// Creates a `Map` (with the original key values). + /// See [toMapOfCanonicalKeys]. + Map toMap() => Map.fromEntries(_base.values); + + /// Creates a `Map` (with the canonicalized keys). + /// See [toMap]. + Map toMapOfCanonicalKeys() => Map.fromEntries( + _base.entries.map((e) => MapEntry(e.key, e.value.value))); +} diff --git a/pkgs/collection/lib/src/combined_wrappers/combined_iterable.dart b/pkgs/collection/lib/src/combined_wrappers/combined_iterable.dart new file mode 100644 index 00000000..281f8a28 --- /dev/null +++ b/pkgs/collection/lib/src/combined_wrappers/combined_iterable.dart @@ -0,0 +1,38 @@ +// Copyright (c) 2017, 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 'dart:collection'; + +import 'combined_iterator.dart'; + +/// A view of several iterables combined sequentially into a single iterable. +/// +/// All methods and accessors treat the [CombinedIterableView] as if it were a +/// single concatenated iterable, but the underlying implementation is based on +/// lazily accessing individual iterable instances. This means that if the +/// underlying iterables change, the [CombinedIterableView] will reflect those +/// changes. +class CombinedIterableView extends IterableBase { + /// The iterables that this combines. + final Iterable> _iterables; + + /// Creates a combined view of [_iterables]. + const CombinedIterableView(this._iterables); + + @override + Iterator get iterator => + CombinedIterator(_iterables.map((i) => i.iterator).iterator); + + // Special cased contains/isEmpty/length since many iterables have an + // efficient implementation instead of running through the entire iterator. + + @override + bool contains(Object? element) => _iterables.any((i) => i.contains(element)); + + @override + bool get isEmpty => _iterables.every((i) => i.isEmpty); + + @override + int get length => _iterables.fold(0, (length, i) => length + i.length); +} diff --git a/pkgs/collection/lib/src/combined_wrappers/combined_iterator.dart b/pkgs/collection/lib/src/combined_wrappers/combined_iterator.dart new file mode 100644 index 00000000..0d6088a9 --- /dev/null +++ b/pkgs/collection/lib/src/combined_wrappers/combined_iterator.dart @@ -0,0 +1,39 @@ +// Copyright (c) 2020, 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. + +/// The iterator for `CombinedIterableView` and `CombinedListView`. +/// +/// Moves through each iterable's iterator in sequence. +class CombinedIterator implements Iterator { + /// The iterators that this combines, or `null` if done iterating. + /// + /// Because this comes from a call to [Iterable.map], it's lazy and will + /// avoid instantiating unnecessary iterators. + Iterator>? _iterators; + + CombinedIterator(Iterator> iterators) : _iterators = iterators { + if (!iterators.moveNext()) _iterators = null; + } + + @override + T get current { + var iterators = _iterators; + if (iterators != null) return iterators.current.current; + return null as T; + } + + @override + bool moveNext() { + var iterators = _iterators; + if (iterators != null) { + do { + if (iterators.current.moveNext()) { + return true; + } + } while (iterators.moveNext()); + _iterators = null; + } + return false; + } +} diff --git a/pkgs/collection/lib/src/combined_wrappers/combined_list.dart b/pkgs/collection/lib/src/combined_wrappers/combined_list.dart new file mode 100644 index 00000000..f0cd4476 --- /dev/null +++ b/pkgs/collection/lib/src/combined_wrappers/combined_list.dart @@ -0,0 +1,79 @@ +// Copyright (c) 2017, 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 'dart:collection'; + +import 'combined_iterator.dart'; + +/// A view of several lists combined into a single list. +/// +/// All methods and accessors treat the [CombinedListView] list as if it were a +/// single concatenated list, but the underlying implementation is based on +/// lazily accessing individual list instances. This means that if the +/// underlying lists change, the [CombinedListView] will reflect those changes. +/// +/// The index operator (`[]`) and [length] property of a [CombinedListView] are +/// both `O(lists)` rather than `O(1)`. A [CombinedListView] is unmodifiable. +class CombinedListView extends ListBase + implements UnmodifiableListView { + static Never _throw() { + throw UnsupportedError('Cannot modify an unmodifiable List'); + } + + /// The lists that this combines. + final List> _lists; + + /// Creates a combined view of [_lists]. + CombinedListView(this._lists); + + @override + Iterator get iterator => + CombinedIterator(_lists.map((i) => i.iterator).iterator); + + @override + set length(int length) { + _throw(); + } + + @override + int get length => _lists.fold(0, (length, list) => length + list.length); + + @override + T operator [](int index) { + var initialIndex = index; + for (var i = 0; i < _lists.length; i++) { + var list = _lists[i]; + if (index < list.length) { + return list[index]; + } + index -= list.length; + } + throw RangeError.index(initialIndex, this, 'index', null, length); + } + + @override + void operator []=(int index, T value) { + _throw(); + } + + @override + void clear() { + _throw(); + } + + @override + bool remove(Object? element) { + _throw(); + } + + @override + void removeWhere(bool Function(T) test) { + _throw(); + } + + @override + void retainWhere(bool Function(T) test) { + _throw(); + } +} diff --git a/pkgs/collection/lib/src/combined_wrappers/combined_map.dart b/pkgs/collection/lib/src/combined_wrappers/combined_map.dart new file mode 100644 index 00000000..18db6e7e --- /dev/null +++ b/pkgs/collection/lib/src/combined_wrappers/combined_map.dart @@ -0,0 +1,104 @@ +// Copyright (c) 2017, 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 'dart:collection'; + +import 'combined_iterable.dart'; + +/// Returns a new map that represents maps flattened into a single map. +/// +/// All methods and accessors treat the new map as-if it were a single +/// concatenated map, but the underlying implementation is based on lazily +/// accessing individual map instances. In the occasion where a key occurs in +/// multiple maps the first value is returned. +/// +/// The resulting map has an index operator (`[]`) that is `O(maps)`, rather +/// than `O(1)`, and the map is unmodifiable, but underlying changes to these +/// maps are still accessible from the resulting map. +/// +/// The `length` getter is `O(M)` where M is the total number of entries in +/// all maps, since it has to remove duplicate entries. +class CombinedMapView extends UnmodifiableMapBase { + final Iterable> _maps; + + /// Create a new combined view of multiple maps. + /// + /// The iterable is accessed lazily so it should be collection type like + /// [List] or [Set] rather than a lazy iterable produced by `map()` et al. + CombinedMapView(this._maps); + + @override + V? operator [](Object? key) { + for (var map in _maps) { + // Avoid two hash lookups on a positive hit. + var value = map[key]; + if (value != null || map.containsKey(value)) { + return value; + } + } + return null; + } + + /// The keys of `this`. + /// + /// The returned iterable has efficient `contains` operations, assuming the + /// iterables returned by the wrapped maps have efficient `contains` + /// operations for their `keys` iterables. + /// + /// The `length` must do deduplication and thus is not optimized. + /// + /// The order of iteration is defined by the individual `Map` implementations, + /// but must be consistent between changes to the maps. + /// + /// Unlike most [Map] implementations, modifying an individual map while + /// iterating the keys will _sometimes_ throw. This behavior may change in + /// the future. + @override + Iterable get keys => _DeduplicatingIterableView( + CombinedIterableView(_maps.map((m) => m.keys))); +} + +/// A view of an iterable that skips any duplicate entries. +class _DeduplicatingIterableView extends IterableBase { + final Iterable _iterable; + + const _DeduplicatingIterableView(this._iterable); + + @override + Iterator get iterator => _DeduplicatingIterator(_iterable.iterator); + + // Special cased contains/isEmpty since many iterables have an efficient + // implementation instead of running through the entire iterator. + // + // Note: We do not do this for `length` because we have to remove the + // duplicates. + + @override + bool contains(Object? element) => _iterable.contains(element); + + @override + bool get isEmpty => _iterable.isEmpty; +} + +/// An iterator that wraps another iterator and skips duplicate values. +class _DeduplicatingIterator implements Iterator { + final Iterator _iterator; + + final _emitted = HashSet(); + + _DeduplicatingIterator(this._iterator); + + @override + T get current => _iterator.current; + + @override + bool moveNext() { + while (_iterator.moveNext()) { + if (_emitted.add(current)) { + return true; + } + } + return false; + } +} diff --git a/pkgs/collection/lib/src/comparators.dart b/pkgs/collection/lib/src/comparators.dart new file mode 100644 index 00000000..6e5d3631 --- /dev/null +++ b/pkgs/collection/lib/src/comparators.dart @@ -0,0 +1,393 @@ +// Copyright (c) 2015, 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. + +// Character constants. +const int _zero = 0x30; +const int _upperCaseA = 0x41; +const int _upperCaseZ = 0x5a; +const int _lowerCaseA = 0x61; +const int _lowerCaseZ = 0x7a; +const int _asciiCaseBit = 0x20; + +/// Checks if strings [a] and [b] differ only on the case of ASCII letters. +/// +/// Strings are equal if they have the same length, and the characters at +/// each index are the same, or they are ASCII letters where one is upper-case +/// and the other is the lower-case version of the same letter. +/// +/// The comparison does not ignore the case of non-ASCII letters, so +/// an upper-case ae-ligature (Æ) is different from +/// a lower case ae-ligature (æ). +/// +/// Ignoring non-ASCII letters is not generally a good idea, but it makes sense +/// for situations where the strings are known to be ASCII. Examples could +/// be Dart identifiers, base-64 or hex encoded strings, GUIDs or similar +/// strings with a known structure. +bool equalsIgnoreAsciiCase(String a, String b) { + if (a.length != b.length) return false; + for (var i = 0; i < a.length; i++) { + var aChar = a.codeUnitAt(i); + var bChar = b.codeUnitAt(i); + if (aChar == bChar) continue; + // Quick-check for whether this may be different cases of the same letter. + if (aChar ^ bChar != _asciiCaseBit) return false; + // If it's possible, then check if either character is actually an ASCII + // letter. + var aCharLowerCase = aChar | _asciiCaseBit; + if (_lowerCaseA <= aCharLowerCase && aCharLowerCase <= _lowerCaseZ) { + continue; + } + return false; + } + return true; +} + +/// Hash code for a string which is compatible with [equalsIgnoreAsciiCase]. +/// +/// The hash code is unaffected by changing the case of ASCII letters, but +/// the case of non-ASCII letters do affect the result. +int hashIgnoreAsciiCase(String string) { + // Jenkins hash code ( http://en.wikipedia.org/wiki/Jenkins_hash_function). + // adapted to smi values. + // Same hash used by dart2js for strings, modified to ignore ASCII letter + // case. + var hash = 0; + for (var i = 0; i < string.length; i++) { + var char = string.codeUnitAt(i); + // Convert lower-case ASCII letters to upper case.upper + // This ensures that strings that differ only in case will have the + // same hash code. + if (_lowerCaseA <= char && char <= _lowerCaseZ) char -= _asciiCaseBit; + hash = 0x1fffffff & (hash + char); + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + hash >>= 6; + } + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + hash >>= 11; + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); +} + +/// Compares [a] and [b] lexically, converting ASCII letters to upper case. +/// +/// Comparison treats all lower-case ASCII letters as upper-case letters, +/// but does no case conversion for non-ASCII letters. +/// +/// If two strings differ only on the case of ASCII letters, the one with the +/// capital letter at the first difference will compare as less than the other +/// string. This tie-breaking ensures that the comparison is a total ordering +/// on strings and is compatible with equality. +/// +/// Ignoring non-ASCII letters is not generally a good idea, but it makes sense +/// for situations where the strings are known to be ASCII. Examples could +/// be Dart identifiers, base-64 or hex encoded strings, GUIDs or similar +/// strings with a known structure. +int compareAsciiUpperCase(String a, String b) { + var defaultResult = 0; // Returned if no difference found. + for (var i = 0; i < a.length; i++) { + if (i >= b.length) return 1; + var aChar = a.codeUnitAt(i); + var bChar = b.codeUnitAt(i); + if (aChar == bChar) continue; + // Upper-case if letters. + var aUpperCase = aChar; + var bUpperCase = bChar; + if (_lowerCaseA <= aChar && aChar <= _lowerCaseZ) { + aUpperCase -= _asciiCaseBit; + } + if (_lowerCaseA <= bChar && bChar <= _lowerCaseZ) { + bUpperCase -= _asciiCaseBit; + } + if (aUpperCase != bUpperCase) return (aUpperCase - bUpperCase).sign; + if (defaultResult == 0) defaultResult = aChar - bChar; + } + if (b.length > a.length) return -1; + return defaultResult.sign; +} + +/// Compares [a] and [b] lexically, converting ASCII letters to lower case. +/// +/// Comparison treats all upper-case ASCII letters as lower-case letters, +/// but does no case conversion for non-ASCII letters. +/// +/// If two strings differ only on the case of ASCII letters, the one with the +/// capital letter at the first difference will compare as less than the other +/// string. This tie-breaking ensures that the comparison is a total ordering +/// on strings. +/// +/// Ignoring non-ASCII letters is not generally a good idea, but it makes sense +/// for situations where the strings are known to be ASCII. Examples could +/// be Dart identifiers, base-64 or hex encoded strings, GUIDs or similar +/// strings with a known structure. +int compareAsciiLowerCase(String a, String b) { + var defaultResult = 0; + for (var i = 0; i < a.length; i++) { + if (i >= b.length) return 1; + var aChar = a.codeUnitAt(i); + var bChar = b.codeUnitAt(i); + if (aChar == bChar) continue; + var aLowerCase = aChar; + var bLowerCase = bChar; + // Upper case if ASCII letters. + if (_upperCaseA <= bChar && bChar <= _upperCaseZ) { + bLowerCase += _asciiCaseBit; + } + if (_upperCaseA <= aChar && aChar <= _upperCaseZ) { + aLowerCase += _asciiCaseBit; + } + if (aLowerCase != bLowerCase) return (aLowerCase - bLowerCase).sign; + if (defaultResult == 0) defaultResult = aChar - bChar; + } + if (b.length > a.length) return -1; + return defaultResult.sign; +} + +/// Compares strings [a] and [b] according to [natural sort ordering][]. +/// +/// A natural sort ordering is a lexical ordering where embedded +/// numerals (digit sequences) are treated as a single unit and ordered by +/// numerical value. +/// This means that `"a10b"` will be ordered after `"a7b"` in natural +/// ordering, where lexical ordering would put the `1` before the `7`, ignoring +/// that the `1` is part of a larger number. +/// +/// Example: +/// The following strings are in the order they would be sorted by using this +/// comparison function: +/// +/// "a", "a0", "a0b", "a1", "a01", "a9", "a10", "a100", "a100b", "aa" +/// +/// [natural sort ordering]: https://en.wikipedia.org/wiki/Natural_sort_order +int compareNatural(String a, String b) { + for (var i = 0; i < a.length; i++) { + if (i >= b.length) return 1; + var aChar = a.codeUnitAt(i); + var bChar = b.codeUnitAt(i); + if (aChar != bChar) { + return _compareNaturally(a, b, i, aChar, bChar); + } + } + if (b.length > a.length) return -1; + return 0; +} + +/// Compares strings [a] and [b] according to lower-case +/// [natural sort ordering][]. +/// +/// ASCII letters are converted to lower case before being compared, like +/// for [compareAsciiLowerCase], then the result is compared like for +/// [compareNatural]. +/// +/// If two strings differ only on the case of ASCII letters, the one with the +/// capital letter at the first difference will compare as less than the other +/// string. This tie-breaking ensures that the comparison is a total ordering +/// on strings. +/// +/// [natural sort ordering]: https://en.wikipedia.org/wiki/Natural_sort_order +int compareAsciiLowerCaseNatural(String a, String b) { + var defaultResult = 0; // Returned if no difference found. + for (var i = 0; i < a.length; i++) { + if (i >= b.length) return 1; + var aChar = a.codeUnitAt(i); + var bChar = b.codeUnitAt(i); + if (aChar == bChar) continue; + var aLowerCase = aChar; + var bLowerCase = bChar; + if (_upperCaseA <= aChar && aChar <= _upperCaseZ) { + aLowerCase += _asciiCaseBit; + } + if (_upperCaseA <= bChar && bChar <= _upperCaseZ) { + bLowerCase += _asciiCaseBit; + } + if (aLowerCase != bLowerCase) { + return _compareNaturally(a, b, i, aLowerCase, bLowerCase); + } + if (defaultResult == 0) defaultResult = aChar - bChar; + } + if (b.length > a.length) return -1; + return defaultResult.sign; +} + +/// Compares strings [a] and [b] according to upper-case +/// [natural sort ordering][]. +/// +/// ASCII letters are converted to upper case before being compared, like +/// for [compareAsciiUpperCase], then the result is compared like for +/// [compareNatural]. +/// +/// If two strings differ only on the case of ASCII letters, the one with the +/// capital letter at the first difference will compare as less than the other +/// string. This tie-breaking ensures that the comparison is a total ordering +/// on strings +/// +/// [natural sort ordering]: https://en.wikipedia.org/wiki/Natural_sort_order +int compareAsciiUpperCaseNatural(String a, String b) { + var defaultResult = 0; + for (var i = 0; i < a.length; i++) { + if (i >= b.length) return 1; + var aChar = a.codeUnitAt(i); + var bChar = b.codeUnitAt(i); + if (aChar == bChar) continue; + var aUpperCase = aChar; + var bUpperCase = bChar; + if (_lowerCaseA <= aChar && aChar <= _lowerCaseZ) { + aUpperCase -= _asciiCaseBit; + } + if (_lowerCaseA <= bChar && bChar <= _lowerCaseZ) { + bUpperCase -= _asciiCaseBit; + } + if (aUpperCase != bUpperCase) { + return _compareNaturally(a, b, i, aUpperCase, bUpperCase); + } + if (defaultResult == 0) defaultResult = aChar - bChar; + } + if (b.length > a.length) return -1; + return defaultResult.sign; +} + +/// Check for numbers overlapping the current mismatched characters. +/// +/// If both [aChar] and [bChar] are digits, use numerical comparison. +/// Check if the previous characters is a non-zero number, and if not, +/// skip - but count - leading zeros before comparing numbers. +/// +/// If one is a digit and the other isn't, check if the previous character +/// is a digit, and if so, the the one with the digit is the greater number. +/// +/// Otherwise just returns the difference between [aChar] and [bChar]. +int _compareNaturally(String a, String b, int index, int aChar, int bChar) { + assert(aChar != bChar); + var aIsDigit = _isDigit(aChar); + var bIsDigit = _isDigit(bChar); + if (aIsDigit) { + if (bIsDigit) { + return _compareNumerically(a, b, aChar, bChar, index); + } else if (index > 0 && _isDigit(a.codeUnitAt(index - 1))) { + // aChar is the continuation of a longer number. + return 1; + } + } else if (bIsDigit && index > 0 && _isDigit(b.codeUnitAt(index - 1))) { + // bChar is the continuation of a longer number. + return -1; + } + // Characters are both non-digits, or not continuation of earlier number. + return (aChar - bChar).sign; +} + +/// Compare numbers overlapping [aChar] and [bChar] numerically. +/// +/// If the numbers have the same numerical value, but one has more leading +/// zeros, the longer number is considered greater than the shorter one. +/// +/// This ensures a total ordering on strings compatible with equality. +int _compareNumerically(String a, String b, int aChar, int bChar, int index) { + // Both are digits. Find the first significant different digit, then find + // the length of the numbers. + if (_isNonZeroNumberSuffix(a, index)) { + // Part of a longer number, differs at this index, just count the length. + var result = _compareDigitCount(a, b, index, index); + if (result != 0) return result; + // If same length, the current character is the most significant differing + // digit. + return (aChar - bChar).sign; + } + // Not part of larger (non-zero) number, so skip leading zeros before + // comparing numbers. + var aIndex = index; + var bIndex = index; + if (aChar == _zero) { + do { + aIndex++; + if (aIndex == a.length) return -1; // number in a is zero, b is not. + aChar = a.codeUnitAt(aIndex); + } while (aChar == _zero); + if (!_isDigit(aChar)) return -1; + } else if (bChar == _zero) { + do { + bIndex++; + if (bIndex == b.length) return 1; // number in b is zero, a is not. + bChar = b.codeUnitAt(bIndex); + } while (bChar == _zero); + if (!_isDigit(bChar)) return 1; + } + if (aChar != bChar) { + var result = _compareDigitCount(a, b, aIndex, bIndex); + if (result != 0) return result; + return (aChar - bChar).sign; + } + // Same leading digit, one had more leading zeros. + // Compare digits until reaching a difference. + while (true) { + var aIsDigit = false; + var bIsDigit = false; + aChar = 0; + bChar = 0; + if (++aIndex < a.length) { + aChar = a.codeUnitAt(aIndex); + aIsDigit = _isDigit(aChar); + } + if (++bIndex < b.length) { + bChar = b.codeUnitAt(bIndex); + bIsDigit = _isDigit(bChar); + } + if (aIsDigit) { + if (bIsDigit) { + if (aChar == bChar) continue; + // First different digit found. + break; + } + // bChar is non-digit, so a has longer number. + return 1; + } else if (bIsDigit) { + return -1; // b has longer number. + } else { + // Neither is digit, so numbers had same numerical value. + // Fall back on number of leading zeros + // (reflected by difference in indices). + return (aIndex - bIndex).sign; + } + } + // At first differing digits. + var result = _compareDigitCount(a, b, aIndex, bIndex); + if (result != 0) return result; + return (aChar - bChar).sign; +} + +/// Checks which of [a] and [b] has the longest sequence of digits. +/// +/// Starts counting from `i + 1` and `j + 1` (assumes that `a[i]` and `b[j]` are +/// both already known to be digits). +int _compareDigitCount(String a, String b, int i, int j) { + while (++i < a.length) { + var aIsDigit = _isDigit(a.codeUnitAt(i)); + if (++j == b.length) return aIsDigit ? 1 : 0; + var bIsDigit = _isDigit(b.codeUnitAt(j)); + if (aIsDigit) { + if (bIsDigit) continue; + return 1; + } else if (bIsDigit) { + return -1; + } else { + return 0; + } + } + if (++j < b.length && _isDigit(b.codeUnitAt(j))) { + return -1; + } + return 0; +} + +bool _isDigit(int charCode) => (charCode ^ _zero) <= 9; + +/// Check if the digit at [index] is continuing a non-zero number. +/// +/// If there is no non-zero digits before, then leading zeros at [index] +/// are also ignored when comparing numerically. If there is a non-zero digit +/// before, then zeros at [index] are significant. +bool _isNonZeroNumberSuffix(String string, int index) { + while (--index >= 0) { + var char = string.codeUnitAt(index); + if (char != _zero) return _isDigit(char); + } + return false; +} diff --git a/pkgs/collection/lib/src/empty_unmodifiable_set.dart b/pkgs/collection/lib/src/empty_unmodifiable_set.dart new file mode 100644 index 00000000..74fd39a1 --- /dev/null +++ b/pkgs/collection/lib/src/empty_unmodifiable_set.dart @@ -0,0 +1,46 @@ +// Copyright (c) 2016, 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 'dart:collection'; + +import 'unmodifiable_wrappers.dart'; +import 'wrappers.dart'; + +/// An unmodifiable, empty set which can be constant. +class EmptyUnmodifiableSet extends IterableBase + with UnmodifiableSetMixin + implements UnmodifiableSetView { + const EmptyUnmodifiableSet(); + + @override + Iterator get iterator => Iterable.empty().iterator; + @override + int get length => 0; + @override + EmptyUnmodifiableSet cast() => EmptyUnmodifiableSet(); + @override + bool contains(Object? element) => false; + @override + bool containsAll(Iterable other) => other.isEmpty; + @override + Iterable followedBy(Iterable other) => DelegatingIterable(other); + @override + E? lookup(Object? element) => null; + @Deprecated('Use cast instead') + @override + EmptyUnmodifiableSet retype() => EmptyUnmodifiableSet(); + @override + E singleWhere(bool Function(E) test, {E Function()? orElse}) => + orElse != null ? orElse() : throw StateError('No element'); + @override + Iterable whereType() => Iterable.empty(); + @override + Set toSet() => {}; + @override + Set union(Set other) => Set.of(other); + @override + Set intersection(Set other) => {}; + @override + Set difference(Set other) => {}; +} diff --git a/pkgs/collection/lib/src/equality.dart b/pkgs/collection/lib/src/equality.dart new file mode 100644 index 00000000..0e1df23d --- /dev/null +++ b/pkgs/collection/lib/src/equality.dart @@ -0,0 +1,491 @@ +// Copyright (c) 2013, 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 'dart:collection'; + +import 'comparators.dart'; + +const int _hashMask = 0x7fffffff; + +/// A generic equality relation on objects. +abstract class Equality { + const factory Equality() = DefaultEquality; + + /// Compare two elements for being equal. + /// + /// This should be a proper equality relation. + bool equals(E e1, E e2); + + /// Get a hashcode of an element. + /// + /// The hashcode should be compatible with [equals], so that if + /// `equals(a, b)` then `hash(a) == hash(b)`. + int hash(E e); + + /// Test whether an object is a valid argument to [equals] and [hash]. + /// + /// Some implementations may be restricted to only work on specific types + /// of objects. + bool isValidKey(Object? o); +} + +/// Equality of objects based on derived values. +/// +/// For example, given the class: +/// ```dart +/// abstract class Employee { +/// int get employmentId; +/// } +/// ``` +/// +/// The following [Equality] considers employees with the same IDs to be equal: +/// ```dart +/// EqualityBy((Employee e) => e.employmentId); +/// ``` +/// +/// It's also possible to pass an additional equality instance that should be +/// used to compare the value itself. +class EqualityBy implements Equality { + final F Function(E) _comparisonKey; + + final Equality _inner; + + EqualityBy(F Function(E) comparisonKey, + [Equality inner = const DefaultEquality()]) + : _comparisonKey = comparisonKey, + _inner = inner; + + @override + bool equals(E e1, E e2) => + _inner.equals(_comparisonKey(e1), _comparisonKey(e2)); + + @override + int hash(E e) => _inner.hash(_comparisonKey(e)); + + @override + bool isValidKey(Object? o) { + if (o is E) { + final value = _comparisonKey(o); + return _inner.isValidKey(value); + } + return false; + } +} + +/// Equality of objects that compares only the natural equality of the objects. +/// +/// This equality uses the objects' own [Object.==] and [Object.hashCode] for +/// the equality. +/// +/// Note that [equals] and [hash] take `Object`s rather than `E`s. This allows +/// `E` to be inferred as `Null` in const contexts where `E` wouldn't be a +/// compile-time constant, while still allowing the class to be used at runtime. +class DefaultEquality implements Equality { + const DefaultEquality(); + @override + bool equals(Object? e1, Object? e2) => e1 == e2; + @override + int hash(Object? e) => e.hashCode; + @override + bool isValidKey(Object? o) => true; +} + +/// Equality of objects that compares only the identity of the objects. +class IdentityEquality implements Equality { + const IdentityEquality(); + @override + bool equals(E e1, E e2) => identical(e1, e2); + @override + int hash(E e) => identityHashCode(e); + @override + bool isValidKey(Object? o) => true; +} + +/// Equality on iterables. +/// +/// Two iterables are equal if they have the same elements in the same order. +/// +/// The [equals] and [hash] methods accepts `null` values, +/// even if the [isValidKey] returns `false` for `null`. +/// The [hash] of `null` is `null.hashCode`. +class IterableEquality implements Equality> { + final Equality _elementEquality; + const IterableEquality( + [Equality elementEquality = const DefaultEquality()]) + : _elementEquality = elementEquality; + + @override + bool equals(Iterable? elements1, Iterable? elements2) { + if (identical(elements1, elements2)) return true; + if (elements1 == null || elements2 == null) return false; + var it1 = elements1.iterator; + var it2 = elements2.iterator; + while (true) { + var hasNext = it1.moveNext(); + if (hasNext != it2.moveNext()) return false; + if (!hasNext) return true; + if (!_elementEquality.equals(it1.current, it2.current)) return false; + } + } + + @override + int hash(Iterable? elements) { + if (elements == null) return null.hashCode; + // Jenkins's one-at-a-time hash function. + var hash = 0; + for (var element in elements) { + var c = _elementEquality.hash(element); + hash = (hash + c) & _hashMask; + hash = (hash + (hash << 10)) & _hashMask; + hash ^= hash >> 6; + } + hash = (hash + (hash << 3)) & _hashMask; + hash ^= hash >> 11; + hash = (hash + (hash << 15)) & _hashMask; + return hash; + } + + @override + bool isValidKey(Object? o) => o is Iterable; +} + +/// Equality on lists. +/// +/// Two lists are equal if they have the same length and their elements +/// at each index are equal. +/// +/// This is effectively the same as [IterableEquality] except that it +/// accesses elements by index instead of through iteration. +/// +/// The [equals] and [hash] methods accepts `null` values, +/// even if the [isValidKey] returns `false` for `null`. +/// The [hash] of `null` is `null.hashCode`. +class ListEquality implements Equality> { + final Equality _elementEquality; + const ListEquality( + [Equality elementEquality = const DefaultEquality()]) + : _elementEquality = elementEquality; + + @override + bool equals(List? list1, List? list2) { + if (identical(list1, list2)) return true; + if (list1 == null || list2 == null) return false; + var length = list1.length; + if (length != list2.length) return false; + for (var i = 0; i < length; i++) { + if (!_elementEquality.equals(list1[i], list2[i])) return false; + } + return true; + } + + @override + int hash(List? list) { + if (list == null) return null.hashCode; + // Jenkins's one-at-a-time hash function. + // This code is almost identical to the one in IterableEquality, except + // that it uses indexing instead of iterating to get the elements. + var hash = 0; + for (var i = 0; i < list.length; i++) { + var c = _elementEquality.hash(list[i]); + hash = (hash + c) & _hashMask; + hash = (hash + (hash << 10)) & _hashMask; + hash ^= hash >> 6; + } + hash = (hash + (hash << 3)) & _hashMask; + hash ^= hash >> 11; + hash = (hash + (hash << 15)) & _hashMask; + return hash; + } + + @override + bool isValidKey(Object? o) => o is List; +} + +abstract class _UnorderedEquality> + implements Equality { + final Equality _elementEquality; + + const _UnorderedEquality(this._elementEquality); + + @override + bool equals(T? elements1, T? elements2) { + if (identical(elements1, elements2)) return true; + if (elements1 == null || elements2 == null) return false; + var counts = HashMap( + equals: _elementEquality.equals, + hashCode: _elementEquality.hash, + isValidKey: _elementEquality.isValidKey); + var length = 0; + for (var e in elements1) { + var count = counts[e] ?? 0; + counts[e] = count + 1; + length++; + } + for (var e in elements2) { + var count = counts[e]; + if (count == null || count == 0) return false; + counts[e] = count - 1; + length--; + } + return length == 0; + } + + @override + int hash(T? elements) { + if (elements == null) return null.hashCode; + var hash = 0; + for (E element in elements) { + var c = _elementEquality.hash(element); + hash = (hash + c) & _hashMask; + } + hash = (hash + (hash << 3)) & _hashMask; + hash ^= hash >> 11; + hash = (hash + (hash << 15)) & _hashMask; + return hash; + } +} + +/// Equality of the elements of two iterables without considering order. +/// +/// Two iterables are considered equal if they have the same number of elements, +/// and the elements of one set can be paired with the elements +/// of the other iterable, so that each pair are equal. +class UnorderedIterableEquality extends _UnorderedEquality> { + const UnorderedIterableEquality( + [super.elementEquality = const DefaultEquality()]); + + @override + bool isValidKey(Object? o) => o is Iterable; +} + +/// Equality of sets. +/// +/// Two sets are considered equal if they have the same number of elements, +/// and the elements of one set can be paired with the elements +/// of the other set, so that each pair are equal. +/// +/// This equality behaves the same as [UnorderedIterableEquality] except that +/// it expects sets instead of iterables as arguments. +/// +/// The [equals] and [hash] methods accepts `null` values, +/// even if the [isValidKey] returns `false` for `null`. +/// The [hash] of `null` is `null.hashCode`. +class SetEquality extends _UnorderedEquality> { + const SetEquality([super.elementEquality = const DefaultEquality()]); + + @override + bool isValidKey(Object? o) => o is Set; +} + +/// Internal class used by [MapEquality]. +/// +/// The class represents a map entry as a single object, +/// using a combined hashCode and equality of the key and value. +class _MapEntry { + final MapEquality equality; + final Object? key; + final Object? value; + _MapEntry(this.equality, this.key, this.value); + + @override + int get hashCode => + (3 * equality._keyEquality.hash(key) + + 7 * equality._valueEquality.hash(value)) & + _hashMask; + + @override + bool operator ==(Object other) => + other is _MapEntry && + equality._keyEquality.equals(key, other.key) && + equality._valueEquality.equals(value, other.value); +} + +/// Equality on maps. +/// +/// Two maps are equal if they have the same number of entries, and if the +/// entries of the two maps are pairwise equal on both key and value. +/// +/// The [equals] and [hash] methods accepts `null` values, +/// even if the [isValidKey] returns `false` for `null`. +/// The [hash] of `null` is `null.hashCode`. +class MapEquality implements Equality> { + final Equality _keyEquality; + final Equality _valueEquality; + const MapEquality( + {Equality keys = const DefaultEquality(), + Equality values = const DefaultEquality()}) + : _keyEquality = keys, + _valueEquality = values; + + @override + bool equals(Map? map1, Map? map2) { + if (identical(map1, map2)) return true; + if (map1 == null || map2 == null) return false; + var length = map1.length; + if (length != map2.length) return false; + Map<_MapEntry, int> equalElementCounts = HashMap(); + for (var key in map1.keys) { + var entry = _MapEntry(this, key, map1[key]); + var count = equalElementCounts[entry] ?? 0; + equalElementCounts[entry] = count + 1; + } + for (var key in map2.keys) { + var entry = _MapEntry(this, key, map2[key]); + var count = equalElementCounts[entry]; + if (count == null || count == 0) return false; + equalElementCounts[entry] = count - 1; + } + return true; + } + + @override + int hash(Map? map) { + if (map == null) return null.hashCode; + var hash = 0; + for (var key in map.keys) { + var keyHash = _keyEquality.hash(key); + var valueHash = _valueEquality.hash(map[key] as V); + hash = (hash + 3 * keyHash + 7 * valueHash) & _hashMask; + } + hash = (hash + (hash << 3)) & _hashMask; + hash ^= hash >> 11; + hash = (hash + (hash << 15)) & _hashMask; + return hash; + } + + @override + bool isValidKey(Object? o) => o is Map; +} + +/// Combines several equalities into a single equality. +/// +/// Tries each equality in order, using [Equality.isValidKey], and returns +/// the result of the first equality that applies to the argument or arguments. +/// +/// For `equals`, the first equality that matches the first argument is used, +/// and if the second argument of `equals` is not valid for that equality, +/// it returns false. +/// +/// Because the equalities are tried in order, they should generally work on +/// disjoint types. Otherwise the multi-equality may give inconsistent results +/// for `equals(e1, e2)` and `equals(e2, e1)`. This can happen if one equality +/// considers only `e1` a valid key, and not `e2`, but an equality which is +/// checked later, allows both. +class MultiEquality implements Equality { + final Iterable> _equalities; + + const MultiEquality(Iterable> equalities) + : _equalities = equalities; + + @override + bool equals(E e1, E e2) { + for (var eq in _equalities) { + if (eq.isValidKey(e1)) return eq.isValidKey(e2) && eq.equals(e1, e2); + } + return false; + } + + @override + int hash(E e) { + for (var eq in _equalities) { + if (eq.isValidKey(e)) return eq.hash(e); + } + return 0; + } + + @override + bool isValidKey(Object? o) { + for (var eq in _equalities) { + if (eq.isValidKey(o)) return true; + } + return false; + } +} + +/// Deep equality on collections. +/// +/// Recognizes lists, sets, iterables and maps and compares their elements using +/// deep equality as well. +/// +/// Non-iterable/map objects are compared using a configurable base equality. +/// +/// Works in one of two modes: ordered or unordered. +/// +/// In ordered mode, lists and iterables are required to have equal elements +/// in the same order. In unordered mode, the order of elements in iterables +/// and lists are not important. +/// +/// A list is only equal to another list, likewise for sets and maps. All other +/// iterables are compared as iterables only. +class DeepCollectionEquality implements Equality { + final Equality _base; + final bool _unordered; + const DeepCollectionEquality([Equality base = const DefaultEquality()]) + : _base = base, + _unordered = false; + + /// Creates a deep equality on collections where the order of lists and + /// iterables are not considered important. That is, lists and iterables are + /// treated as unordered iterables. + const DeepCollectionEquality.unordered( + [Equality base = const DefaultEquality()]) + : _base = base, + _unordered = true; + + @override + bool equals(Object? e1, Object? e2) { + if (e1 is Set) { + return e2 is Set && SetEquality(this).equals(e1, e2); + } + if (e1 is Map) { + return e2 is Map && MapEquality(keys: this, values: this).equals(e1, e2); + } + if (!_unordered) { + if (e1 is List) { + return e2 is List && ListEquality(this).equals(e1, e2); + } + if (e1 is Iterable) { + return e2 is Iterable && IterableEquality(this).equals(e1, e2); + } + } else if (e1 is Iterable) { + if (e1 is List != e2 is List) return false; + return e2 is Iterable && UnorderedIterableEquality(this).equals(e1, e2); + } + return _base.equals(e1, e2); + } + + @override + int hash(Object? o) { + if (o is Set) return SetEquality(this).hash(o); + if (o is Map) return MapEquality(keys: this, values: this).hash(o); + if (!_unordered) { + if (o is List) return ListEquality(this).hash(o); + if (o is Iterable) return IterableEquality(this).hash(o); + } else if (o is Iterable) { + return UnorderedIterableEquality(this).hash(o); + } + return _base.hash(o); + } + + @override + bool isValidKey(Object? o) => + o is Iterable || o is Map || _base.isValidKey(o); +} + +/// String equality that's insensitive to differences in ASCII case. +/// +/// Non-ASCII characters are compared as-is, with no conversion. +class CaseInsensitiveEquality implements Equality { + const CaseInsensitiveEquality(); + + @override + bool equals(String string1, String string2) => + equalsIgnoreAsciiCase(string1, string2); + + @override + int hash(String string) => hashIgnoreAsciiCase(string); + + @override + bool isValidKey(Object? object) => object is String; +} diff --git a/pkgs/collection/lib/src/equality_map.dart b/pkgs/collection/lib/src/equality_map.dart new file mode 100644 index 00000000..542977f6 --- /dev/null +++ b/pkgs/collection/lib/src/equality_map.dart @@ -0,0 +1,31 @@ +// Copyright (c) 2016, 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 'dart:collection'; + +import 'equality.dart'; +import 'wrappers.dart'; + +/// A [Map] whose key equality is determined by an [Equality] object. +class EqualityMap extends DelegatingMap { + /// Creates a map with equality based on [equality]. + EqualityMap(Equality equality) + : super(LinkedHashMap( + equals: equality.equals, + hashCode: equality.hash, + isValidKey: equality.isValidKey)); + + /// Creates a map with equality based on [equality] that contains all + /// key-value pairs of [other]. + /// + /// If [other] has multiple keys that are equivalent according to [equality], + /// the last one reached during iteration takes precedence. + EqualityMap.from(Equality equality, Map other) + : super(LinkedHashMap( + equals: equality.equals, + hashCode: equality.hash, + isValidKey: equality.isValidKey)) { + addAll(other); + } +} diff --git a/pkgs/collection/lib/src/equality_set.dart b/pkgs/collection/lib/src/equality_set.dart new file mode 100644 index 00000000..8edbba5b --- /dev/null +++ b/pkgs/collection/lib/src/equality_set.dart @@ -0,0 +1,31 @@ +// Copyright (c) 2016, 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 'dart:collection'; + +import 'equality.dart'; +import 'wrappers.dart'; + +/// A [Set] whose key equality is determined by an [Equality] object. +class EqualitySet extends DelegatingSet { + /// Creates a set with equality based on [equality]. + EqualitySet(Equality equality) + : super(LinkedHashSet( + equals: equality.equals, + hashCode: equality.hash, + isValidKey: equality.isValidKey)); + + /// Creates a set with equality based on [equality] that contains all + /// elements in [other]. + /// + /// If [other] has multiple values that are equivalent according to + /// [equality], the first one reached during iteration takes precedence. + EqualitySet.from(Equality equality, Iterable other) + : super(LinkedHashSet( + equals: equality.equals, + hashCode: equality.hash, + isValidKey: equality.isValidKey)) { + addAll(other); + } +} diff --git a/pkgs/collection/lib/src/functions.dart b/pkgs/collection/lib/src/functions.dart new file mode 100644 index 00000000..db865741 --- /dev/null +++ b/pkgs/collection/lib/src/functions.dart @@ -0,0 +1,213 @@ +// Copyright (c) 2016, 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 'dart:collection'; +import 'dart:math' as math; + +import 'utils.dart'; + +/// Creates a new map from [map] with new keys and values. +/// +/// The return values of [key] are used as the keys and the return values of +/// [value] are used as the values for the new map. +@Deprecated('Use Map.map or a for loop in a Map literal.') +Map mapMap(Map map, + {K2 Function(K1, V1)? key, V2 Function(K1, V1)? value}) { + var keyFn = key ?? (mapKey, _) => mapKey as K2; + var valueFn = value ?? (_, mapValue) => mapValue as V2; + + var result = {}; + map.forEach((mapKey, mapValue) { + result[keyFn(mapKey, mapValue)] = valueFn(mapKey, mapValue); + }); + return result; +} + +/// Returns a new map with all key/value pairs in both [map1] and [map2]. +/// +/// If there are keys that occur in both maps, the [value] function is used to +/// select the value that goes into the resulting map based on the two original +/// values. If [value] is omitted, the value from [map2] is used. +Map mergeMaps(Map map1, Map map2, + {V Function(V, V)? value}) { + var result = Map.of(map1); + if (value == null) return result..addAll(map2); + + map2.forEach((key, mapValue) { + result[key] = + result.containsKey(key) ? value(result[key] as V, mapValue) : mapValue; + }); + return result; +} + +/// Associates the elements in [values] by the value returned by [key]. +/// +/// Returns a map from keys computed by [key] to the last value for which [key] +/// returns that key. +Map lastBy(Iterable values, T Function(S) key) => + {for (var element in values) key(element): element}; + +/// Groups the elements in [values] by the value returned by [key]. +/// +/// Returns a map from keys computed by [key] to a list of all values for which +/// [key] returns that key. The values appear in the list in the same relative +/// order as in [values]. +Map> groupBy(Iterable values, T Function(S) key) { + var map = >{}; + for (var element in values) { + (map[key(element)] ??= []).add(element); + } + return map; +} + +/// Returns the element of [values] for which [orderBy] returns the minimum +/// value. +/// +/// The values returned by [orderBy] are compared using the [compare] function. +/// If [compare] is omitted, values must implement [Comparable]`` and they +/// are compared using their [Comparable.compareTo]. +/// +/// Returns `null` if [values] is empty. +S? minBy(Iterable values, T Function(S) orderBy, + {int Function(T, T)? compare}) { + compare ??= defaultCompare; + + S? minValue; + T? minOrderBy; + for (var element in values) { + var elementOrderBy = orderBy(element); + if (minOrderBy == null || compare(elementOrderBy, minOrderBy) < 0) { + minValue = element; + minOrderBy = elementOrderBy; + } + } + return minValue; +} + +/// Returns the element of [values] for which [orderBy] returns the maximum +/// value. +/// +/// The values returned by [orderBy] are compared using the [compare] function. +/// If [compare] is omitted, values must implement [Comparable]`` and they +/// are compared using their [Comparable.compareTo]. +/// +/// Returns `null` if [values] is empty. +S? maxBy(Iterable values, T Function(S) orderBy, + {int Function(T, T)? compare}) { + compare ??= defaultCompare; + + S? maxValue; + T? maxOrderBy; + for (var element in values) { + var elementOrderBy = orderBy(element); + if (maxOrderBy == null || compare(elementOrderBy, maxOrderBy) > 0) { + maxValue = element; + maxOrderBy = elementOrderBy; + } + } + return maxValue; +} + +/// Returns the [transitive closure][] of [graph]. +/// +/// [transitive closure]: https://en.wikipedia.org/wiki/Transitive_closure +/// +/// Interprets [graph] as a directed graph with a vertex for each key and edges +/// from each key to the values that the key maps to. +/// +/// Assumes that every vertex in the graph has a key to represent it, even if +/// that vertex has no outgoing edges. This isn't checked, but if it's not +/// satisfied, the function may crash or provide unexpected output. For example, +/// `{"a": ["b"]}` is not valid, but `{"a": ["b"], "b": []}` is. +@Deprecated('This method will be removed. Consider using package:graphs.') +Map> transitiveClosure(Map> graph) { + // This uses [Warshall's algorithm][], modified not to add a vertex from each + // node to itself. + // + // [Warshall's algorithm]: https://en.wikipedia.org/wiki/Floyd%E2%80%93Warshall_algorithm#Applications_and_generalizations. + var result = >{}; + graph.forEach((vertex, edges) { + result[vertex] = Set.from(edges); + }); + + // Lists are faster to iterate than maps, so we create a list since we're + // iterating repeatedly. + var keys = graph.keys.toList(); + for (var vertex1 in keys) { + for (var vertex2 in keys) { + for (var vertex3 in keys) { + if (result[vertex2]!.contains(vertex1) && + result[vertex1]!.contains(vertex3)) { + result[vertex2]!.add(vertex3); + } + } + } + } + + return result; +} + +/// Returns the [strongly connected components][] of [graph], in topological +/// order. +/// +/// [strongly connected components]: https://en.wikipedia.org/wiki/Strongly_connected_component +/// +/// Interprets [graph] as a directed graph with a vertex for each key and edges +/// from each key to the values that the key maps to. +/// +/// Assumes that every vertex in the graph has a key to represent it, even if +/// that vertex has no outgoing edges. This isn't checked, but if it's not +/// satisfied, the function may crash or provide unexpected output. For example, +/// `{"a": ["b"]}` is not valid, but `{"a": ["b"], "b": []}` is. +List> stronglyConnectedComponents(Map> graph) { + // This uses [Tarjan's algorithm][]. + // + // [Tarjan's algorithm]: https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm + var index = 0; + var stack = []; + var result = >[]; + + // The order of these doesn't matter, so we use un-linked implementations to + // avoid unnecessary overhead. + var indices = HashMap(); + var lowLinks = HashMap(); + var onStack = HashSet(); + + void strongConnect(T vertex) { + indices[vertex] = index; + lowLinks[vertex] = index; + index++; + + stack.add(vertex); + onStack.add(vertex); + + for (var successor in graph[vertex]!) { + if (!indices.containsKey(successor)) { + strongConnect(successor); + lowLinks[vertex] = math.min(lowLinks[vertex]!, lowLinks[successor]!); + } else if (onStack.contains(successor)) { + lowLinks[vertex] = math.min(lowLinks[vertex]!, lowLinks[successor]!); + } + } + + if (lowLinks[vertex] == indices[vertex]) { + var component = {}; + T? neighbor; + do { + neighbor = stack.removeLast(); + onStack.remove(neighbor); + component.add(neighbor as T); + } while (neighbor != vertex); + result.add(component); + } + } + + for (var vertex in graph.keys) { + if (!indices.containsKey(vertex)) strongConnect(vertex); + } + + // Tarjan's algorithm produces a reverse-topological sort, so we reverse it to + // get a normal topological sort. + return result.reversed.toList(); +} diff --git a/pkgs/collection/lib/src/iterable_extensions.dart b/pkgs/collection/lib/src/iterable_extensions.dart new file mode 100644 index 00000000..e2050062 --- /dev/null +++ b/pkgs/collection/lib/src/iterable_extensions.dart @@ -0,0 +1,1013 @@ +// Copyright (c) 2020, 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 'dart:math' show Random; + +import 'algorithms.dart'; +import 'functions.dart' as functions; +import 'utils.dart'; + +/// Extensions that apply to all iterables. +/// +/// These extensions provide direct access to some of the +/// algorithms expose by this package, +/// as well as some generally useful convenience methods. +/// +/// More specialized extension methods that only apply to +/// iterables with specific element types include those of +/// [IterableComparableExtension] and [IterableNullableExtension]. +extension IterableExtension on Iterable { + /// Selects [count] elements at random from this iterable. + /// + /// The returned list contains [count] different elements of the iterable. + /// If the iterable contains fewer that [count] elements, + /// the result will contain all of them, but will be shorter than [count]. + /// If the same value occurs more than once in the iterable, + /// it can also occur more than once in the chosen elements. + /// + /// Each element of the iterable has the same chance of being chosen. + /// The chosen elements are not in any specific order. + List sample(int count, [Random? random]) { + RangeError.checkNotNegative(count, 'count'); + var iterator = this.iterator; + var chosen = []; + random ??= Random(); + while (chosen.length < count) { + if (iterator.moveNext()) { + var nextElement = iterator.current; + var position = random.nextInt(chosen.length + 1); + if (position == chosen.length) { + chosen.add(nextElement); + } else { + chosen.add(chosen[position]); + chosen[position] = nextElement; + } + } else { + return chosen; + } + } + var index = count; + while (iterator.moveNext()) { + index++; + var position = random.nextInt(index); + if (position < count) chosen[position] = iterator.current; + } + return chosen; + } + + /// The elements that do not satisfy [test]. + Iterable whereNot(bool Function(T element) test) => + where((element) => !test(element)); + + /// Creates a sorted list of the elements of the iterable. + /// + /// The elements are ordered by the [compare] [Comparator]. + List sorted(Comparator compare) => [...this]..sort(compare); + + /// Creates a shuffled list of the elements of the iterable. + List shuffled([Random? random]) => [...this]..shuffle(random); + + /// Creates a sorted list of the elements of the iterable. + /// + /// The elements are ordered by the natural ordering of the + /// property [keyOf] of the element. + List sortedBy>(K Function(T element) keyOf) { + var elements = [...this]; + mergeSortBy(elements, keyOf, compareComparable); + return elements; + } + + /// Creates a sorted list of the elements of the iterable. + /// + /// The elements are ordered by the [compare] [Comparator] of the + /// property [keyOf] of the element. + List sortedByCompare( + K Function(T element) keyOf, Comparator compare) { + var elements = [...this]; + mergeSortBy(elements, keyOf, compare); + return elements; + } + + /// Whether the elements are sorted by the [compare] ordering. + /// + /// Compares pairs of elements using `compare` to check that + /// the elements of this iterable to check + /// that earlier elements always compare + /// smaller than or equal to later elements. + /// + /// An single-element or empty iterable is trivially in sorted order. + bool isSorted(Comparator compare) { + var iterator = this.iterator; + if (!iterator.moveNext()) return true; + var previousElement = iterator.current; + while (iterator.moveNext()) { + var element = iterator.current; + if (compare(previousElement, element) > 0) return false; + previousElement = element; + } + return true; + } + + /// Whether the elements are sorted by their [keyOf] property. + /// + /// Applies [keyOf] to each element in iteration order, + /// then checks whether the results are in non-decreasing [Comparable] order. + bool isSortedBy>(K Function(T element) keyOf) { + var iterator = this.iterator; + if (!iterator.moveNext()) return true; + var previousKey = keyOf(iterator.current); + while (iterator.moveNext()) { + var key = keyOf(iterator.current); + if (previousKey.compareTo(key) > 0) return false; + previousKey = key; + } + return true; + } + + /// Whether the elements are [compare]-sorted by their [keyOf] property. + /// + /// Applies [keyOf] to each element in iteration order, + /// then checks whether the results are in non-decreasing order + /// using the [compare] [Comparator].. + bool isSortedByCompare( + K Function(T element) keyOf, Comparator compare) { + var iterator = this.iterator; + if (!iterator.moveNext()) return true; + var previousKey = keyOf(iterator.current); + while (iterator.moveNext()) { + var key = keyOf(iterator.current); + if (compare(previousKey, key) > 0) return false; + previousKey = key; + } + return true; + } + + /// Takes an action for each element. + /// + /// Calls [action] for each element along with the index in the + /// iteration order. + void forEachIndexed(void Function(int index, T element) action) { + var index = 0; + for (var element in this) { + action(index++, element); + } + } + + /// Takes an action for each element as long as desired. + /// + /// Calls [action] for each element. + /// Stops iteration if [action] returns `false`. + void forEachWhile(bool Function(T element) action) { + for (var element in this) { + if (!action(element)) break; + } + } + + /// Takes an action for each element and index as long as desired. + /// + /// Calls [action] for each element along with the index in the + /// iteration order. + /// Stops iteration if [action] returns `false`. + void forEachIndexedWhile(bool Function(int index, T element) action) { + var index = 0; + for (var element in this) { + if (!action(index++, element)) break; + } + } + + /// Maps each element and its index to a new value. + Iterable mapIndexed(R Function(int index, T element) convert) sync* { + var index = 0; + for (var element in this) { + yield convert(index++, element); + } + } + + /// The elements whose value and index satisfies [test]. + Iterable whereIndexed(bool Function(int index, T element) test) sync* { + var index = 0; + for (var element in this) { + if (test(index++, element)) yield element; + } + } + + /// The elements whose value and index do not satisfy [test]. + Iterable whereNotIndexed(bool Function(int index, T element) test) sync* { + var index = 0; + for (var element in this) { + if (!test(index++, element)) yield element; + } + } + + /// Expands each element and index to a number of elements in a new iterable. + Iterable expandIndexed( + Iterable Function(int index, T element) expand) sync* { + var index = 0; + for (var element in this) { + yield* expand(index++, element); + } + } + + /// Combine the elements with each other and the current index. + /// + /// Calls [combine] for each element except the first. + /// The call passes the index of the current element, the result of the + /// previous call, or the first element for the first call, and + /// the current element. + /// + /// Returns the result of the last call, or the first element if + /// there is only one element. + /// There must be at least one element. + T reduceIndexed(T Function(int index, T previous, T element) combine) { + var iterator = this.iterator; + if (!iterator.moveNext()) { + throw StateError('no elements'); + } + var index = 1; + var result = iterator.current; + while (iterator.moveNext()) { + result = combine(index++, result, iterator.current); + } + return result; + } + + /// Combine the elements with a value and the current index. + /// + /// Calls [combine] for each element with the current index, + /// the result of the previous call, or [initialValue] for the first element, + /// and the current element. + /// + /// Returns the result of the last call to [combine], + /// or [initialValue] if there are no elements. + R foldIndexed( + R initialValue, R Function(int index, R previous, T element) combine) { + var result = initialValue; + var index = 0; + for (var element in this) { + result = combine(index++, result, element); + } + return result; + } + + /// The first element satisfying [test], or `null` if there are none. + T? firstWhereOrNull(bool Function(T element) test) { + for (var element in this) { + if (test(element)) return element; + } + return null; + } + + /// The first element whose value and index satisfies [test]. + /// + /// Returns `null` if there are no element and index satisfying [test]. + T? firstWhereIndexedOrNull(bool Function(int index, T element) test) { + var index = 0; + for (var element in this) { + if (test(index++, element)) return element; + } + return null; + } + + /// The first element, or `null` if the iterable is empty. + T? get firstOrNull { + var iterator = this.iterator; + if (iterator.moveNext()) return iterator.current; + return null; + } + + /// The last element satisfying [test], or `null` if there are none. + T? lastWhereOrNull(bool Function(T element) test) { + T? result; + for (var element in this) { + if (test(element)) result = element; + } + return result; + } + + /// The last element whose index and value satisfies [test]. + /// + /// Returns `null` if no element and index satisfies [test]. + T? lastWhereIndexedOrNull(bool Function(int index, T element) test) { + T? result; + var index = 0; + for (var element in this) { + if (test(index++, element)) result = element; + } + return result; + } + + /// The last element, or `null` if the iterable is empty. + T? get lastOrNull { + if (isEmpty) return null; + return last; + } + + /// The single element satisfying [test]. + /// + /// Returns `null` if there are either no elements + /// or more than one element satisfying [test]. + /// + /// **Notice**: This behavior differs from [Iterable.singleWhere] + /// which always throws if there are more than one match, + /// and only calls the `orElse` function on zero matches. + T? singleWhereOrNull(bool Function(T element) test) { + T? result; + var found = false; + for (var element in this) { + if (test(element)) { + if (!found) { + result = element; + found = true; + } else { + return null; + } + } + } + return result; + } + + /// The single element satisfying [test]. + /// + /// Returns `null` if there are either none + /// or more than one element and index satisfying [test]. + T? singleWhereIndexedOrNull(bool Function(int index, T element) test) { + T? result; + var found = false; + var index = 0; + for (var element in this) { + if (test(index++, element)) { + if (!found) { + result = element; + found = true; + } else { + return null; + } + } + } + return result; + } + + /// The single element of the iterable, or `null`. + /// + /// The value is `null` if the iterable is empty + /// or it contains more than one element. + T? get singleOrNull { + var iterator = this.iterator; + if (iterator.moveNext()) { + var result = iterator.current; + if (!iterator.moveNext()) { + return result; + } + } + return null; + } + + /// The [index]th element, or `null` if there is no such element. + /// + /// Returns the element at position [index] of this iterable, + /// just like [elementAt], if this iterable has such an element. + /// If this iterable does not have enough elements to have one with the given + /// [index], the `null` value is returned, unlike [elementAt] which throws + /// instead. + /// + /// The [index] must not be negative. + T? elementAtOrNull(int index) => skip(index).firstOrNull; + + /// Associates the elements in `this` by the value returned by [key]. + /// + /// Returns a map from keys computed by [key] to the last value for which + /// [key] returns that key. + Map lastBy(K Function(T) key) => functions.lastBy(this, key); + + /// Groups elements by [keyOf] then folds the elements in each group. + /// + /// A key is found for each element using [keyOf]. + /// Then the elements with the same key are all folded using [combine]. + /// The first call to [combine] for a particular key receives `null` as + /// the previous value, the remaining ones receive the result of the previous + /// call. + /// + /// Can be used to _group_ elements into arbitrary collections. + /// For example [groupSetsBy] could be written as: + /// ```dart + /// iterable.groupFoldBy(keyOf, + /// (Set? previous, T element) => (previous ?? {})..add(element)); + /// ```` + Map groupFoldBy( + K Function(T element) keyOf, G Function(G? previous, T element) combine) { + var result = {}; + for (var element in this) { + var key = keyOf(element); + result[key] = combine(result[key], element); + } + return result; + } + + /// Groups elements into sets by [keyOf]. + Map> groupSetsBy(K Function(T element) keyOf) { + var result = >{}; + for (var element in this) { + (result[keyOf(element)] ??= {}).add(element); + } + return result; + } + + /// Groups elements into lists by [keyOf]. + Map> groupListsBy(K Function(T element) keyOf) { + var result = >{}; + for (var element in this) { + (result[keyOf(element)] ??= []).add(element); + } + return result; + } + + /// Splits the elements into chunks before some elements. + /// + /// Each element except the first is checked using [test] + /// for whether it should be the first element in a new chunk. + /// If so, the elements since the previous chunk-starting element + /// are emitted as a list. + /// Any remaining elements are emitted at the end. + /// + /// Example: + /// Example: + /// ```dart + /// var parts = [1, 0, 2, 1, 5, 7, 6, 8, 9].splitBefore(isPrime); + /// print(parts); // ([1, 0], [2, 1], [5], [7, 6, 8, 9]) + /// ``` + Iterable> splitBefore(bool Function(T element) test) => + splitBeforeIndexed((_, element) => test(element)); + + /// Splits the elements into chunks after some elements. + /// + /// Each element is checked using [test] for whether it should end a chunk. + /// If so, the elements following the previous chunk-ending element, + /// including the element that satisfied [test], + /// are emitted as a list. + /// Any remaining elements are emitted at the end, + /// whether the last element should be split after or not. + /// + /// Example: + /// ```dart + /// var parts = [1, 0, 2, 1, 5, 7, 6, 8, 9].splitAfter(isPrime); + /// print(parts); // ([1, 0, 2], [1, 5], [7], [6, 8, 9]) + /// ``` + Iterable> splitAfter(bool Function(T element) test) => + splitAfterIndexed((_, element) => test(element)); + + /// Splits the elements into chunks between some elements. + /// + /// Each pair of adjacent elements are checked using [test] + /// for whether a chunk should end between them. + /// If so, the elements since the previous chunk-splitting elements + /// are emitted as a list. + /// Any remaining elements are emitted at the end. + /// + /// Example: + /// ```dart + /// var parts = [1, 0, 2, 1, 5, 7, 6, 8, 9].splitBetween((v1, v2) => v1 > v2); + /// print(parts); // ([1], [0, 2], [1, 5, 7], [6, 8, 9]) + /// ``` + Iterable> splitBetween(bool Function(T first, T second) test) => + splitBetweenIndexed((_, first, second) => test(first, second)); + + /// Splits the elements into chunks before some elements and indices. + /// + /// Each element and index except the first is checked using [test] + /// for whether it should start a new chunk. + /// If so, the elements since the previous chunk-starting element + /// are emitted as a list. + /// Any remaining elements are emitted at the end. + /// + /// Example: + /// ```dart + /// var parts = [1, 0, 2, 1, 5, 7, 6, 8, 9] + /// .splitBeforeIndexed((i, v) => i < v); + /// print(parts); // ([1], [0, 2], [1, 5, 7], [6, 8, 9]) + /// ``` + Iterable> splitBeforeIndexed( + bool Function(int index, T element) test) sync* { + var iterator = this.iterator; + if (!iterator.moveNext()) { + return; + } + var index = 1; + var chunk = [iterator.current]; + while (iterator.moveNext()) { + var element = iterator.current; + if (test(index++, element)) { + yield chunk; + chunk = []; + } + chunk.add(element); + } + yield chunk; + } + + /// Splits the elements into chunks after some elements and indices. + /// + /// Each element and index is checked using [test] + /// for whether it should end the current chunk. + /// If so, the elements since the previous chunk-ending element, + /// including the element that satisfied [test], + /// are emitted as a list. + /// Any remaining elements are emitted at the end, whether the last + /// element should be split after or not. + /// + /// Example: + /// ```dart + /// var parts = [1, 0, 2, 1, 5, 7, 6, 8, 9] + /// .splitAfterIndexed((i, v) => i < v); + /// print(parts); // ([1, 0], [2, 1], [5, 7, 6], [8, 9]) + /// ``` + Iterable> splitAfterIndexed( + bool Function(int index, T element) test) sync* { + var index = 0; + List? chunk; + for (var element in this) { + (chunk ??= []).add(element); + if (test(index++, element)) { + yield chunk; + chunk = null; + } + } + if (chunk != null) yield chunk; + } + + /// Splits the elements into chunks between some elements and indices. + /// + /// Each pair of adjacent elements and the index of the latter are + /// checked using [test] for whether a chunk should end between them. + /// If so, the elements since the previous chunk-splitting elements + /// are emitted as a list. + /// Any remaining elements are emitted at the end. + /// + /// Example: + /// ```dart + /// var parts = [1, 0, 2, 1, 5, 7, 6, 8, 9] + /// .splitBetweenIndexed((i, v1, v2) => v1 > v2); + /// print(parts); // ([1], [0, 2], [1, 5, 7], [6, 8, 9]) + /// ``` + Iterable> splitBetweenIndexed( + bool Function(int index, T first, T second) test) sync* { + var iterator = this.iterator; + if (!iterator.moveNext()) return; + var previous = iterator.current; + var chunk = [previous]; + var index = 1; + while (iterator.moveNext()) { + var element = iterator.current; + if (test(index++, previous, element)) { + yield chunk; + chunk = []; + } + chunk.add(element); + previous = element; + } + yield chunk; + } + + /// Whether no element satisfies [test]. + /// + /// Returns true if no element satisfies [test], + /// and false if at least one does. + /// + /// Equivalent to `iterable.every((x) => !test(x))` or + /// `!iterable.any(test)`. + bool none(bool Function(T) test) { + for (var element in this) { + if (test(element)) return false; + } + return true; + } + + /// Contiguous slices of `this` with the given [length]. + /// + /// Each slice is [length] elements long, except for the last one which may be + /// shorter if `this` contains too few elements. Each slice begins after the + /// last one ends. The [length] must be greater than zero. + /// + /// For example, `{1, 2, 3, 4, 5}.slices(2)` returns `([1, 2], [3, 4], [5])`. + Iterable> slices(int length) sync* { + if (length < 1) throw RangeError.range(length, 1, null, 'length'); + + var iterator = this.iterator; + while (iterator.moveNext()) { + var slice = [iterator.current]; + for (var i = 1; i < length && iterator.moveNext(); i++) { + slice.add(iterator.current); + } + yield slice; + } + } +} + +/// Extensions that apply to iterables with a nullable element type. +extension IterableNullableExtension on Iterable { + /// The non-`null` elements of this `Iterable`. + /// + /// Returns an iterable which emits all the non-`null` elements + /// of this iterable, in their original iteration order. + /// + /// For an `Iterable`, this method is equivalent to `.whereType()`. + @Deprecated('Use .nonNulls instead.') + Iterable whereNotNull() sync* { + for (var element in this) { + if (element != null) yield element; + } + } +} + +/// Extensions that apply to iterables of numbers. +/// +/// Specialized version of some extensions of [IterableComparableExtension] +/// since doubles require special handling of [double.nan]. +extension IterableNumberExtension on Iterable { + /// A minimal element of the iterable, or `null` it the iterable is empty. + /// + /// If any element is [NaN](double.nan), the result is NaN. + num? get minOrNull { + var iterator = this.iterator; + if (iterator.moveNext()) { + var value = iterator.current; + if (value.isNaN) { + return value; + } + while (iterator.moveNext()) { + var newValue = iterator.current; + if (newValue.isNaN) { + return newValue; + } + if (newValue < value) { + value = newValue; + } + } + return value; + } + return null; + } + + /// A minimal element of the iterable. + /// + /// If any element is [NaN](double.nan), the result is NaN. + /// + /// The iterable must not be empty. + num get min => minOrNull ?? (throw StateError('No element')); + + /// A maximal element of the iterable, or `null` if the iterable is empty. + /// + /// If any element is [NaN](double.nan), the result is NaN. + num? get maxOrNull { + var iterator = this.iterator; + if (iterator.moveNext()) { + var value = iterator.current; + if (value.isNaN) { + return value; + } + while (iterator.moveNext()) { + var newValue = iterator.current; + if (newValue.isNaN) { + return newValue; + } + if (newValue > value) { + value = newValue; + } + } + return value; + } + return null; + } + + /// A maximal element of the iterable. + /// + /// If any element is [NaN](double.nan), the result is NaN. + /// + /// The iterable must not be empty. + num get max => maxOrNull ?? (throw StateError('No element')); + + /// The sum of the elements. + /// + /// The sum is zero if the iterable is empty. + num get sum { + num result = 0; + for (var value in this) { + result += value; + } + return result; + } + + /// The arithmetic mean of the elements of a non-empty iterable. + /// + /// The arithmetic mean is the sum of the elements + /// divided by the number of elements. + /// + /// The iterable must not be empty. + double get average { + var result = 0.0; + var count = 0; + for (var value in this) { + count += 1; + result += (value - result) / count; + } + if (count == 0) throw StateError('No elements'); + return result; + } +} + +/// Extension on iterables of integers. +/// +/// Specialized version of some extensions of [IterableNumberExtension] or +/// [IterableComparableExtension] since integers are only `Comparable`. +extension IterableIntegerExtension on Iterable { + /// A minimal element of the iterable, or `null` it the iterable is empty. + int? get minOrNull { + var iterator = this.iterator; + if (iterator.moveNext()) { + var value = iterator.current; + while (iterator.moveNext()) { + var newValue = iterator.current; + if (newValue < value) { + value = newValue; + } + } + return value; + } + return null; + } + + /// A minimal element of the iterable. + /// + /// The iterable must not be empty. + int get min => minOrNull ?? (throw StateError('No element')); + + /// A maximal element of the iterable, or `null` if the iterable is empty. + int? get maxOrNull { + var iterator = this.iterator; + if (iterator.moveNext()) { + var value = iterator.current; + while (iterator.moveNext()) { + var newValue = iterator.current; + if (newValue > value) { + value = newValue; + } + } + return value; + } + return null; + } + + /// A maximal element of the iterable. + /// + /// The iterable must not be empty. + int get max => maxOrNull ?? (throw StateError('No element')); + + /// The sum of the elements. + /// + /// The sum is zero if the iterable is empty. + int get sum { + var result = 0; + for (var value in this) { + result += value; + } + return result; + } + + /// The arithmetic mean of the elements of a non-empty iterable. + /// + /// The arithmetic mean is the sum of the elements + /// divided by the number of elements. + /// This method is specialized for integers, + /// and may give a different result than [IterableNumberExtension.average] + /// for the same values, because the the number algorithm + /// converts all numbers to doubles. + /// + /// The iterable must not be empty. + double get average { + var average = 0; + var remainder = 0; + var count = 0; + for (var value in this) { + // Invariant: Sum of values so far = average * count + remainder. + // (Unless overflow has occurred). + count += 1; + var delta = value - average + remainder; + average += delta ~/ count; + remainder = delta.remainder(count); + } + if (count == 0) throw StateError('No elements'); + return average + remainder / count; + } +} + +/// Extension on iterables of double. +/// +/// Specialized version of some extensions of [IterableNumberExtension] or +/// [IterableComparableExtension] since doubles are only `Comparable`. +extension IterableDoubleExtension on Iterable { + /// A minimal element of the iterable, or `null` it the iterable is empty. + /// + /// If any element is [NaN](double.nan), the result is NaN. + double? get minOrNull { + var iterator = this.iterator; + if (iterator.moveNext()) { + var value = iterator.current; + if (value.isNaN) { + return value; + } + while (iterator.moveNext()) { + var newValue = iterator.current; + if (newValue.isNaN) { + return newValue; + } + if (newValue < value) { + value = newValue; + } + } + return value; + } + return null; + } + + /// A minimal element of the iterable. + /// + /// If any element is [NaN](double.nan), the result is NaN. + /// + /// The iterable must not be empty. + double get min => minOrNull ?? (throw StateError('No element')); + + /// A maximal element of the iterable, or `null` if the iterable is empty. + /// + /// If any element is [NaN](double.nan), the result is NaN. + double? get maxOrNull { + var iterator = this.iterator; + if (iterator.moveNext()) { + var value = iterator.current; + if (value.isNaN) { + return value; + } + while (iterator.moveNext()) { + var newValue = iterator.current; + if (newValue.isNaN) { + return newValue; + } + if (newValue > value) { + value = newValue; + } + } + return value; + } + return null; + } + + /// A maximal element of the iterable. + /// + /// If any element is [NaN](double.nan), the result is NaN. + /// + /// The iterable must not be empty. + double get max => maxOrNull ?? (throw StateError('No element')); + + /// The sum of the elements. + /// + /// The sum is zero if the iterable is empty. + double get sum { + var result = 0.0; + for (var value in this) { + result += value; + } + return result; + } +} + +/// Extensions on iterables whose elements are also iterables. +extension IterableIterableExtension on Iterable> { + /// The sequential elements of each iterable in this iterable. + /// + /// Iterates the elements of this iterable. + /// For each one, which is itself an iterable, + /// all the elements of that are emitted + /// on the returned iterable, before moving on to the next element. + Iterable get flattened sync* { + for (var elements in this) { + yield* elements; + } + } + + /// The sequential elements of each iterable in this iterable. + /// + /// Iterates the elements of this iterable. + /// For each one, which is itself an iterable, + /// all the elements of that are added + /// to the returned list, before moving on to the next element. + List get flattenedToList => [ + for (final elements in this) ...elements, + ]; + + /// The unique sequential elements of each iterable in this iterable. + /// + /// Iterates the elements of this iterable. + /// For each one, which is itself an iterable, + /// all the elements of that are added + /// to the returned set, before moving on to the next element. + Set get flattenedToSet => { + for (final elements in this) ...elements, + }; +} + +/// Extensions that apply to iterables of [Comparable] elements. +/// +/// These operations can assume that the elements have a natural ordering, +/// and can therefore omit, or make it optional, for the user to provide +/// a [Comparator]. +extension IterableComparableExtension> on Iterable { + /// A minimal element of the iterable, or `null` it the iterable is empty. + T? get minOrNull { + var iterator = this.iterator; + if (iterator.moveNext()) { + var value = iterator.current; + while (iterator.moveNext()) { + var newValue = iterator.current; + if (value.compareTo(newValue) > 0) { + value = newValue; + } + } + return value; + } + return null; + } + + /// A minimal element of the iterable. + /// + /// The iterable must not be empty. + T get min => minOrNull ?? (throw StateError('No element')); + + /// A maximal element of the iterable, or `null` if the iterable is empty. + T? get maxOrNull { + var iterator = this.iterator; + if (iterator.moveNext()) { + var value = iterator.current; + while (iterator.moveNext()) { + var newValue = iterator.current; + if (value.compareTo(newValue) < 0) { + value = newValue; + } + } + return value; + } + return null; + } + + /// A maximal element of the iterable. + /// + /// The iterable must not be empty. + T get max => maxOrNull ?? (throw StateError('No element')); + + /// Creates a sorted list of the elements of the iterable. + /// + /// If the [compare] function is not supplied, the sorting uses the + /// natural [Comparable] ordering of the elements. + List sorted([Comparator? compare]) => [...this]..sort(compare); + + /// Whether the elements are sorted by the [compare] ordering. + /// + /// If [compare] is omitted, it defaults to comparing the + /// elements using their natural [Comparable] ordering. + bool isSorted([Comparator? compare]) { + if (compare != null) { + return IterableExtension(this).isSorted(compare); + } + var iterator = this.iterator; + if (!iterator.moveNext()) return true; + var previousElement = iterator.current; + while (iterator.moveNext()) { + var element = iterator.current; + if (previousElement.compareTo(element) > 0) return false; + previousElement = element; + } + return true; + } +} + +/// Extensions on comparator functions. +extension ComparatorExtension on Comparator { + /// The inverse ordering of this comparator. + Comparator get inverse => (T a, T b) => this(b, a); + + /// Makes a comparator on [R] values using this comparator. + /// + /// Compares [R] values by comparing their [keyOf] value + /// using this comparator. + Comparator compareBy(T Function(R) keyOf) => + (R a, R b) => this(keyOf(a), keyOf(b)); + + /// Combine comparators sequentially. + /// + /// Creates a comparator which orders elements the same way as + /// this comparator, except that when two elements are considered + /// equal, the [tieBreaker] comparator is used instead. + Comparator then(Comparator tieBreaker) => (T a, T b) { + var result = this(a, b); + if (result == 0) result = tieBreaker(a, b); + return result; + }; +} diff --git a/pkgs/collection/lib/src/iterable_zip.dart b/pkgs/collection/lib/src/iterable_zip.dart new file mode 100644 index 00000000..9671eaf3 --- /dev/null +++ b/pkgs/collection/lib/src/iterable_zip.dart @@ -0,0 +1,52 @@ +// Copyright (c) 2013, 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 'dart:collection'; + +/// Iterable that iterates over lists of values from other iterables. +/// +/// When [iterator] is read, an [Iterator] is created for each [Iterable] in +/// the [Iterable] passed to the constructor. +/// +/// As long as all these iterators have a next value, those next values are +/// combined into a single list, which becomes the next value of this +/// [Iterable]'s [Iterator]. As soon as any of the iterators run out, +/// the zipped iterator also stops. +class IterableZip extends IterableBase> { + final Iterable> _iterables; + + IterableZip(Iterable> iterables) : _iterables = iterables; + + /// Returns an iterator that combines values of the iterables' iterators + /// as long as they all have values. + @override + Iterator> get iterator { + var iterators = _iterables.map((x) => x.iterator).toList(growable: false); + return _IteratorZip(iterators); + } +} + +class _IteratorZip implements Iterator> { + final List> _iterators; + List? _current; + + _IteratorZip(List> iterators) : _iterators = iterators; + + @override + bool moveNext() { + if (_iterators.isEmpty) return false; + for (var i = 0; i < _iterators.length; i++) { + if (!_iterators[i].moveNext()) { + _current = null; + return false; + } + } + _current = List.generate(_iterators.length, (i) => _iterators[i].current, + growable: false); + return true; + } + + @override + List get current => _current ?? (throw StateError('No element')); +} diff --git a/pkgs/collection/lib/src/list_extensions.dart b/pkgs/collection/lib/src/list_extensions.dart new file mode 100644 index 00000000..40fa8af6 --- /dev/null +++ b/pkgs/collection/lib/src/list_extensions.dart @@ -0,0 +1,518 @@ +// Copyright (c) 2020, 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. + +// Extension methods on common collection types. +import 'dart:collection'; +import 'dart:math'; + +import 'algorithms.dart'; +import 'algorithms.dart' as algorithms; +import 'equality.dart'; +import 'utils.dart'; + +/// Various extensions on lists of arbitrary elements. +extension ListExtensions on List { + /// Returns the index of [element] in this sorted list. + /// + /// Uses binary search to find the location of [element]. + /// This takes on the order of `log(n)` comparisons. + /// The list *must* be sorted according to [compare], + /// otherwise the result is unspecified + /// + /// Returns -1 if [element] does not occur in this list. + int binarySearch(E element, int Function(E, E) compare) => + algorithms.binarySearchBy(this, identity, compare, element); + + /// Returns the index of [element] in this sorted list. + /// + /// Uses binary search to find the location of [element]. + /// This takes on the order of `log(n)` comparisons. + /// The list *must* be sorted according to [compare] on the [keyOf] of + /// elements, otherwise the result is unspecified. + /// + /// Returns -1 if [element] does not occur in this list. + /// + /// If [start] and [end] are supplied, only the list range from [start] to + /// [end] is searched, and only that range needs to be sorted. + int binarySearchByCompare( + E element, K Function(E element) keyOf, int Function(K, K) compare, + [int start = 0, int? end]) => + algorithms.binarySearchBy( + this, keyOf, compare, element, start, end); + + /// Returns the index of [element] in this sorted list. + /// + /// Uses binary search to find the location of [element]. + /// This takes on the order of `log(n)` comparisons. + /// The list *must* be sorted according to the natural ordering of + /// the [keyOf] of elements, otherwise the result is unspecified. + /// + /// Returns -1 if [element] does not occur in this list. + /// + /// If [start] and [end] are supplied, only the list range from [start] to + /// [end] is searched, and only that range needs to be sorted. + int binarySearchBy>( + E element, K Function(E element) keyOf, [int start = 0, int? end]) => + algorithms.binarySearchBy( + this, keyOf, (a, b) => a.compareTo(b), element, start, end); + + /// Returns the index where [element] should be in this sorted list. + /// + /// Uses binary search to find the location of [element]. + /// This takes on the order of `log(n)` comparisons. + /// The list *must* be sorted according to [compare], + /// otherwise the result is unspecified. + /// + /// If [element] is in the list, its index is returned, + /// otherwise returns the first position where adding [element] + /// would keep the list sorted. This may be the [length] of + /// the list if all elements of the list compare less than + /// [element]. + int lowerBound(E element, int Function(E, E) compare) => + algorithms.lowerBoundBy(this, identity, compare, element); + + /// Returns the index where [element] should be in this sorted list. + /// + /// Uses binary search to find the location of [element]. + /// This takes on the order of `log(n)` comparisons. + /// The list *must* be sorted according to [compare] of + /// the [keyOf] of the elements, otherwise the result is unspecified. + /// + /// If [element] is in the list, its index is returned, + /// otherwise returns the first position where adding [element] + /// would keep the list sorted. This may be the [length] of + /// the list if all elements of the list compare less than + /// [element]. + /// + /// If [start] and [end] are supplied, only that range is searched, + /// and only that range need to be sorted. + int lowerBoundByCompare( + E element, K Function(E) keyOf, int Function(K, K) compare, + [int start = 0, int? end]) => + algorithms.lowerBoundBy(this, keyOf, compare, element, start, end); + + /// Returns the index where [element] should be in this sorted list. + /// + /// Uses binary search to find the location of [element]. + /// This takes on the order of `log(n)` comparisons. + /// The list *must* be sorted according to the + /// natural ordering of the [keyOf] of the elements, + /// otherwise the result is unspecified. + /// + /// If [element] is in the list, its index is returned, + /// otherwise returns the first position where adding [element] + /// would keep the list sorted. This may be the [length] of + /// the list if all elements of the list compare less than + /// [element]. + /// + /// If [start] and [end] are supplied, only that range is searched, + /// and only that range need to be sorted. + int lowerBoundBy>(E element, K Function(E) keyOf, + [int start = 0, int? end]) => + algorithms.lowerBoundBy( + this, keyOf, compareComparable, element, start, end); + + /// Takes an action for each element. + /// + /// Calls [action] for each element along with the index in the + /// iteration order. + void forEachIndexed(void Function(int index, E element) action) { + for (var index = 0; index < length; index++) { + action(index, this[index]); + } + } + + /// Takes an action for each element as long as desired. + /// + /// Calls [action] for each element. + /// Stops iteration if [action] returns `false`. + void forEachWhile(bool Function(E element) action) { + for (var index = 0; index < length; index++) { + if (!action(this[index])) break; + } + } + + /// Takes an action for each element and index as long as desired. + /// + /// Calls [action] for each element along with the index in the + /// iteration order. + /// Stops iteration if [action] returns `false`. + void forEachIndexedWhile(bool Function(int index, E element) action) { + for (var index = 0; index < length; index++) { + if (!action(index, this[index])) break; + } + } + + /// Maps each element and its index to a new value. + Iterable mapIndexed(R Function(int index, E element) convert) sync* { + for (var index = 0; index < length; index++) { + yield convert(index, this[index]); + } + } + + /// The elements whose value and index satisfies [test]. + Iterable whereIndexed(bool Function(int index, E element) test) sync* { + for (var index = 0; index < length; index++) { + var element = this[index]; + if (test(index, element)) yield element; + } + } + + /// The elements whose value and index do not satisfy [test]. + Iterable whereNotIndexed(bool Function(int index, E element) test) sync* { + for (var index = 0; index < length; index++) { + var element = this[index]; + if (!test(index, element)) yield element; + } + } + + /// Expands each element and index to a number of elements in a new iterable. + /// + /// Like [Iterable.expand] except that the callback function is supplied with + /// both the index and the element. + Iterable expandIndexed( + Iterable Function(int index, E element) expand) sync* { + for (var index = 0; index < length; index++) { + yield* expand(index, this[index]); + } + } + + /// Sort a range of elements by [compare]. + void sortRange(int start, int end, int Function(E a, E b) compare) { + quickSortBy(this, identity, compare, start, end); + } + + /// Sorts elements by the [compare] of their [keyOf] property. + /// + /// Sorts elements from [start] to [end], defaulting to the entire list. + void sortByCompare( + K Function(E element) keyOf, int Function(K a, K b) compare, + [int start = 0, int? end]) { + quickSortBy(this, keyOf, compare, start, end); + } + + /// Sorts elements by the natural order of their [keyOf] property. + /// + /// Sorts elements from [start] to [end], defaulting to the entire list. + void sortBy>(K Function(E element) keyOf, + [int start = 0, int? end]) { + quickSortBy(this, keyOf, compareComparable, start, end); + } + + /// Shuffle a range of elements. + void shuffleRange(int start, int end, [Random? random]) { + RangeError.checkValidRange(start, end, length); + shuffle(this, start, end, random); + } + + /// Reverses the elements in a range of the list. + void reverseRange(int start, int end) { + RangeError.checkValidRange(start, end, length); + while (start < --end) { + var tmp = this[start]; + this[start] = this[end]; + this[end] = tmp; + start += 1; + } + } + + /// Swaps two elements of this list. + void swap(int index1, int index2) { + RangeError.checkValidIndex(index1, this, 'index1'); + RangeError.checkValidIndex(index2, this, 'index2'); + var tmp = this[index1]; + this[index1] = this[index2]; + this[index2] = tmp; + } + + /// A fixed length view of a range of this list. + /// + /// The view is backed by this list, which must not change its length while + /// the view is being used. + /// + /// The view can be used to perform specific whole-list + /// actions on a part of the list. + /// For example, to see if a list contains more than one + /// "marker" element, you can do: + /// ```dart + /// someList.slice(someList.indexOf(marker) + 1).contains(marker) + /// ``` + ListSlice slice(int start, [int? end]) { + end = RangeError.checkValidRange(start, end, length); + var self = this; + if (self is ListSlice) return self.slice(start, end); + return ListSlice(this, start, end); + } + + /// Whether [other] has the same elements as this list. + /// + /// Returns true iff [other] has the same [length] + /// as this list, and the elements of this list and [other] + /// at the same indices are equal according to [equality], + /// which defaults to using `==`. + bool equals(List other, [Equality equality = const DefaultEquality()]) { + if (length != other.length) return false; + for (var i = 0; i < length; i++) { + if (!equality.equals(this[i], other[i])) return false; + } + return true; + } + + /// The [index]th element, or `null` if there is no such element. + /// + /// Returns the element at position [index] of this list, + /// just like [elementAt], if this list has such an element. + /// If this list does not have enough elements to have one with the given + /// [index], the `null` value is returned, unlike [elementAt] which throws + /// instead. + /// + /// The [index] must not be negative. + E? elementAtOrNull(int index) => (index < length) ? this[index] : null; + + /// Contiguous [slice]s of `this` with the given [length]. + /// + /// Each slice is a view of this list [length] elements long, except for the + /// last one which may be shorter if `this` contains too few elements. Each + /// slice begins after the last one ends. + /// + /// As with [slice], these slices are backed by this list, which must not + /// change its length while the views are being used. + /// + /// For example, `[1, 2, 3, 4, 5].slices(2)` returns `[[1, 2], [3, 4], [5]]`. + Iterable> slices(int length) sync* { + if (length < 1) throw RangeError.range(length, 1, null, 'length'); + for (var i = 0; i < this.length; i += length) { + yield slice(i, min(i + length, this.length)); + } + } +} + +/// Various extensions on lists of comparable elements. +extension ListComparableExtensions> on List { + /// Returns the index of [element] in this sorted list. + /// + /// Uses binary search to find the location of [element]. + /// This takes on the order of `log(n)` comparisons. + /// The list *must* be sorted according to [compare], + /// otherwise the result is unspecified. + /// If [compare] is omitted, it uses the natural order of the elements. + /// + /// Returns -1 if [element] does not occur in this list. + int binarySearch(E element, [int Function(E, E)? compare]) => + algorithms.binarySearchBy( + this, identity, compare ?? compareComparable, element); + + /// Returns the index where [element] should be in this sorted list. + /// + /// Uses binary search to find the location of where [element] should be. + /// This takes on the order of `log(n)` comparisons. + /// The list *must* be sorted according to [compare], + /// otherwise the result is unspecified. + /// If [compare] is omitted, it uses the natural order of the elements. + /// + /// If [element] does not occur in this list, the returned index is + /// the first index where inserting [element] would keep the list + /// sorted. + int lowerBound(E element, [int Function(E, E)? compare]) => + algorithms.lowerBoundBy( + this, identity, compare ?? compareComparable, element); + + /// Sort a range of elements by [compare]. + /// + /// If [compare] is omitted, the range is sorted according to the + /// natural ordering of the elements. + void sortRange(int start, int end, [int Function(E a, E b)? compare]) { + RangeError.checkValidRange(start, end, length); + algorithms.quickSortBy( + this, identity, compare ?? compareComparable, start, end); + } +} + +/// A list view of a range of another list. +/// +/// Wraps the range of the [source] list from [start] to [end] +/// and acts like a fixed-length list view of that range. +/// The source list must not change length while a list slice is being used. +class ListSlice extends ListBase { + /// Original length of [source]. + /// + /// Used to detect modifications to [source] which may invalidate + /// the slice. + final int _initialSize; + + /// The original list backing this slice. + final List source; + + /// The start index of the slice. + final int start; + + @override + final int length; + + /// Creates a slice of [source] from [start] to [end]. + ListSlice(this.source, this.start, int end) + : length = end - start, + _initialSize = source.length { + RangeError.checkValidRange(start, end, source.length); + } + + // No argument checking, for internal use. + ListSlice._(this._initialSize, this.source, this.start, this.length); + + /// The end index of the slice. + int get end => start + length; + + @override + E operator [](int index) { + if (source.length != _initialSize) { + throw ConcurrentModificationError(source); + } + RangeError.checkValidIndex(index, this, null, length); + return source[start + index]; + } + + @override + void operator []=(int index, E value) { + if (source.length != _initialSize) { + throw ConcurrentModificationError(source); + } + RangeError.checkValidIndex(index, this, null, length); + source[start + index] = value; + } + + @override + void setRange(int start, int end, Iterable iterable, [int skipCount = 0]) { + if (source.length != _initialSize) { + throw ConcurrentModificationError(source); + } + RangeError.checkValidRange(start, end, length); + source.setRange(start + start, start + end, iterable, skipCount); + } + + /// A fixed length view of a range of this list. + /// + /// The view is backed by this list, which must not change its length while + /// the view is being used. + /// + /// The view can be used to perform specific whole-list + /// actions on a part of the list. + /// For example, to see if a list contains more than one + /// "marker" element, you can do: + /// ```dart + /// someList.slice(someList.indexOf(marker) + 1).contains(marker) + /// ``` + ListSlice slice(int start, [int? end]) { + end = RangeError.checkValidRange(start, end, length); + return ListSlice._(_initialSize, source, this.start + start, end - start); + } + + @override + void shuffle([Random? random]) { + if (source.length != _initialSize) { + throw ConcurrentModificationError(source); + } + algorithms.shuffle(source, start, end, random); + } + + @override + void sort([int Function(E a, E b)? compare]) { + if (source.length != _initialSize) { + throw ConcurrentModificationError(source); + } + compare ??= defaultCompare; + quickSort(source, compare, start, start + length); + } + + /// Sort a range of elements by [compare]. + void sortRange(int start, int end, int Function(E a, E b) compare) { + if (source.length != _initialSize) { + throw ConcurrentModificationError(source); + } + source.sortRange(start, end, compare); + } + + /// Shuffles a range of elements. + /// + /// If [random] is omitted, a new instance of [Random] is used. + void shuffleRange(int start, int end, [Random? random]) { + if (source.length != _initialSize) { + throw ConcurrentModificationError(source); + } + RangeError.checkValidRange(start, end, length); + algorithms.shuffle(source, this.start + start, this.start + end, random); + } + + /// Reverses a range of elements. + void reverseRange(int start, int end) { + RangeError.checkValidRange(start, end, length); + source.reverseRange(this.start + start, this.start + end); + } + + // Act like a fixed-length list. + + @override + set length(int newLength) { + throw UnsupportedError('Cannot change the length of a fixed-length list'); + } + + @override + void add(E element) { + throw UnsupportedError('Cannot add to a fixed-length list'); + } + + @override + void insert(int index, E element) { + throw UnsupportedError('Cannot add to a fixed-length list'); + } + + @override + void insertAll(int index, Iterable iterable) { + throw UnsupportedError('Cannot add to a fixed-length list'); + } + + @override + void addAll(Iterable iterable) { + throw UnsupportedError('Cannot add to a fixed-length list'); + } + + @override + bool remove(Object? element) { + throw UnsupportedError('Cannot remove from a fixed-length list'); + } + + @override + void removeWhere(bool Function(E element) test) { + throw UnsupportedError('Cannot remove from a fixed-length list'); + } + + @override + void retainWhere(bool Function(E element) test) { + throw UnsupportedError('Cannot remove from a fixed-length list'); + } + + @override + void clear() { + throw UnsupportedError('Cannot clear a fixed-length list'); + } + + @override + E removeAt(int index) { + throw UnsupportedError('Cannot remove from a fixed-length list'); + } + + @override + E removeLast() { + throw UnsupportedError('Cannot remove from a fixed-length list'); + } + + @override + void removeRange(int start, int end) { + throw UnsupportedError('Cannot remove from a fixed-length list'); + } + + @override + void replaceRange(int start, int end, Iterable newContents) { + throw UnsupportedError('Cannot remove from a fixed-length list'); + } +} diff --git a/pkgs/collection/lib/src/priority_queue.dart b/pkgs/collection/lib/src/priority_queue.dart new file mode 100644 index 00000000..11b0348a --- /dev/null +++ b/pkgs/collection/lib/src/priority_queue.dart @@ -0,0 +1,497 @@ +// Copyright (c) 2014, 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 'dart:collection'; + +import 'utils.dart'; + +/// A priority queue is a priority based work-list of elements. +/// +/// The queue allows adding elements, and removing them again in priority order. +/// The same object can be added to the queue more than once. +/// There is no specified ordering for objects with the same priority +/// (where the `comparison` function returns zero). +/// +/// Operations which care about object equality, [contains] and [remove], +/// use [Object.==] for testing equality. +/// In most situations this will be the same as identity ([identical]), +/// but there are types, like [String], where users can reasonably expect +/// distinct objects to represent the same value. +/// If elements override [Object.==], the `comparison` function must +/// always give equal objects the same priority, +/// otherwise [contains] or [remove] might not work correctly. +abstract class PriorityQueue { + /// Creates an empty [PriorityQueue]. + /// + /// The created [PriorityQueue] is a plain [HeapPriorityQueue]. + /// + /// The [comparison] is a [Comparator] used to compare the priority of + /// elements. An element that compares as less than another element has + /// a higher priority. + /// + /// If [comparison] is omitted, it defaults to [Comparable.compare]. If this + /// is the case, `E` must implement [Comparable], and this is checked at + /// runtime for every comparison. + factory PriorityQueue([int Function(E, E)? comparison]) = + HeapPriorityQueue; + + /// Number of elements in the queue. + int get length; + + /// Whether the queue is empty. + bool get isEmpty; + + /// Whether the queue has any elements. + bool get isNotEmpty; + + /// Checks if [object] is in the queue. + /// + /// Returns true if the element is found. + /// + /// Uses the [Object.==] of elements in the queue to check + /// for whether they are equal to [object]. + /// Equal objects objects must have the same priority + /// according to the comparison function. + /// That is, if `a == b` then `comparison(a, b) == 0`. + /// If that is not the case, this check might fail to find + /// an object. + bool contains(E object); + + /// Provides efficient access to all the elements currently in the queue. + /// + /// The operation should be performed without copying or moving + /// the elements, if at all possible. + /// + /// The elements are iterated in no particular order. + /// The order is stable as long as the queue is not modified. + /// The queue must not be modified during an iteration. + Iterable get unorderedElements; + + /// Adds element to the queue. + /// + /// The element will become the next to be removed by [removeFirst] + /// when all elements with higher priority have been removed. + void add(E element); + + /// Adds all [elements] to the queue. + void addAll(Iterable elements); + + /// Returns the next element that will be returned by [removeFirst]. + /// + /// The element is not removed from the queue. + /// + /// The queue must not be empty when this method is called. + E get first; + + /// Removes and returns the element with the highest priority. + /// + /// Repeatedly calling this method, without adding element in between, + /// is guaranteed to return elements in non-decreasing order as, specified by + /// the `comparison` constructor parameter. + /// + /// The queue must not be empty when this method is called. + E removeFirst(); + + /// Removes an element of the queue that compares equal to [element]. + /// + /// Returns true if an element is found and removed, + /// and false if no equal element is found. + /// + /// If the queue contains more than one object equal to [element], + /// only one of them is removed. + /// + /// Uses the [Object.==] of elements in the queue to check + /// for whether they are equal to [element]. + /// Equal objects objects must have the same priority + /// according to the `comparison` function. + /// That is, if `a == b` then `comparison(a, b) == 0`. + /// If that is not the case, this check might fail to find + /// an object. + bool remove(E element); + + /// Removes all the elements from this queue and returns them. + /// + /// The returned iterable has no specified order. + Iterable removeAll(); + + /// Removes all the elements from this queue. + void clear(); + + /// Returns a list of the elements of this queue in priority order. + /// + /// The queue is not modified. + /// + /// The order is the order that the elements would be in if they were + /// removed from this queue using [removeFirst]. + List toList(); + + /// Returns a list of the elements of this queue in no specific order. + /// + /// The queue is not modified. + /// + /// The order of the elements is implementation specific. + /// The order may differ between different calls on the same queue. + List toUnorderedList(); + + /// Return a comparator based set using the comparator of this queue. + /// + /// The queue is not modified. + /// + /// The returned [Set] is currently a [SplayTreeSet], + /// but this may change as other ordered sets are implemented. + /// + /// The set contains all the elements of this queue. + /// If an element occurs more than once in the queue, + /// the set will contain it only once. + Set toSet(); +} + +/// Heap based priority queue. +/// +/// The elements are kept in a heap structure, +/// where the element with the highest priority is immediately accessible, +/// and modifying a single element takes +/// logarithmic time in the number of elements on average. +/// +/// * The [add] and [removeFirst] operations take amortized logarithmic time, +/// O(log(n)), but may occasionally take linear time when growing the capacity +/// of the heap. +/// * The [addAll] operation works as doing repeated [add] operations. +/// * The [first] getter takes constant time, O(1). +/// * The [clear] and [removeAll] methods also take constant time, O(1). +/// * The [contains] and [remove] operations may need to search the entire +/// queue for the elements, taking O(n) time. +/// * The [toList] operation effectively sorts the elements, taking O(n*log(n)) +/// time. +/// * The [toUnorderedList] operation copies, but does not sort, the elements, +/// and is linear, O(n). +/// * The [toSet] operation effectively adds each element to the new set, taking +/// an expected O(n*log(n)) time. +class HeapPriorityQueue implements PriorityQueue { + /// Initial capacity of a queue when created, or when added to after a + /// [clear]. + /// + /// Number can be any positive value. Picking a size that gives a whole + /// number of "tree levels" in the heap is only done for aesthetic reasons. + static const int _initialCapacity = 7; + + /// The comparison being used to compare the priority of elements. + final Comparator comparison; + + /// List implementation of a heap. + List _queue = List.filled(_initialCapacity, null); + + /// Number of elements in queue. + /// + /// The heap is implemented in the first [_length] entries of [_queue]. + int _length = 0; + + /// Modification count. + /// + /// Used to detect concurrent modifications during iteration. + int _modificationCount = 0; + + /// Create a new priority queue. + /// + /// The [comparison] is a [Comparator] used to compare the priority of + /// elements. An element that compares as less than another element has + /// a higher priority. + /// + /// If [comparison] is omitted, it defaults to [Comparable.compare]. If this + /// is the case, `E` must implement [Comparable], and this is checked at + /// runtime for every comparison. + HeapPriorityQueue([int Function(E, E)? comparison]) + : comparison = comparison ?? defaultCompare; + + E _elementAt(int index) => _queue[index] ?? (null as E); + + @override + void add(E element) { + _modificationCount++; + _add(element); + } + + @override + void addAll(Iterable elements) { + var modified = 0; + for (var element in elements) { + modified = 1; + _add(element); + } + _modificationCount += modified; + } + + @override + void clear() { + _modificationCount++; + _queue = const []; + _length = 0; + } + + @override + bool contains(E object) => _locate(object) >= 0; + + /// Provides efficient access to all the elements currently in the queue. + /// + /// The operation is performed in the order they occur + /// in the underlying heap structure. + /// + /// The order is stable as long as the queue is not modified. + /// The queue must not be modified during an iteration. + @override + Iterable get unorderedElements => _UnorderedElementsIterable(this); + + @override + E get first { + if (_length == 0) throw StateError('No element'); + return _elementAt(0); + } + + @override + bool get isEmpty => _length == 0; + + @override + bool get isNotEmpty => _length != 0; + + @override + int get length => _length; + + @override + bool remove(E element) { + var index = _locate(element); + if (index < 0) return false; + _modificationCount++; + var last = _removeLast(); + if (index < _length) { + var comp = comparison(last, element); + if (comp <= 0) { + _bubbleUp(last, index); + } else { + _bubbleDown(last, index); + } + } + return true; + } + + /// Removes all the elements from this queue and returns them. + /// + /// The returned iterable has no specified order. + /// The operation does not copy the elements, + /// but instead keeps them in the existing heap structure, + /// and iterates over that directly. + @override + Iterable removeAll() { + _modificationCount++; + var result = _queue; + var length = _length; + _queue = const []; + _length = 0; + return result.take(length).cast(); + } + + @override + E removeFirst() { + if (_length == 0) throw StateError('No element'); + _modificationCount++; + var result = _elementAt(0); + var last = _removeLast(); + if (_length > 0) { + _bubbleDown(last, 0); + } + return result; + } + + @override + List toList() => _toUnorderedList()..sort(comparison); + + @override + Set toSet() { + var set = SplayTreeSet(comparison); + for (var i = 0; i < _length; i++) { + set.add(_elementAt(i)); + } + return set; + } + + @override + List toUnorderedList() => _toUnorderedList(); + + List _toUnorderedList() => + [for (var i = 0; i < _length; i++) _elementAt(i)]; + + /// Returns some representation of the queue. + /// + /// The format isn't significant, and may change in the future. + @override + String toString() { + return _queue.take(_length).toString(); + } + + /// Add element to the queue. + /// + /// Grows the capacity if the backing list is full. + void _add(E element) { + if (_length == _queue.length) _grow(); + _bubbleUp(element, _length++); + } + + /// Find the index of an object in the heap. + /// + /// Returns -1 if the object is not found. + /// + /// A matching object, `o`, must satisfy that + /// `comparison(o, object) == 0 && o == object`. + int _locate(E object) { + if (_length == 0) return -1; + // Count positions from one instead of zero. This gives the numbers + // some nice properties. For example, all right children are odd, + // their left sibling is even, and the parent is found by shifting + // right by one. + // Valid range for position is [1.._length], inclusive. + var position = 1; + // Pre-order depth first search, omit child nodes if the current + // node has lower priority than [object], because all nodes lower + // in the heap will also have lower priority. + do { + var index = position - 1; + var element = _elementAt(index); + var comp = comparison(element, object); + if (comp <= 0) { + if (comp == 0 && element == object) return index; + // Element may be in subtree. + // Continue with the left child, if it is there. + var leftChildPosition = position * 2; + if (leftChildPosition <= _length) { + position = leftChildPosition; + continue; + } + } + // Find the next right sibling or right ancestor sibling. + do { + while (position.isOdd) { + // While position is a right child, go to the parent. + position >>= 1; + } + // Then go to the right sibling of the left-child. + position += 1; + } while (position > _length); // Happens if last element is a left child. + } while (position != 1); // At root again. Happens for right-most element. + return -1; + } + + E _removeLast() { + var newLength = _length - 1; + var last = _elementAt(newLength); + _queue[newLength] = null; + _length = newLength; + return last; + } + + /// Place [element] in heap at [index] or above. + /// + /// Put element into the empty cell at `index`. + /// While the `element` has higher priority than the + /// parent, swap it with the parent. + void _bubbleUp(E element, int index) { + while (index > 0) { + var parentIndex = (index - 1) ~/ 2; + var parent = _elementAt(parentIndex); + if (comparison(element, parent) > 0) break; + _queue[index] = parent; + index = parentIndex; + } + _queue[index] = element; + } + + /// Place [element] in heap at [index] or above. + /// + /// Put element into the empty cell at `index`. + /// While the `element` has lower priority than either child, + /// swap it with the highest priority child. + void _bubbleDown(E element, int index) { + var rightChildIndex = index * 2 + 2; + while (rightChildIndex < _length) { + var leftChildIndex = rightChildIndex - 1; + var leftChild = _elementAt(leftChildIndex); + var rightChild = _elementAt(rightChildIndex); + var comp = comparison(leftChild, rightChild); + int minChildIndex; + E minChild; + if (comp < 0) { + minChild = leftChild; + minChildIndex = leftChildIndex; + } else { + minChild = rightChild; + minChildIndex = rightChildIndex; + } + comp = comparison(element, minChild); + if (comp <= 0) { + _queue[index] = element; + return; + } + _queue[index] = minChild; + index = minChildIndex; + rightChildIndex = index * 2 + 2; + } + var leftChildIndex = rightChildIndex - 1; + if (leftChildIndex < _length) { + var child = _elementAt(leftChildIndex); + var comp = comparison(element, child); + if (comp > 0) { + _queue[index] = child; + index = leftChildIndex; + } + } + _queue[index] = element; + } + + /// Grows the capacity of the list holding the heap. + /// + /// Called when the list is full. + void _grow() { + var newCapacity = _queue.length * 2 + 1; + if (newCapacity < _initialCapacity) newCapacity = _initialCapacity; + var newQueue = List.filled(newCapacity, null); + newQueue.setRange(0, _length, _queue); + _queue = newQueue; + } +} + +/// Implementation of [HeapPriorityQueue.unorderedElements]. +class _UnorderedElementsIterable extends Iterable { + final HeapPriorityQueue _queue; + _UnorderedElementsIterable(this._queue); + @override + Iterator get iterator => _UnorderedElementsIterator(_queue); +} + +class _UnorderedElementsIterator implements Iterator { + final HeapPriorityQueue _queue; + final int _initialModificationCount; + E? _current; + int _index = -1; + + _UnorderedElementsIterator(this._queue) + : _initialModificationCount = _queue._modificationCount; + + @override + bool moveNext() { + if (_initialModificationCount != _queue._modificationCount) { + throw ConcurrentModificationError(_queue); + } + var nextIndex = _index + 1; + if (0 <= nextIndex && nextIndex < _queue.length) { + _current = _queue._queue[nextIndex]; + _index = nextIndex; + return true; + } + _current = null; + _index = -2; + return false; + } + + @override + E get current => + _index < 0 ? throw StateError('No element') : (_current ?? null as E); +} diff --git a/pkgs/collection/lib/src/queue_list.dart b/pkgs/collection/lib/src/queue_list.dart new file mode 100644 index 00000000..a3eaba13 --- /dev/null +++ b/pkgs/collection/lib/src/queue_list.dart @@ -0,0 +1,295 @@ +// Copyright (c) 2014, 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 'dart:collection'; + +/// A class that efficiently implements both [Queue] and [List]. +// TODO(nweiz): Currently this code is copied almost verbatim from +// dart:collection. The only changes are to implement List and to remove methods +// that are redundant with ListMixin. Remove or simplify it when issue 21330 is +// fixed. +class QueueList extends Object with ListMixin implements Queue { + /// Adapts [source] to be a `QueueList`. + /// + /// Any time the class would produce an element that is not a [T], the element + /// access will throw. + /// + /// Any time a [T] value is attempted stored into the adapted class, the store + /// will throw unless the value is also an instance of [S]. + /// + /// If all accessed elements of [source] are actually instances of [T] and if + /// all elements stored in the returned are actually instance of [S], + /// then the returned instance can be used as a `QueueList`. + static QueueList _castFrom(QueueList source) { + return _CastQueueList(source); + } + + /// Default and minimal initial capacity of the queue-list. + static const int _initialCapacity = 8; + List _table; + int _head; + int _tail; + + /// Creates an empty queue. + /// + /// If [initialCapacity] is given, prepare the queue for at least that many + /// elements. + QueueList([int? initialCapacity]) + : this._init(_computeInitialCapacity(initialCapacity)); + + /// Creates an empty queue with the specific initial capacity. + QueueList._init(int initialCapacity) + : assert(_isPowerOf2(initialCapacity)), + _table = List.filled(initialCapacity, null), + _head = 0, + _tail = 0; + + /// An internal constructor for use by [_CastQueueList]. + QueueList._(this._head, this._tail, this._table); + + /// Create a queue initially containing the elements of [source]. + factory QueueList.from(Iterable source) { + if (source is List) { + var length = source.length; + var queue = QueueList(length + 1); + assert(queue._table.length > length); + var sourceList = source; + queue._table.setRange(0, length, sourceList, 0); + queue._tail = length; + return queue; + } else { + return QueueList()..addAll(source); + } + } + + /// Computes the actual initial capacity based on the constructor parameter. + static int _computeInitialCapacity(int? initialCapacity) { + if (initialCapacity == null || initialCapacity < _initialCapacity) { + return _initialCapacity; + } + initialCapacity += 1; + if (_isPowerOf2(initialCapacity)) { + return initialCapacity; + } + return _nextPowerOf2(initialCapacity); + } + + // Collection interface. + + @override + void add(E element) { + _add(element); + } + + @override + void addAll(Iterable iterable) { + if (iterable is List) { + var list = iterable; + var addCount = list.length; + var length = this.length; + if (length + addCount >= _table.length) { + _preGrow(length + addCount); + // After preGrow, all elements are at the start of the list. + _table.setRange(length, length + addCount, list, 0); + _tail += addCount; + } else { + // Adding addCount elements won't reach _head. + var endSpace = _table.length - _tail; + if (addCount < endSpace) { + _table.setRange(_tail, _tail + addCount, list, 0); + _tail += addCount; + } else { + var preSpace = addCount - endSpace; + _table.setRange(_tail, _tail + endSpace, list, 0); + _table.setRange(0, preSpace, list, endSpace); + _tail = preSpace; + } + } + } else { + for (var element in iterable) { + _add(element); + } + } + } + + QueueList cast() => QueueList._castFrom(this); + + @Deprecated('Use cast instead') + QueueList retype() => cast(); + + @override + String toString() => IterableBase.iterableToFullString(this, '{', '}'); + + // Queue interface. + + @override + void addLast(E element) { + _add(element); + } + + @override + void addFirst(E element) { + _head = (_head - 1) & (_table.length - 1); + _table[_head] = element; + if (_head == _tail) _grow(); + } + + @override + E removeFirst() { + if (_head == _tail) throw StateError('No element'); + var result = _table[_head] as E; + _table[_head] = null; + _head = (_head + 1) & (_table.length - 1); + return result; + } + + @override + E removeLast() { + if (_head == _tail) throw StateError('No element'); + _tail = (_tail - 1) & (_table.length - 1); + var result = _table[_tail] as E; + _table[_tail] = null; + return result; + } + + // List interface. + + @override + int get length => (_tail - _head) & (_table.length - 1); + + @override + set length(int value) { + if (value < 0) throw RangeError('Length $value may not be negative.'); + if (value > length && null is! E) { + throw UnsupportedError( + 'The length can only be increased when the element type is ' + 'nullable, but the current element type is `$E`.'); + } + + var delta = value - length; + if (delta >= 0) { + if (_table.length <= value) { + _preGrow(value); + } + _tail = (_tail + delta) & (_table.length - 1); + return; + } + + var newTail = _tail + delta; // [delta] is negative. + if (newTail >= 0) { + _table.fillRange(newTail, _tail, null); + } else { + newTail += _table.length; + _table.fillRange(0, _tail, null); + _table.fillRange(newTail, _table.length, null); + } + _tail = newTail; + } + + @override + E operator [](int index) { + if (index < 0 || index >= length) { + throw RangeError('Index $index must be in the range [0..$length).'); + } + + return _table[(_head + index) & (_table.length - 1)] as E; + } + + @override + void operator []=(int index, E value) { + if (index < 0 || index >= length) { + throw RangeError('Index $index must be in the range [0..$length).'); + } + + _table[(_head + index) & (_table.length - 1)] = value; + } + + // Internal helper functions. + + /// Whether [number] is a power of two. + /// + /// Only works for positive numbers. + static bool _isPowerOf2(int number) => (number & (number - 1)) == 0; + + /// Rounds [number] up to the nearest power of 2. + /// + /// If [number] is a power of 2 already, it is returned. + /// + /// Only works for positive numbers. + static int _nextPowerOf2(int number) { + assert(number > 0); + number = (number << 1) - 1; + for (;;) { + var nextNumber = number & (number - 1); + if (nextNumber == 0) return number; + number = nextNumber; + } + } + + /// Adds element at end of queue. Used by both [add] and [addAll]. + void _add(E element) { + _table[_tail] = element; + _tail = (_tail + 1) & (_table.length - 1); + if (_head == _tail) _grow(); + } + + /// Grow the table when full. + void _grow() { + var newTable = List.filled(_table.length * 2, null); + var split = _table.length - _head; + newTable.setRange(0, split, _table, _head); + newTable.setRange(split, split + _head, _table, 0); + _head = 0; + _tail = _table.length; + _table = newTable; + } + + int _writeToList(List target) { + assert(target.length >= length); + if (_head <= _tail) { + var length = _tail - _head; + target.setRange(0, length, _table, _head); + return length; + } else { + var firstPartSize = _table.length - _head; + target.setRange(0, firstPartSize, _table, _head); + target.setRange(firstPartSize, firstPartSize + _tail, _table, 0); + return _tail + firstPartSize; + } + } + + /// Grows the table even if it is not full. + void _preGrow(int newElementCount) { + assert(newElementCount >= length); + + // Add 1.5x extra room to ensure that there's room for more elements after + // expansion. + newElementCount += newElementCount >> 1; + var newCapacity = _nextPowerOf2(newElementCount); + var newTable = List.filled(newCapacity, null); + _tail = _writeToList(newTable); + _table = newTable; + _head = 0; + } +} + +class _CastQueueList extends QueueList { + final QueueList _delegate; + + // Assigns invalid values for head/tail because it uses the delegate to hold + // the real values, but they are non-null fields. + _CastQueueList(this._delegate) : super._(-1, -1, _delegate._table.cast()); + + @override + int get _head => _delegate._head; + + @override + set _head(int value) => _delegate._head = value; + + @override + int get _tail => _delegate._tail; + + @override + set _tail(int value) => _delegate._tail = value; +} diff --git a/pkgs/collection/lib/src/union_set.dart b/pkgs/collection/lib/src/union_set.dart new file mode 100644 index 00000000..a34d7ad3 --- /dev/null +++ b/pkgs/collection/lib/src/union_set.dart @@ -0,0 +1,80 @@ +// Copyright (c) 2016, 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 'dart:collection'; + +import 'unmodifiable_wrappers.dart'; + +/// A single set that provides a view of the union over a set of sets. +/// +/// Since this is just a view, it reflects all changes in the underlying sets. +/// +/// If an element is in multiple sets and the outer set is ordered, the version +/// in the earliest inner set is preferred. Component sets are assumed to use +/// `==` and `hashCode` for equality. +class UnionSet extends SetBase with UnmodifiableSetMixin { + /// The set of sets that this provides a view of. + final Set> _sets; + + /// Whether the sets in [_sets] are guaranteed to be disjoint. + final bool _disjoint; + + /// Creates a new set that's a view of the union of all sets in [sets]. + /// + /// If any sets in [sets] change, this [UnionSet] reflects that change. If a + /// new set is added to [sets], this [UnionSet] reflects that as well. + /// + /// If [disjoint] is `true`, then all component sets must be disjoint. That + /// is, that they contain no elements in common. This makes many operations + /// including [length] more efficient. If the component sets turn out not to + /// be disjoint, some operations may behave inconsistently. + UnionSet(Set> sets, {bool disjoint = false}) + : _sets = sets, + _disjoint = disjoint; + + /// Creates a new set that's a view of the union of all sets in [sets]. + /// + /// If any sets in [sets] change, this [UnionSet] reflects that change. + /// However, unlike [UnionSet.new], this creates a copy of its parameter, so + /// changes in [sets] aren't reflected in this [UnionSet]. + /// + /// If [disjoint] is `true`, then all component sets must be disjoint. That + /// is, that they contain no elements in common. This makes many operations + /// including [length] more efficient. If the component sets turn out not to + /// be disjoint, some operations may behave inconsistently. + UnionSet.from(Iterable> sets, {bool disjoint = false}) + : this(sets.toSet(), disjoint: disjoint); + + @override + int get length => _disjoint + ? _sets.fold(0, (length, set) => length + set.length) + : _iterable.length; + + @override + Iterator get iterator => _iterable.iterator; + + /// An iterable over the contents of all [_sets]. + /// + /// If this is not a [_disjoint] union an extra set is used to deduplicate + /// values. + Iterable get _iterable { + var allElements = _sets.expand((set) => set); + return _disjoint ? allElements : allElements.where({}.add); + } + + @override + bool contains(Object? element) => _sets.any((set) => set.contains(element)); + + @override + E? lookup(Object? element) { + for (var set in _sets) { + var result = set.lookup(element); + if (result != null || set.contains(null)) return result; + } + return null; + } + + @override + Set toSet() => {for (var set in _sets) ...set}; +} diff --git a/pkgs/collection/lib/src/union_set_controller.dart b/pkgs/collection/lib/src/union_set_controller.dart new file mode 100644 index 00000000..498528e2 --- /dev/null +++ b/pkgs/collection/lib/src/union_set_controller.dart @@ -0,0 +1,55 @@ +// Copyright (c) 2016, 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 'union_set.dart'; + +/// A controller that exposes a view of the union of a collection of sets. +/// +/// This is a convenience class for creating a [UnionSet] whose contents change +/// over the lifetime of a class. For example: +/// +/// ```dart +/// class Engine { +/// Set get activeTests => _activeTestsGroup.set; +/// final _activeTestsGroup = UnionSetController(); +/// +/// void addSuite(Suite suite) { +/// _activeTestsGroup.add(suite.tests); +/// _runSuite(suite); +/// _activeTestsGroup.remove(suite.tests); +/// } +/// } +/// ``` +class UnionSetController { + /// The [UnionSet] that provides a view of the union of sets in `this`. + final UnionSet set; + + /// The sets whose union is exposed through [set]. + final Set> _sets; + + /// Creates a set of sets that provides a view of the union of those sets. + /// + /// If [disjoint] is `true`, this assumes that all component sets are + /// disjoint—that is, that they contain no elements in common. This makes + /// many operations including `length` more efficient. + UnionSetController({bool disjoint = false}) : this._(>{}, disjoint); + + /// Creates a controller with the provided [_sets]. + UnionSetController._(this._sets, bool disjoint) + : set = UnionSet(_sets, disjoint: disjoint); + + /// Adds the contents of [component] to [set]. + /// + /// If the contents of [component] change over time, [set] will change + /// accordingly. + void add(Set component) { + _sets.add(component); + } + + /// Removes the contents of [component] to [set]. + /// + /// If another set in `this` has overlapping elements with [component], those + /// elements will remain in [set]. + bool remove(Set component) => _sets.remove(component); +} diff --git a/pkgs/collection/lib/src/unmodifiable_wrappers.dart b/pkgs/collection/lib/src/unmodifiable_wrappers.dart new file mode 100644 index 00000000..3b211c07 --- /dev/null +++ b/pkgs/collection/lib/src/unmodifiable_wrappers.dart @@ -0,0 +1,204 @@ +// Copyright (c) 2013, 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 'empty_unmodifiable_set.dart'; +import 'wrappers.dart'; + +export 'dart:collection' show UnmodifiableListView, UnmodifiableMapView; + +/// A fixed-length list. +/// +/// A `NonGrowableListView` contains a [List] object and ensures that +/// its length does not change. +/// Methods that would change the length of the list, +/// such as [add] and [remove], throw an [UnsupportedError]. +/// All other methods work directly on the underlying list. +/// +/// This class _does_ allow changes to the contents of the wrapped list. +/// You can, for example, [sort] the list. +/// Permitted operations defer to the wrapped list. +class NonGrowableListView extends DelegatingList + with NonGrowableListMixin { + NonGrowableListView(super.listBase); +} + +/// Mixin class that implements a throwing version of all list operations that +/// change the List's length. +abstract mixin class NonGrowableListMixin implements List { + static Never _throw() { + throw UnsupportedError('Cannot change the length of a fixed-length list'); + } + + /// Throws an [UnsupportedError]; + /// operations that change the length of the list are disallowed. + @override + set length(int newLength) => _throw(); + + /// Throws an [UnsupportedError]; + /// operations that change the length of the list are disallowed. + @override + bool add(E value) => _throw(); + + /// Throws an [UnsupportedError]; + /// operations that change the length of the list are disallowed. + @override + void addAll(Iterable iterable) => _throw(); + + /// Throws an [UnsupportedError]; + /// operations that change the length of the list are disallowed. + @override + void insert(int index, E element) => _throw(); + + /// Throws an [UnsupportedError]; + /// operations that change the length of the list are disallowed. + @override + void insertAll(int index, Iterable iterable) => _throw(); + + /// Throws an [UnsupportedError]; + /// operations that change the length of the list are disallowed. + @override + bool remove(Object? value) => _throw(); + + /// Throws an [UnsupportedError]; + /// operations that change the length of the list are disallowed. + @override + E removeAt(int index) => _throw(); + + /// Throws an [UnsupportedError]; + /// operations that change the length of the list are disallowed. + @override + E removeLast() => _throw(); + + /// Throws an [UnsupportedError]; + /// operations that change the length of the list are disallowed. + @override + void removeWhere(bool Function(E) test) => _throw(); + + /// Throws an [UnsupportedError]; + /// operations that change the length of the list are disallowed. + @override + void retainWhere(bool Function(E) test) => _throw(); + + /// Throws an [UnsupportedError]; + /// operations that change the length of the list are disallowed. + @override + void removeRange(int start, int end) => _throw(); + + /// Throws an [UnsupportedError]; + /// operations that change the length of the list are disallowed. + @override + void replaceRange(int start, int end, Iterable iterable) => _throw(); + + /// Throws an [UnsupportedError]; + /// operations that change the length of the list are disallowed. + @override + void clear() => _throw(); +} + +/// An unmodifiable set. +/// +/// An [UnmodifiableSetView] contains a [Set], +/// and prevents that set from being changed through the view. +/// Methods that could change the set, +/// such as [add] and [remove], throw an [UnsupportedError]. +/// Permitted operations defer to the wrapped set. +class UnmodifiableSetView extends DelegatingSet + with UnmodifiableSetMixin { + UnmodifiableSetView(super.setBase); + + /// An unmodifiable empty set. + /// + /// This is the same as `UnmodifiableSetView(Set())`, except that it + /// can be used in const contexts. + const factory UnmodifiableSetView.empty() = EmptyUnmodifiableSet; +} + +/// Mixin class that implements a throwing version of all set operations that +/// change the Set. +abstract mixin class UnmodifiableSetMixin implements Set { + static Never _throw() { + throw UnsupportedError('Cannot modify an unmodifiable Set'); + } + + /// Throws an [UnsupportedError]; + /// operations that change the set are disallowed. + @override + bool add(E value) => _throw(); + + /// Throws an [UnsupportedError]; + /// operations that change the set are disallowed. + @override + void addAll(Iterable elements) => _throw(); + + /// Throws an [UnsupportedError]; + /// operations that change the set are disallowed. + @override + bool remove(Object? value) => _throw(); + + /// Throws an [UnsupportedError]; + /// operations that change the set are disallowed. + @override + void removeAll(Iterable elements) => _throw(); + + /// Throws an [UnsupportedError]; + /// operations that change the set are disallowed. + @override + void retainAll(Iterable elements) => _throw(); + + /// Throws an [UnsupportedError]; + /// operations that change the set are disallowed. + @override + void removeWhere(bool Function(E) test) => _throw(); + + /// Throws an [UnsupportedError]; + /// operations that change the set are disallowed. + @override + void retainWhere(bool Function(E) test) => _throw(); + + /// Throws an [UnsupportedError]; + /// operations that change the set are disallowed. + @override + void clear() => _throw(); +} + +/// Mixin class that implements a throwing version of all map operations that +/// change the Map. +abstract mixin class UnmodifiableMapMixin implements Map { + static Never _throw() { + throw UnsupportedError('Cannot modify an unmodifiable Map'); + } + + /// Throws an [UnsupportedError]; + /// operations that change the map are disallowed. + @override + void operator []=(K key, V value) => _throw(); + + /// Throws an [UnsupportedError]; + /// operations that change the map are disallowed. + @override + V putIfAbsent(K key, V Function() ifAbsent) => _throw(); + + /// Throws an [UnsupportedError]; + /// operations that change the map are disallowed. + @override + void addAll(Map other) => _throw(); + + /// Throws an [UnsupportedError]; + /// operations that change the map are disallowed. + @override + V remove(Object? key) => _throw(); + + /// Throws an [UnsupportedError]; + /// operations that change the map are disallowed. + @override + void clear() => _throw(); + + /// Throws an [UnsupportedError]; + /// operations that change the map are disallowed. + set first(_) => _throw(); + + /// Throws an [UnsupportedError]; + /// operations that change the map are disallowed. + set last(_) => _throw(); +} diff --git a/pkgs/collection/lib/src/utils.dart b/pkgs/collection/lib/src/utils.dart new file mode 100644 index 00000000..64088f0f --- /dev/null +++ b/pkgs/collection/lib/src/utils.dart @@ -0,0 +1,20 @@ +// Copyright (c) 2014, 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. + +/// A [Comparator] that asserts that its first argument is comparable. +/// +/// The function behaves just like [List.sort]'s +/// default comparison function. It is entirely dynamic in its testing. +/// +/// Should be used when optimistically comparing object that are assumed +/// to be comparable. +/// If the elements are known to be comparable, use [compareComparable]. +int defaultCompare(Object? value1, Object? value2) => + (value1 as Comparable).compareTo(value2); + +/// A reusable identity function at any type. +T identity(T value) => value; + +/// A reusable typed comparable comparator. +int compareComparable>(T a, T b) => a.compareTo(b); diff --git a/pkgs/collection/lib/src/wrappers.dart b/pkgs/collection/lib/src/wrappers.dart new file mode 100644 index 00000000..859d0bcc --- /dev/null +++ b/pkgs/collection/lib/src/wrappers.dart @@ -0,0 +1,838 @@ +// Copyright (c) 2013, 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 'dart:collection'; +import 'dart:math' as math; + +import 'unmodifiable_wrappers.dart'; + +/// A base class for delegating iterables. +/// +/// Subclasses can provide a [_base] that should be delegated to. Unlike +/// [DelegatingIterable], this allows the base to be created on demand. +abstract class _DelegatingIterableBase implements Iterable { + Iterable get _base; + + const _DelegatingIterableBase(); + + @override + bool any(bool Function(E) test) => _base.any(test); + + @override + Iterable cast() => _base.cast(); + + @override + bool contains(Object? element) => _base.contains(element); + + @override + E elementAt(int index) => _base.elementAt(index); + + @override + bool every(bool Function(E) test) => _base.every(test); + + @override + Iterable expand(Iterable Function(E) f) => _base.expand(f); + + @override + E get first => _base.first; + + @override + E firstWhere(bool Function(E) test, {E Function()? orElse}) => + _base.firstWhere(test, orElse: orElse); + + @override + T fold(T initialValue, T Function(T previousValue, E element) combine) => + _base.fold(initialValue, combine); + + @override + Iterable followedBy(Iterable other) => _base.followedBy(other); + + @override + void forEach(void Function(E) f) => _base.forEach(f); + + @override + bool get isEmpty => _base.isEmpty; + + @override + bool get isNotEmpty => _base.isNotEmpty; + + @override + Iterator get iterator => _base.iterator; + + @override + String join([String separator = '']) => _base.join(separator); + + @override + E get last => _base.last; + + @override + E lastWhere(bool Function(E) test, {E Function()? orElse}) => + _base.lastWhere(test, orElse: orElse); + + @override + int get length => _base.length; + + @override + Iterable map(T Function(E) f) => _base.map(f); + + @override + E reduce(E Function(E value, E element) combine) => _base.reduce(combine); + + @Deprecated('Use cast instead') + Iterable retype() => cast(); + + @override + E get single => _base.single; + + @override + E singleWhere(bool Function(E) test, {E Function()? orElse}) { + return _base.singleWhere(test, orElse: orElse); + } + + @override + Iterable skip(int n) => _base.skip(n); + + @override + Iterable skipWhile(bool Function(E) test) => _base.skipWhile(test); + + @override + Iterable take(int n) => _base.take(n); + + @override + Iterable takeWhile(bool Function(E) test) => _base.takeWhile(test); + + @override + List toList({bool growable = true}) => _base.toList(growable: growable); + + @override + Set toSet() => _base.toSet(); + + @override + Iterable where(bool Function(E) test) => _base.where(test); + + @override + Iterable whereType() => _base.whereType(); + + @override + String toString() => _base.toString(); +} + +/// An [Iterable] that delegates all operations to a base iterable. +/// +/// This class can be used to hide non-`Iterable` methods of an iterable object, +/// or it can be extended to add extra functionality on top of an existing +/// iterable object. +class DelegatingIterable extends _DelegatingIterableBase { + @override + final Iterable _base; + + /// Creates a wrapper that forwards operations to [base]. + const DelegatingIterable(Iterable base) : _base = base; + + /// Creates a wrapper that asserts the types of values in [base]. + /// + /// This soundly converts an [Iterable] without a generic type to an + /// `Iterable` by asserting that its elements are instances of `E` whenever + /// they're accessed. If they're not, it throws a [TypeError]. + /// + /// This forwards all operations to [base], so any changes in [base] will be + /// reflected in `this`. If [base] is already an `Iterable`, it's returned + /// unmodified. + @Deprecated('Use iterable.cast instead.') + static Iterable typed(Iterable base) => base.cast(); +} + +/// A [List] that delegates all operations to a base list. +/// +/// This class can be used to hide non-`List` methods of a list object, or it +/// can be extended to add extra functionality on top of an existing list +/// object. +class DelegatingList extends _DelegatingIterableBase implements List { + @override + final List _base; + + const DelegatingList(List base) : _base = base; + + /// Creates a wrapper that asserts the types of values in [base]. + /// + /// This soundly converts a [List] without a generic type to a `List` by + /// asserting that its elements are instances of `E` whenever they're + /// accessed. If they're not, it throws a [TypeError]. Note that even if an + /// operation throws a [TypeError], it may still mutate the underlying + /// collection. + /// + /// This forwards all operations to [base], so any changes in [base] will be + /// reflected in `this`. If [base] is already a `List`, it's returned + /// unmodified. + @Deprecated('Use list.cast instead.') + static List typed(List base) => base.cast(); + + @override + E operator [](int index) => _base[index]; + + @override + void operator []=(int index, E value) { + _base[index] = value; + } + + @override + List operator +(List other) => _base + other; + + @override + void add(E value) { + _base.add(value); + } + + @override + void addAll(Iterable iterable) { + _base.addAll(iterable); + } + + @override + Map asMap() => _base.asMap(); + + @override + List cast() => _base.cast(); + + @override + void clear() { + _base.clear(); + } + + @override + void fillRange(int start, int end, [E? fillValue]) { + _base.fillRange(start, end, fillValue); + } + + @override + set first(E value) { + if (isEmpty) throw RangeError.index(0, this); + this[0] = value; + } + + @override + Iterable getRange(int start, int end) => _base.getRange(start, end); + + @override + int indexOf(E element, [int start = 0]) => _base.indexOf(element, start); + + @override + int indexWhere(bool Function(E) test, [int start = 0]) => + _base.indexWhere(test, start); + + @override + void insert(int index, E element) { + _base.insert(index, element); + } + + @override + void insertAll(int index, Iterable iterable) { + _base.insertAll(index, iterable); + } + + @override + set last(E value) { + if (isEmpty) throw RangeError.index(0, this); + this[length - 1] = value; + } + + @override + int lastIndexOf(E element, [int? start]) => _base.lastIndexOf(element, start); + + @override + int lastIndexWhere(bool Function(E) test, [int? start]) => + _base.lastIndexWhere(test, start); + + @override + set length(int newLength) { + _base.length = newLength; + } + + @override + bool remove(Object? value) => _base.remove(value); + + @override + E removeAt(int index) => _base.removeAt(index); + + @override + E removeLast() => _base.removeLast(); + + @override + void removeRange(int start, int end) { + _base.removeRange(start, end); + } + + @override + void removeWhere(bool Function(E) test) { + _base.removeWhere(test); + } + + @override + void replaceRange(int start, int end, Iterable iterable) { + _base.replaceRange(start, end, iterable); + } + + @override + void retainWhere(bool Function(E) test) { + _base.retainWhere(test); + } + + @Deprecated('Use cast instead') + @override + List retype() => cast(); + + @override + Iterable get reversed => _base.reversed; + + @override + void setAll(int index, Iterable iterable) { + _base.setAll(index, iterable); + } + + @override + void setRange(int start, int end, Iterable iterable, [int skipCount = 0]) { + _base.setRange(start, end, iterable, skipCount); + } + + @override + void shuffle([math.Random? random]) { + _base.shuffle(random); + } + + @override + void sort([int Function(E, E)? compare]) { + _base.sort(compare); + } + + @override + List sublist(int start, [int? end]) => _base.sublist(start, end); +} + +/// A [Set] that delegates all operations to a base set. +/// +/// This class can be used to hide non-`Set` methods of a set object, or it can +/// be extended to add extra functionality on top of an existing set object. +class DelegatingSet extends _DelegatingIterableBase implements Set { + @override + final Set _base; + + const DelegatingSet(Set base) : _base = base; + + /// Creates a wrapper that asserts the types of values in [base]. + /// + /// This soundly converts a [Set] without a generic type to a `Set` by + /// asserting that its elements are instances of `E` whenever they're + /// accessed. If they're not, it throws a [TypeError]. Note that even if an + /// operation throws a [TypeError], it may still mutate the underlying + /// collection. + /// + /// This forwards all operations to [base], so any changes in [base] will be + /// reflected in `this`. If [base] is already a `Set`, it's returned + /// unmodified. + @Deprecated('Use set.cast instead.') + static Set typed(Set base) => base.cast(); + + @override + bool add(E value) => _base.add(value); + + @override + void addAll(Iterable elements) { + _base.addAll(elements); + } + + @override + Set cast() => _base.cast(); + + @override + void clear() { + _base.clear(); + } + + @override + bool containsAll(Iterable other) => _base.containsAll(other); + + @override + Set difference(Set other) => _base.difference(other); + + @override + Set intersection(Set other) => _base.intersection(other); + + @override + E? lookup(Object? element) => _base.lookup(element); + + @override + bool remove(Object? value) => _base.remove(value); + + @override + void removeAll(Iterable elements) { + _base.removeAll(elements); + } + + @override + void removeWhere(bool Function(E) test) { + _base.removeWhere(test); + } + + @override + void retainAll(Iterable elements) { + _base.retainAll(elements); + } + + @Deprecated('Use cast instead') + @override + Set retype() => cast(); + + @override + void retainWhere(bool Function(E) test) { + _base.retainWhere(test); + } + + @override + Set union(Set other) => _base.union(other); + + @override + Set toSet() => DelegatingSet(_base.toSet()); +} + +/// A [Queue] that delegates all operations to a base queue. +/// +/// This class can be used to hide non-`Queue` methods of a queue object, or it +/// can be extended to add extra functionality on top of an existing queue +/// object. +class DelegatingQueue extends _DelegatingIterableBase + implements Queue { + @override + final Queue _base; + + const DelegatingQueue(Queue queue) : _base = queue; + + /// Creates a wrapper that asserts the types of values in [base]. + /// + /// This soundly converts a [Queue] without a generic type to a `Queue` by + /// asserting that its elements are instances of `E` whenever they're + /// accessed. If they're not, it throws a [TypeError]. Note that even if an + /// operation throws a [TypeError], it may still mutate the underlying + /// collection. + /// + /// This forwards all operations to [base], so any changes in [base] will be + /// reflected in `this`. If [base] is already a `Queue`, it's returned + /// unmodified. + @Deprecated('Use queue.cast instead.') + static Queue typed(Queue base) => base.cast(); + + @override + void add(E value) { + _base.add(value); + } + + @override + void addAll(Iterable iterable) { + _base.addAll(iterable); + } + + @override + void addFirst(E value) { + _base.addFirst(value); + } + + @override + void addLast(E value) { + _base.addLast(value); + } + + @override + Queue cast() => _base.cast(); + + @override + void clear() { + _base.clear(); + } + + @override + bool remove(Object? object) => _base.remove(object); + + @override + void removeWhere(bool Function(E) test) { + _base.removeWhere(test); + } + + @override + void retainWhere(bool Function(E) test) { + _base.retainWhere(test); + } + + @Deprecated('Use cast instead') + @override + Queue retype() => cast(); + + @override + E removeFirst() => _base.removeFirst(); + + @override + E removeLast() => _base.removeLast(); +} + +/// A [Map] that delegates all operations to a base map. +/// +/// This class can be used to hide non-`Map` methods of an object that extends +/// `Map`, or it can be extended to add extra functionality on top of an +/// existing map object. +class DelegatingMap implements Map { + final Map _base; + + const DelegatingMap(Map base) : _base = base; + + /// Creates a wrapper that asserts the types of keys and values in [base]. + /// + /// This soundly converts a [Map] without generic types to a `Map` by + /// asserting that its keys are instances of `E` and its values are instances + /// of `V` whenever they're accessed. If they're not, it throws a [TypeError]. + /// Note that even if an operation throws a [TypeError], it may still mutate + /// the underlying collection. + /// + /// This forwards all operations to [base], so any changes in [base] will be + /// reflected in `this`. If [base] is already a `Map`, it's returned + /// unmodified. + @Deprecated('Use map.cast instead.') + static Map typed(Map base) => base.cast(); + + @override + V? operator [](Object? key) => _base[key]; + + @override + void operator []=(K key, V value) { + _base[key] = value; + } + + @override + void addAll(Map other) { + _base.addAll(other); + } + + @override + void addEntries(Iterable> entries) { + _base.addEntries(entries); + } + + @override + void clear() { + _base.clear(); + } + + @override + Map cast() => _base.cast(); + + @override + bool containsKey(Object? key) => _base.containsKey(key); + + @override + bool containsValue(Object? value) => _base.containsValue(value); + + @override + Iterable> get entries => _base.entries; + + @override + void forEach(void Function(K, V) f) { + _base.forEach(f); + } + + @override + bool get isEmpty => _base.isEmpty; + + @override + bool get isNotEmpty => _base.isNotEmpty; + + @override + Iterable get keys => _base.keys; + + @override + int get length => _base.length; + + @override + Map map(MapEntry Function(K, V) transform) => + _base.map(transform); + + @override + V putIfAbsent(K key, V Function() ifAbsent) => + _base.putIfAbsent(key, ifAbsent); + + @override + V? remove(Object? key) => _base.remove(key); + + @override + void removeWhere(bool Function(K, V) test) => _base.removeWhere(test); + + @Deprecated('Use cast instead') + Map retype() => cast(); + + @override + Iterable get values => _base.values; + + @override + String toString() => _base.toString(); + + @override + V update(K key, V Function(V) update, {V Function()? ifAbsent}) => + _base.update(key, update, ifAbsent: ifAbsent); + + @override + void updateAll(V Function(K, V) update) => _base.updateAll(update); +} + +/// An unmodifiable [Set] view of the keys of a [Map]. +/// +/// The set delegates all operations to the underlying map. +/// +/// A `Map` can only contain each key once, so its keys can always +/// be viewed as a `Set` without any loss, even if the [Map.keys] +/// getter only shows an [Iterable] view of the keys. +/// +/// Note that [lookup] is not supported for this set. +class MapKeySet extends _DelegatingIterableBase + with UnmodifiableSetMixin { + final Map _baseMap; + + MapKeySet(this._baseMap); + + @override + Iterable get _base => _baseMap.keys; + + @override + Set cast() { + if (this is MapKeySet) { + return this as MapKeySet; + } + return Set.castFrom(this); + } + + @override + bool contains(Object? element) => _baseMap.containsKey(element); + + @override + bool get isEmpty => _baseMap.isEmpty; + + @override + bool get isNotEmpty => _baseMap.isNotEmpty; + + @override + int get length => _baseMap.length; + + @override + String toString() => SetBase.setToString(this); + + @override + bool containsAll(Iterable other) => other.every(contains); + + /// Returns a new set with the the elements of `this` that are not in [other]. + /// + /// That is, the returned set contains all the elements of this [Set] that are + /// not elements of [other] according to `other.contains`. + /// + /// Note that the returned set will use the default equality operation, which + /// may be different than the equality operation `this` uses. + @override + Set difference(Set other) => + where((element) => !other.contains(element)).toSet(); + + /// Returns a new set which is the intersection between `this` and [other]. + /// + /// That is, the returned set contains all the elements of this [Set] that are + /// also elements of [other] according to `other.contains`. + /// + /// Note that the returned set will use the default equality operation, which + /// may be different than the equality operation `this` uses. + @override + Set intersection(Set other) => where(other.contains).toSet(); + + /// Throws an [UnsupportedError] since there's no corresponding method for + /// [Map]s. + @override + E lookup(Object? element) => + throw UnsupportedError("MapKeySet doesn't support lookup()."); + + @Deprecated('Use cast instead') + @override + Set retype() => Set.castFrom(this); + + /// Returns a new set which contains all the elements of `this` and [other]. + /// + /// That is, the returned set contains all the elements of this [Set] and all + /// the elements of [other]. + /// + /// Note that the returned set will use the default equality operation, which + /// may be different than the equality operation `this` uses. + @override + Set union(Set other) => toSet()..addAll(other); +} + +/// Creates a modifiable [Set] view of the values of a [Map]. +/// +/// The `Set` view assumes that the keys of the `Map` can be uniquely determined +/// from the values. The `keyForValue` function passed to the constructor finds +/// the key for a single value. The `keyForValue` function should be consistent +/// with equality. If `value1 == value2` then `keyForValue(value1)` and +/// `keyForValue(value2)` should be considered equal keys by the underlying map, +/// and vice versa. +/// +/// Modifying the set will modify the underlying map based on the key returned +/// by `keyForValue`. +/// +/// If the `Map` contents are not compatible with the `keyForValue` function, +/// the set will not work consistently, and may give meaningless responses or do +/// inconsistent updates. +/// +/// This set can, for example, be used on a map from database record IDs to the +/// records. It exposes the records as a set, and allows for writing both +/// `recordSet.add(databaseRecord)` and `recordMap[id]`. +/// +/// Effectively, the map will act as a kind of index for the set. +class MapValueSet extends _DelegatingIterableBase implements Set { + final Map _baseMap; + final K Function(V) _keyForValue; + + /// Creates a new [MapValueSet] based on [_baseMap]. + /// + /// [_keyForValue] returns the key in the map that should be associated with + /// the given value. The set's notion of equality is identical to the equality + /// of the return values of [_keyForValue]. + MapValueSet(this._baseMap, this._keyForValue); + + @override + Iterable get _base => _baseMap.values; + + @override + Set cast() { + if (this is Set) { + return this as Set; + } + return Set.castFrom(this); + } + + @override + bool contains(Object? element) { + if (element is! V) return false; + var key = _keyForValue(element); + + return _baseMap.containsKey(key); + } + + @override + bool get isEmpty => _baseMap.isEmpty; + + @override + bool get isNotEmpty => _baseMap.isNotEmpty; + + @override + int get length => _baseMap.length; + + @override + String toString() => toSet().toString(); + + @override + bool add(V value) { + var key = _keyForValue(value); + var result = false; + _baseMap.putIfAbsent(key, () { + result = true; + return value; + }); + return result; + } + + @override + void addAll(Iterable elements) => elements.forEach(add); + + @override + void clear() => _baseMap.clear(); + + @override + bool containsAll(Iterable other) => other.every(contains); + + /// Returns a new set with the the elements of `this` that are not in [other]. + /// + /// That is, the returned set contains all the elements of this [Set] that are + /// not elements of [other] according to `other.contains`. + /// + /// Note that the returned set will use the default equality operation, which + /// may be different than the equality operation `this` uses. + @override + Set difference(Set other) => + where((element) => !other.contains(element)).toSet(); + + /// Returns a new set which is the intersection between `this` and [other]. + /// + /// That is, the returned set contains all the elements of this [Set] that are + /// also elements of [other] according to `other.contains`. + /// + /// Note that the returned set will use the default equality operation, which + /// may be different than the equality operation `this` uses. + @override + Set intersection(Set other) => where(other.contains).toSet(); + + @override + V? lookup(Object? element) { + if (element is! V) return null; + var key = _keyForValue(element); + + return _baseMap[key]; + } + + @override + bool remove(Object? element) { + if (element is! V) return false; + var key = _keyForValue(element); + + if (!_baseMap.containsKey(key)) return false; + _baseMap.remove(key); + return true; + } + + @override + void removeAll(Iterable elements) => elements.forEach(remove); + + @override + void removeWhere(bool Function(V) test) { + var toRemove = []; + _baseMap.forEach((key, value) { + if (test(value)) toRemove.add(key); + }); + toRemove.forEach(_baseMap.remove); + } + + @override + void retainAll(Iterable elements) { + var valuesToRetain = Set.identity(); + for (var element in elements) { + if (element is! V) continue; + var key = _keyForValue(element); + + if (!_baseMap.containsKey(key)) continue; + valuesToRetain.add(_baseMap[key] ?? null as V); + } + + var keysToRemove = []; + _baseMap.forEach((k, v) { + if (!valuesToRetain.contains(v)) keysToRemove.add(k); + }); + keysToRemove.forEach(_baseMap.remove); + } + + @override + void retainWhere(bool Function(V) test) => + removeWhere((element) => !test(element)); + + @Deprecated('Use cast instead') + @override + Set retype() => Set.castFrom(this); + + /// Returns a new set which contains all the elements of `this` and [other]. + /// + /// That is, the returned set contains all the elements of this [Set] and all + /// the elements of [other]. + /// + /// Note that the returned set will use the default equality operation, which + /// may be different than the equality operation `this` uses. + @override + Set union(Set other) => toSet()..addAll(other); +} diff --git a/pkgs/collection/lib/wrappers.dart b/pkgs/collection/lib/wrappers.dart new file mode 100644 index 00000000..be529ca2 --- /dev/null +++ b/pkgs/collection/lib/wrappers.dart @@ -0,0 +1,11 @@ +// Copyright (c) 2013, 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 `collection.dart` instead. +@Deprecated('Will be removed in collection 2.0.0.') +library; + +export 'src/canonicalized_map.dart'; +export 'src/unmodifiable_wrappers.dart'; +export 'src/wrappers.dart'; diff --git a/pkgs/collection/pubspec.yaml b/pkgs/collection/pubspec.yaml new file mode 100644 index 00000000..54477398 --- /dev/null +++ b/pkgs/collection/pubspec.yaml @@ -0,0 +1,16 @@ +name: collection +version: 1.19.1 +description: >- + Collections and utilities functions and classes related to collections. +repository: https://github.com/dart-lang/core/tree/main/pkgs/collection + +topics: + - collections + - data-structures + +environment: + sdk: ^3.4.0 + +dev_dependencies: + dart_flutter_team_lints: ^3.0.0 + test: ^1.16.0 diff --git a/pkgs/collection/test/algorithms_test.dart b/pkgs/collection/test/algorithms_test.dart new file mode 100644 index 00000000..4bc1d54b --- /dev/null +++ b/pkgs/collection/test/algorithms_test.dart @@ -0,0 +1,422 @@ +// Copyright (c) 2013, 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. + +/// Tests algorithm utilities. +library; + +import 'dart:math'; + +import 'package:collection/collection.dart'; +import 'package:collection/src/algorithms.dart'; +import 'package:test/test.dart'; + +void main() { + void testShuffle(List list) { + var copy = list.toList(); + shuffle(list); + expect(const UnorderedIterableEquality().equals(list, copy), isTrue); + } + + test('Shuffle 0', () { + testShuffle([]); + }); + test('Shuffle 1', () { + testShuffle([1]); + }); + test('Shuffle 3', () { + testShuffle([1, 2, 3]); + }); + test('Shuffle 10', () { + testShuffle([1, 2, 3, 4, 5, 1, 3, 5, 7, 9]); + }); + test('Shuffle shuffles', () { + var l = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; + var c = l.toList(); + var count = 0; + for (;;) { + shuffle(l); + if (!const ListEquality().equals(c, l)) return; + // Odds of not changing the order should be one in ~ 16! ~= 2e+13. + // Repeat this 10 times, and the odds of accidentally shuffling to the + // same result every time is disappearingly tiny. + count++; + // If this happens even once, it's ok to report it. + print('Failed shuffle $count times'); + if (count == 10) fail("Shuffle didn't change order."); + } + }); + test('Shuffle sublist', () { + var l = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; + var c = l.toList(); + shuffle(l, 4, 12); + expect(const IterableEquality().equals(l.getRange(0, 4), c.getRange(0, 4)), + isTrue); + expect( + const IterableEquality().equals(l.getRange(12, 16), c.getRange(12, 16)), + isTrue); + expect( + const UnorderedIterableEquality() + .equals(l.getRange(4, 12), c.getRange(4, 12)), + isTrue); + }); + + test('binsearch0', () { + expect(binarySearch([], 2), equals(-1)); + }); + + test('binsearch1', () { + expect(binarySearch([5], 2), equals(-1)); + expect(binarySearch([5], 5), equals(0)); + expect(binarySearch([5], 7), equals(-1)); + }); + + test('binsearch3', () { + expect(binarySearch([0, 5, 10], -1), equals(-1)); + expect(binarySearch([0, 5, 10], 0), equals(0)); + expect(binarySearch([0, 5, 10], 2), equals(-1)); + expect(binarySearch([0, 5, 10], 5), equals(1)); + expect(binarySearch([0, 5, 10], 7), equals(-1)); + expect(binarySearch([0, 5, 10], 10), equals(2)); + expect(binarySearch([0, 5, 10], 12), equals(-1)); + }); + + test('binsearchCompare0', () { + expect(binarySearch([], C(2), compare: compareC), equals(-1)); + }); + + test('binsearchCompare1', () { + var l1 = [C(5)]; + expect(binarySearch(l1, C(2), compare: compareC), equals(-1)); + expect(binarySearch(l1, C(5), compare: compareC), equals(0)); + expect(binarySearch(l1, C(7), compare: compareC), equals(-1)); + }); + + test('binsearchCompare3', () { + var l3 = [C(0), C(5), C(10)]; + expect(binarySearch(l3, C(-1), compare: compareC), equals(-1)); + expect(binarySearch(l3, C(0), compare: compareC), equals(0)); + expect(binarySearch(l3, C(2), compare: compareC), equals(-1)); + expect(binarySearch(l3, C(5), compare: compareC), equals(1)); + expect(binarySearch(l3, C(7), compare: compareC), equals(-1)); + expect(binarySearch(l3, C(10), compare: compareC), equals(2)); + expect(binarySearch(l3, C(12), compare: compareC), equals(-1)); + }); + + test('lowerbound0', () { + expect(lowerBound([], 2), equals(0)); + }); + + test('lowerbound1', () { + expect(lowerBound([5], 2), equals(0)); + expect(lowerBound([5], 5), equals(0)); + expect(lowerBound([5], 7), equals(1)); + }); + + test('lowerbound3', () { + expect(lowerBound([0, 5, 10], -1), equals(0)); + expect(lowerBound([0, 5, 10], 0), equals(0)); + expect(lowerBound([0, 5, 10], 2), equals(1)); + expect(lowerBound([0, 5, 10], 5), equals(1)); + expect(lowerBound([0, 5, 10], 7), equals(2)); + expect(lowerBound([0, 5, 10], 10), equals(2)); + expect(lowerBound([0, 5, 10], 12), equals(3)); + }); + + test('lowerboundRepeat', () { + expect(lowerBound([5, 5, 5], 5), equals(0)); + expect(lowerBound([0, 5, 5, 5, 10], 5), equals(1)); + }); + + test('lowerboundCompare0', () { + expect(lowerBound([], C(2), compare: compareC), equals(0)); + }); + + test('lowerboundCompare1', () { + var l1 = [C(5)]; + expect(lowerBound(l1, C(2), compare: compareC), equals(0)); + expect(lowerBound(l1, C(5), compare: compareC), equals(0)); + expect(lowerBound(l1, C(7), compare: compareC), equals(1)); + }); + + test('lowerboundCompare3', () { + var l3 = [C(0), C(5), C(10)]; + expect(lowerBound(l3, C(-1), compare: compareC), equals(0)); + expect(lowerBound(l3, C(0), compare: compareC), equals(0)); + expect(lowerBound(l3, C(2), compare: compareC), equals(1)); + expect(lowerBound(l3, C(5), compare: compareC), equals(1)); + expect(lowerBound(l3, C(7), compare: compareC), equals(2)); + expect(lowerBound(l3, C(10), compare: compareC), equals(2)); + expect(lowerBound(l3, C(12), compare: compareC), equals(3)); + }); + + test('lowerboundCompareRepeat', () { + var l1 = [C(5), C(5), C(5)]; + var l2 = [C(0), C(5), C(5), C(5), C(10)]; + expect(lowerBound(l1, C(5), compare: compareC), equals(0)); + expect(lowerBound(l2, C(5), compare: compareC), equals(1)); + }); + + void testSort(String name, + void Function(List elements, [int? start, int? end]) sort) { + test('${name}Random', () { + var random = Random(); + for (var i = 0; i < 250; i += 10) { + var list = [ + for (var j = 0; j < i; j++) + random.nextInt(25) // Expect some equal elements. + ]; + sort(list); + for (var j = 1; j < i; j++) { + expect(list[j - 1], lessThanOrEqualTo(list[j])); + } + } + }); + + test('${name}SubRanges', () { + var l = [6, 5, 4, 3, 2, 1]; + sort(l, 2, 4); + expect(l, equals([6, 5, 3, 4, 2, 1])); + sort(l, 1, 1); + expect(l, equals([6, 5, 3, 4, 2, 1])); + sort(l, 4, 6); + expect(l, equals([6, 5, 3, 4, 1, 2])); + sort(l, 0, 2); + expect(l, equals([5, 6, 3, 4, 1, 2])); + sort(l, 0, 6); + expect(l, equals([1, 2, 3, 4, 5, 6])); + }); + + test('$name insertionSortSpecialCases', () { + var l = [6, 6, 6, 6, 6, 6]; + sort(l); + expect(l, equals([6, 6, 6, 6, 6, 6])); + + l = [6, 6, 3, 3, 0, 0]; + sort(l); + expect(l, equals([0, 0, 3, 3, 6, 6])); + }); + } + + int intId(int x) => x; + int intCompare(int a, int b) => a - b; + testSort('insertionSort', (list, [start, end]) { + insertionSortBy(list, intId, intCompare, start ?? 0, end ?? list.length); + }); + testSort('mergeSort compare', (list, [start, end]) { + mergeSort(list, + start: start ?? 0, end: end ?? list.length, compare: intCompare); + }); + testSort('mergeSort comparable', (list, [start, end]) { + mergeSort(list, start: start ?? 0, end: end ?? list.length); + }); + testSort('mergeSortBy', (list, [start, end]) { + mergeSortBy(list, intId, intCompare, start ?? 0, end ?? list.length); + }); + testSort('quickSort', (list, [start, end]) { + quickSort(list, intCompare, start ?? 0, end ?? list.length); + }); + testSort('quickSortBy', (list, [start, end]) { + quickSortBy(list, intId, intCompare, start ?? 0, end ?? list.length); + }); + test('MergeSortSpecialCases', () { + for (var size in [511, 512, 513]) { + // All equal. + var list = List.generate(size, (i) => OC(0, i)); + mergeSort(list); + for (var i = 0; i < size; i++) { + expect(list[i].order, equals(i)); + } + // All but one equal, first. + list[0] = OC(1, 0); + for (var i = 1; i < size; i++) { + list[i] = OC(0, i); + } + mergeSort(list); + for (var i = 0; i < size - 1; i++) { + expect(list[i].order, equals(i + 1)); + } + expect(list[size - 1].order, equals(0)); + + // All but one equal, last. + for (var i = 0; i < size - 1; i++) { + list[i] = OC(0, i); + } + list[size - 1] = OC(-1, size - 1); + mergeSort(list); + expect(list[0].order, equals(size - 1)); + for (var i = 1; i < size; i++) { + expect(list[i].order, equals(i - 1)); + } + + // Reversed. + for (var i = 0; i < size; i++) { + list[i] = OC(size - 1 - i, i); + } + mergeSort(list); + for (var i = 0; i < size; i++) { + expect(list[i].id, equals(i)); + expect(list[i].order, equals(size - 1 - i)); + } + } + }); + + void testSortBy( + String name, + void Function(List elements, K Function(T element) keyOf, + int Function(K a, K b) compare, + [int start, int end]) + sort) { + for (var n in [0, 1, 2, 10, 75, 250]) { + var name2 = name; + test('$name2: Same #$n', () { + var list = List.generate(n, (i) => OC(i, 0)); + // Should succeed. Bad implementations of, e.g., quicksort can diverge. + sort(list, _ocOrder, _compareInt); + }); + test('$name: Pre-sorted #$n', () { + var list = List.generate(n, (i) => OC(-i, i)); + var expected = list.toList(); + sort(list, _ocOrder, _compareInt); + // Elements have not moved. + expect(list, expected); + }); + test('$name: Reverse-sorted #$n', () { + var list = List.generate(n, (i) => OC(i, -i)); + sort(list, _ocOrder, _compareInt); + expectSorted(list, _ocOrder, _compareInt); + }); + test('$name: Random #$n', () { + var random = Random(); + var list = List.generate(n, (i) => OC(i, random.nextInt(n))); + sort(list, _ocOrder, _compareInt); + expectSorted(list, _ocOrder, _compareInt); + }); + test('$name: Sublist #$n', () { + var random = Random(); + var list = List.generate(n, (i) => OC(i, random.nextInt(n))); + var original = list.toList(); + var start = n ~/ 4; + var end = start * 3; + sort(list, _ocOrder, _compareInt, start, end); + expectSorted(list, _ocOrder, _compareInt, start, end); + expect(list.sublist(0, start), original.sublist(0, start)); + expect(list.sublist(end), original.sublist(end)); + }); + } + } + + testSortBy('insertionSort', insertionSortBy); + testSortBy('mergeSort', mergeSortBy); + testSortBy('quickSortBy', quickSortBy); + + test('MergeSortPreservesOrder', () { + var random = Random(); + // Small case where only insertion call is called, + // larger case where the internal moving insertion sort is used + // larger cases with multiple splittings, numbers just around a power of 2. + for (var size in [8, 50, 511, 512, 513]) { + // Class OC compares using id. + // With size elements with id's in the range 0..size/4, a number of + // collisions are guaranteed. These should be sorted so that the 'order' + // part of the objects are still in order. + var list = [ + for (var i = 0; i < size; i++) OC(random.nextInt(size >> 2), i) + ]; + mergeSort(list); + var prev = list[0]; + for (var i = 1; i < size; i++) { + var next = list[i]; + expect(prev.id, lessThanOrEqualTo(next.id)); + if (next.id == prev.id) { + expect(prev.order, lessThanOrEqualTo(next.order)); + } + prev = next; + } + // Reverse compare on part of list. + List copy = list.toList(); + var min = size >> 2; + var max = size - min; + mergeSort(list, + start: min, end: max, compare: (a, b) => b.compareTo(a)); + prev = list[min]; + for (var i = min + 1; i < max; i++) { + var next = list[i]; + expect(prev.id, greaterThanOrEqualTo(next.id)); + if (next.id == prev.id) { + expect(prev.order, lessThanOrEqualTo(next.order)); + } + prev = next; + } + // Equals on OC objects is identity, so this means the parts before min, + // and the parts after max, didn't change at all. + expect(list.sublist(0, min), equals(copy.sublist(0, min))); + expect(list.sublist(max), equals(copy.sublist(max))); + } + }); + + test('Reverse', () { + var l = [6, 5, 4, 3, 2, 1]; + reverse(l, 2, 4); + expect(l, equals([6, 5, 3, 4, 2, 1])); + reverse(l, 1, 1); + expect(l, equals([6, 5, 3, 4, 2, 1])); + reverse(l, 4, 6); + expect(l, equals([6, 5, 3, 4, 1, 2])); + reverse(l, 0, 2); + expect(l, equals([5, 6, 3, 4, 1, 2])); + reverse(l, 0, 6); + expect(l, equals([2, 1, 4, 3, 6, 5])); + }); + + test('mergeSort works when runtime generic is a subtype of the static type', + () { + // Regression test for https://github.com/dart-lang/collection/issues/317 + final length = 1000; // Larger than _mergeSortLimit + // Out of order list, with first half guaranteed to empty first during + // merge. + final list = [ + for (var i = 0; i < length / 2; i++) -i, + for (var i = 0; i < length / 2; i++) i + length, + ]; + expect(() => mergeSort(list), returnsNormally); + }); +} + +class C { + final int id; + C(this.id); +} + +int compareC(C one, C other) => one.id - other.id; + +/// Class naturally ordered by its first constructor argument. +class OC implements Comparable { + final int id; + final int order; + OC(this.id, this.order); + + @override + int compareTo(OC other) => id - other.id; + + @override + String toString() => 'OC[$id,$order]'; +} + +int _ocOrder(OC oc) => oc.order; + +int _compareInt(int a, int b) => a - b; + +/// Check that a list is sorted according to [compare] of [keyOf] of elements. +void expectSorted( + List list, K Function(T element) keyOf, int Function(K a, K b) compare, + [int start = 0, int? end]) { + end ??= list.length; + if (start == end) return; + var prev = keyOf(list[start]); + for (var i = start + 1; i < end; i++) { + var next = keyOf(list[i]); + expect(compare(prev, next), isNonPositive); + prev = next; + } +} diff --git a/pkgs/collection/test/analysis_options.yaml b/pkgs/collection/test/analysis_options.yaml new file mode 100644 index 00000000..899b0f33 --- /dev/null +++ b/pkgs/collection/test/analysis_options.yaml @@ -0,0 +1,8 @@ +include: ../analysis_options.yaml + +# Turn off the avoid_dynamic_calls lint for the test/ directory. +analyzer: + errors: + avoid_dynamic_calls: ignore + inference_failure_on_collection_literal: ignore + inference_failure_on_instance_creation: ignore diff --git a/pkgs/collection/test/boollist_test.dart b/pkgs/collection/test/boollist_test.dart new file mode 100644 index 00000000..da7b7364 --- /dev/null +++ b/pkgs/collection/test/boollist_test.dart @@ -0,0 +1,166 @@ +// Copyright (c) 2021, 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. + +// Tests for BoolList. + +import 'package:collection/collection.dart'; +import 'package:test/test.dart'; + +void main() { + bool generator(int index) { + if (index < 512) { + return index.isEven; + } + return false; + } + + test('BoolList()', () { + expect(BoolList(1024, fill: false), List.filled(1024, false)); + + expect(BoolList(1024, fill: true), List.filled(1024, true)); + }); + + test('BoolList.empty()', () { + expect(BoolList.empty(growable: true, capacity: 1024), []); + + expect(BoolList.empty(growable: false, capacity: 1024), []); + }); + + test('BoolList.generate()', () { + expect( + BoolList.generate(1024, generator), + List.generate(1024, generator), + ); + }); + + test('BoolList.of()', () { + var src = List.generate(1024, generator); + expect(BoolList.of(src), src); + }); + + group('[], []=', () { + test('RangeError', () { + var b = BoolList(1024, fill: false); + + expect(() { + // ignore: unnecessary_statements + b[-1]; + }, throwsRangeError); + + expect(() { + // ignore: unnecessary_statements + b[1024]; + }, throwsRangeError); + }); + + test('[], []=', () { + var b = BoolList(1024, fill: false); + + bool posVal; + for (var pos = 0; pos < 1024; ++pos) { + posVal = generator(pos); + b[pos] = posVal; + expect(b[pos], posVal, reason: 'at pos $pos'); + } + }); + }); + + group('length', () { + test('shrink length', () { + var b = BoolList(1024, fill: true, growable: true); + + b.length = 768; + expect(b, List.filled(768, true)); + + b.length = 128; + expect(b, List.filled(128, true)); + + b.length = 0; + expect(b, []); + }); + + test('expand from != 0', () { + var b = BoolList(256, fill: true, growable: true); + + b.length = 384; + expect(b, List.filled(384, false)..fillRange(0, 256, true)); + + b.length = 2048; + expect(b, List.filled(2048, false)..fillRange(0, 256, true)); + }); + + test('expand from = 0', () { + var b = BoolList(0, growable: true); + expect(b.length, 0); + + b.length = 256; + expect(b, List.filled(256, false)); + }); + + test('throw UnsupportedError', () { + expect(() { + BoolList(1024).length = 512; + }, throwsUnsupportedError); + }); + }); + + group('fillRange', () { + test('In one word', () { + expect( + BoolList(1024)..fillRange(32, 64, true), + List.filled(1024, false)..fillRange(32, 64, true), + ); + + expect( + // BoolList.filled constructor isn't used due internal usage of + // fillRange + BoolList.generate(1024, (i) => true)..fillRange(32, 64, false), + List.filled(1024, true)..fillRange(32, 64, false), + ); + }); + + test('In several words', () { + expect( + BoolList(1024)..fillRange(32, 128, true), + List.filled(1024, false)..fillRange(32, 128, true), + ); + + expect( + // BoolList.filled constructor isn't used due internal usage of + // fillRange + BoolList.generate(1024, (i) => true)..fillRange(32, 128, false), + List.filled(1024, true)..fillRange(32, 128, false), + ); + }); + }); + + group('Iterator', () { + test('Iterator', () { + var b = BoolList.generate(1024, generator); + var iter = b.iterator; + + expect(iter.current, false); + for (var i = 0; i < 1024; i++) { + expect(iter.moveNext(), true); + + expect(iter.current, generator(i), reason: 'at pos $i'); + } + + expect(iter.moveNext(), false); + expect(iter.current, false); + }); + + test('throw ConcurrentModificationError', () { + var b = BoolList(1024, fill: true, growable: true); + + var iter = b.iterator; + + iter.moveNext(); + b.length = 512; + expect(() { + iter.moveNext(); + }, throwsConcurrentModificationError); + }); + }); +} diff --git a/pkgs/collection/test/canonicalized_map_test.dart b/pkgs/collection/test/canonicalized_map_test.dart new file mode 100644 index 00000000..aadb7346 --- /dev/null +++ b/pkgs/collection/test/canonicalized_map_test.dart @@ -0,0 +1,282 @@ +// Copyright (c) 2014, 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('with an empty canonicalized map', () { + late CanonicalizedMap map; + + setUp(() { + map = CanonicalizedMap(int.parse, isValidKey: RegExp(r'^\d+$').hasMatch); + }); + + test('canonicalizes keys on set and get', () { + map['1'] = 'value'; + expect(map['01'], equals('value')); + }); + + test('get returns null for uncanonicalizable key', () { + expect(map['foo'], isNull); + }); + + test('set affects nothing for uncanonicalizable key', () { + map['foo'] = 'value'; + expect(map['foo'], isNull); + expect(map.containsKey('foo'), isFalse); + expect(map.length, equals(0)); + }); + + test('canonicalizes keys for addAll', () { + map.addAll({'1': 'value 1', '2': 'value 2', '3': 'value 3'}); + expect(map['01'], equals('value 1')); + expect(map['02'], equals('value 2')); + expect(map['03'], equals('value 3')); + }); + + test('uses the final value for addAll collisions', () { + map.addAll({'1': 'value 1', '01': 'value 2', '001': 'value 3'}); + expect(map.length, equals(1)); + expect(map['0001'], equals('value 3')); + }); + + test('clear clears the map', () { + map.addAll({'1': 'value 1', '2': 'value 2', '3': 'value 3'}); + expect(map, isNot(isEmpty)); + map.clear(); + expect(map, isEmpty); + }); + + test('canonicalizes keys for containsKey', () { + map['1'] = 'value'; + expect(map.containsKey('01'), isTrue); + expect(map.containsKey('2'), isFalse); + }); + + test('containsKey returns false for uncanonicalizable key', () { + expect(map.containsKey('foo'), isFalse); + }); + + test('canonicalizes keys for putIfAbsent', () { + map['1'] = 'value'; + expect(map.putIfAbsent('01', () => throw Exception("shouldn't run")), + equals('value')); + expect(map.putIfAbsent('2', () => 'new value'), equals('new value')); + }); + + test('canonicalizes keys for remove', () { + map['1'] = 'value'; + expect(map.remove('2'), isNull); + expect(map.remove('01'), equals('value')); + expect(map, isEmpty); + }); + + test('remove returns null for uncanonicalizable key', () { + expect(map.remove('foo'), isNull); + }); + + test('containsValue returns whether a value is in the map', () { + map['1'] = 'value'; + expect(map.containsValue('value'), isTrue); + expect(map.containsValue('not value'), isFalse); + }); + + test('isEmpty returns whether the map is empty', () { + expect(map.isEmpty, isTrue); + map['1'] = 'value'; + expect(map.isEmpty, isFalse); + map.remove('01'); + expect(map.isEmpty, isTrue); + }); + + test("isNotEmpty returns whether the map isn't empty", () { + expect(map.isNotEmpty, isFalse); + map['1'] = 'value'; + expect(map.isNotEmpty, isTrue); + map.remove('01'); + expect(map.isNotEmpty, isFalse); + }); + + test('length returns the number of pairs in the map', () { + expect(map.length, equals(0)); + map['1'] = 'value 1'; + expect(map.length, equals(1)); + map['01'] = 'value 01'; + expect(map.length, equals(1)); + map['02'] = 'value 02'; + expect(map.length, equals(2)); + }); + + test('uses original keys for keys', () { + map['001'] = 'value 1'; + map['02'] = 'value 2'; + expect(map.keys, equals(['001', '02'])); + }); + + test('uses original keys for forEach', () { + map['001'] = 'value 1'; + map['02'] = 'value 2'; + + var keys = []; + map.forEach((key, value) => keys.add(key)); + expect(keys, equals(['001', '02'])); + }); + + test('values returns all values in the map', () { + map.addAll( + {'1': 'value 1', '01': 'value 01', '2': 'value 2', '03': 'value 03'}); + + expect(map.values, equals(['value 01', 'value 2', 'value 03'])); + }); + + test('entries returns all key-value pairs in the map', () { + map.addAll({ + '1': 'value 1', + '01': 'value 01', + '2': 'value 2', + }); + + var entries = map.entries.toList(); + expect(entries[0].key, '01'); + expect(entries[0].value, 'value 01'); + expect(entries[1].key, '2'); + expect(entries[1].value, 'value 2'); + }); + + test('addEntries adds key-value pairs to the map', () { + map.addEntries([ + const MapEntry('1', 'value 1'), + const MapEntry('01', 'value 01'), + const MapEntry('2', 'value 2'), + ]); + expect(map, {'01': 'value 01', '2': 'value 2'}); + }); + + test('cast returns a new map instance', () { + expect(map.cast(), isNot(same(map))); + }); + }); + + group('CanonicalizedMap builds an informative string representation', () { + dynamic map; + setUp(() { + map = CanonicalizedMap(int.parse, + isValidKey: RegExp(r'^\d+$').hasMatch); + }); + + test('for an empty map', () { + expect(map.toString(), equals('{}')); + }); + + test('for a map with one value', () { + map.addAll({'1': 'value 1'}); + expect(map.toString(), equals('{1: value 1}')); + }); + + test('for a map with multiple values', () { + map.addAll( + {'1': 'value 1', '01': 'value 01', '2': 'value 2', '03': 'value 03'}); + expect( + map.toString(), equals('{01: value 01, 2: value 2, 03: value 03}')); + }); + + test('for a map with a loop', () { + map.addAll({'1': 'value 1', '2': map}); + expect(map.toString(), equals('{1: value 1, 2: {...}}')); + }); + }); + + group('CanonicalizedMap.from', () { + test('canonicalizes its keys', () { + var map = CanonicalizedMap.from( + {'1': 'value 1', '2': 'value 2', '3': 'value 3'}, int.parse); + expect(map['01'], equals('value 1')); + expect(map['02'], equals('value 2')); + expect(map['03'], equals('value 3')); + }); + + test('uses the final value for collisions', () { + var map = CanonicalizedMap.from( + {'1': 'value 1', '01': 'value 2', '001': 'value 3'}, int.parse); + expect(map.length, equals(1)); + expect(map['0001'], equals('value 3')); + }); + }); + + group('CanonicalizedMap.fromEntries', () { + test('canonicalizes its keys', () { + var map = CanonicalizedMap.fromEntries( + {'1': 'value 1', '2': 'value 2', '3': 'value 3'}.entries, int.parse); + expect(map['01'], equals('value 1')); + expect(map['02'], equals('value 2')); + expect(map['03'], equals('value 3')); + }); + + test('uses the final value for collisions', () { + var map = CanonicalizedMap.fromEntries( + {'1': 'value 1', '01': 'value 2', '001': 'value 3'}.entries, + int.parse); + expect(map.length, equals(1)); + expect(map['0001'], equals('value 3')); + }); + }); + + group('CanonicalizedMap.toMapOfCanonicalKeys', () { + test('convert to a `Map`', () { + var map = CanonicalizedMap.from( + {'1': 'value 1', '2': 'value 2', '3': 'value 3'}, int.parse); + + var map2 = map.toMapOfCanonicalKeys(); + + expect(map2, isNot(isA())); + + expect(map2[1], equals('value 1')); + expect(map2[2], equals('value 2')); + expect(map2[3], equals('value 3')); + + expect(map2, equals({1: 'value 1', 2: 'value 2', 3: 'value 3'})); + }); + }); + + group('CanonicalizedMap.toMap', () { + test('convert to a `Map`', () { + var map = CanonicalizedMap.from( + {'1': 'value 1', '2': 'value 2', '3': 'value 3'}, int.parse); + + var map2 = map.toMap(); + + expect(map2, isNot(isA())); + + expect(map2['1'], equals('value 1')); + expect(map2['2'], equals('value 2')); + expect(map2['3'], equals('value 3')); + + expect(map2, equals({'1': 'value 1', '2': 'value 2', '3': 'value 3'})); + }); + }); + + group('CanonicalizedMap.copy', () { + test('copy instance', () { + var map = CanonicalizedMap.from( + {'1': 'value 1', '2': 'value 2', '3': 'value 3'}, int.parse); + + var map2 = map.copy(); + + expect(map2['01'], equals('value 1')); + expect(map2['02'], equals('value 2')); + expect(map2['03'], equals('value 3')); + + expect(map2['1'], equals('value 1')); + expect(map2['2'], equals('value 2')); + expect(map2['3'], equals('value 3')); + + expect(map2, equals({'1': 'value 1', '2': 'value 2', '3': 'value 3'})); + + var map3 = Map.fromEntries(map2.entries); + + expect(map3, equals({'1': 'value 1', '2': 'value 2', '3': 'value 3'})); + }); + }); +} diff --git a/pkgs/collection/test/combined_wrapper/iterable_test.dart b/pkgs/collection/test/combined_wrapper/iterable_test.dart new file mode 100644 index 00000000..d5c90bfe --- /dev/null +++ b/pkgs/collection/test/combined_wrapper/iterable_test.dart @@ -0,0 +1,60 @@ +// Copyright (c) 2017, 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() { + var iterable1 = Iterable.generate(3); + var iterable2 = Iterable.generate(3, (i) => i + 3); + var iterable3 = Iterable.generate(3, (i) => i + 6); + + test('should combine multiple iterables when iterating', () { + var combined = CombinedIterableView([iterable1, iterable2, iterable3]); + expect(combined, [0, 1, 2, 3, 4, 5, 6, 7, 8]); + }); + + test('should combine multiple iterables with some empty ones', () { + var combined = + CombinedIterableView([iterable1, [], iterable2, [], iterable3, []]); + expect(combined, [0, 1, 2, 3, 4, 5, 6, 7, 8]); + }); + + test('should function as an empty iterable when no iterables are passed', () { + var empty = const CombinedIterableView([]); + expect(empty, isEmpty); + }); + + test('should function as an empty iterable with all empty iterables', () { + var empty = const CombinedIterableView([[], [], []]); + expect(empty, isEmpty); + }); + + test('should reflect changes from the underlying iterables', () { + var list1 = []; + var list2 = []; + var combined = CombinedIterableView([list1, list2]); + expect(combined, isEmpty); + list1.addAll([1, 2]); + list2.addAll([3, 4]); + expect(combined, [1, 2, 3, 4]); + expect(combined.last, 4); + expect(combined.first, 1); + }); + + test('should reflect changes from the iterable of iterables', () { + var iterables = []; + var combined = CombinedIterableView(iterables); + expect(combined, isEmpty); + expect(combined, hasLength(0)); + + iterables.add(iterable1); + expect(combined, isNotEmpty); + expect(combined, hasLength(3)); + + iterables.clear(); + expect(combined, isEmpty); + expect(combined, hasLength(0)); + }); +} diff --git a/pkgs/collection/test/combined_wrapper/list_test.dart b/pkgs/collection/test/combined_wrapper/list_test.dart new file mode 100644 index 00000000..2705e391 --- /dev/null +++ b/pkgs/collection/test/combined_wrapper/list_test.dart @@ -0,0 +1,67 @@ +// Copyright (c) 2017, 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'; + +import '../unmodifiable_collection_test.dart' as common; + +void main() { + var list1 = [1, 2, 3]; + var list2 = [4, 5, 6]; + var list3 = [7, 8, 9]; + var concat = [...list1, ...list2, ...list3]; + + // In every way possible this should test the same as an UnmodifiableListView. + common.testUnmodifiableList( + concat, CombinedListView([list1, list2, list3]), 'combineLists'); + + common.testUnmodifiableList(concat, + CombinedListView([list1, [], list2, [], list3, []]), 'combineLists'); + + test('should function as an empty list when no lists are passed', () { + var empty = CombinedListView([]); + expect(empty, isEmpty); + expect(empty.length, 0); + expect(() => empty[0], throwsRangeError); + }); + + test('should function as an empty list when only empty lists are passed', () { + var empty = CombinedListView([[], [], []]); + expect(empty, isEmpty); + expect(empty.length, 0); + expect(() => empty[0], throwsRangeError); + }); + + test('should reflect underlying changes back to the combined list', () { + var backing1 = []; + var backing2 = []; + var combined = CombinedListView([backing1, backing2]); + expect(combined, isEmpty); + backing1.addAll(list1); + expect(combined, list1); + backing2.addAll(list2); + expect(combined, backing1.toList()..addAll(backing2)); + }); + + test('should reflect underlying changes from the list of lists', () { + var listOfLists = >[]; + var combined = CombinedListView(listOfLists); + expect(combined, isEmpty); + listOfLists.add(list1); + expect(combined, list1); + listOfLists.add(list2); + expect(combined, [...list1, ...list2]); + listOfLists.clear(); + expect(combined, isEmpty); + }); + + test('should reflect underlying changes with a single list', () { + var backing1 = []; + var combined = CombinedListView([backing1]); + expect(combined, isEmpty); + backing1.addAll(list1); + expect(combined, list1); + }); +} diff --git a/pkgs/collection/test/combined_wrapper/map_test.dart b/pkgs/collection/test/combined_wrapper/map_test.dart new file mode 100644 index 00000000..9b9a1a83 --- /dev/null +++ b/pkgs/collection/test/combined_wrapper/map_test.dart @@ -0,0 +1,118 @@ +// Copyright (c) 2017, 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 'dart:collection'; + +import 'package:collection/collection.dart'; +import 'package:test/test.dart'; + +void main() { + var map1 = const {1: 1, 2: 2, 3: 3}; + var map2 = const {4: 4, 5: 5, 6: 6}; + var map3 = const {7: 7, 8: 8, 9: 9}; + var map4 = const {1: -1, 2: -2, 3: -3}; + var concat = SplayTreeMap() + // The duplicates map appears first here but last in the CombinedMapView + // which has the opposite semantics of `concat`. Keys/values should be + // returned from the first map that contains them. + ..addAll(map4) + ..addAll(map1) + ..addAll(map2) + ..addAll(map3); + + // In every way possible this should test the same as an UnmodifiableMapView. + _testReadMap( + concat, CombinedMapView([map1, map2, map3, map4]), 'CombinedMapView'); + + _testReadMap( + concat, + CombinedMapView([map1, {}, map2, {}, map3, {}, map4, {}]), + 'CombinedMapView (some empty)'); + + test('should function as an empty map when no maps are passed', () { + var empty = CombinedMapView([]); + expect(empty, isEmpty); + expect(empty.length, 0); + }); + + test('should function as an empty map when only empty maps are passed', () { + var empty = CombinedMapView([{}, {}, {}]); + expect(empty, isEmpty); + expect(empty.length, 0); + }); + + test('should reflect underlying changes back to the combined map', () { + var backing1 = {}; + var backing2 = {}; + var combined = CombinedMapView([backing1, backing2]); + expect(combined, isEmpty); + backing1.addAll(map1); + expect(combined, map1); + backing2.addAll(map2); + expect(combined, Map.from(backing1)..addAll(backing2)); + }); + + test('should reflect underlying changes with a single map', () { + var backing1 = {}; + var combined = CombinedMapView([backing1]); + expect(combined, isEmpty); + backing1.addAll(map1); + expect(combined, map1); + }); + + test('re-iterating keys produces same result', () { + var combined = CombinedMapView([map1, map2, map3, map4]); + var keys = combined.keys; + expect(keys.toList(), keys.toList()); + }); +} + +void _testReadMap(Map original, Map wrapped, String name) { + test('$name length', () { + expect(wrapped.length, equals(original.length)); + }); + + test('$name isEmpty', () { + expect(wrapped.isEmpty, equals(original.isEmpty)); + }); + + test('$name isNotEmpty', () { + expect(wrapped.isNotEmpty, equals(original.isNotEmpty)); + }); + + test('$name operator[]', () { + expect(wrapped[0], equals(original[0])); + expect(wrapped[999], equals(original[999])); + }); + + test('$name containsKey', () { + expect(wrapped.containsKey(0), equals(original.containsKey(0))); + expect(wrapped.containsKey(999), equals(original.containsKey(999))); + }); + + test('$name containsValue', () { + expect(wrapped.containsValue(0), equals(original.containsValue(0))); + expect(wrapped.containsValue(999), equals(original.containsValue(999))); + }); + + test('$name forEach', () { + var origCnt = 0; + var wrapCnt = 0; + wrapped.forEach((k, v) { + wrapCnt += 1 << k + 3 * v; + }); + original.forEach((k, v) { + origCnt += 1 << k + 3 * v; + }); + expect(wrapCnt, equals(origCnt)); + }); + + test('$name keys', () { + expect(wrapped.keys, orderedEquals(original.keys)); + }); + + test('$name values', () { + expect(wrapped.values, orderedEquals(original.values)); + }); +} diff --git a/pkgs/collection/test/comparators_test.dart b/pkgs/collection/test/comparators_test.dart new file mode 100644 index 00000000..ab487116 --- /dev/null +++ b/pkgs/collection/test/comparators_test.dart @@ -0,0 +1,121 @@ +// Copyright (c) 2015, 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() { + var strings = [ + '', + '\x00', + ' ', + '+', + '/', + '0', + '00', + '000', + '001', + '01', + '011', + '1', + '100', + '11', + '110', + '9', + ':', + '=', + '@', + 'A', + 'A0', + 'A000A', + 'A001A', + 'A00A', + 'A01A', + 'A0A', + 'A1A', + 'AA', + 'AAB', + 'AB', + 'Z', + '[', + '_', + '`', + 'a', + 'a0', + 'a000a', + 'a001a', + 'a00a', + 'a01a', + 'a0a', + 'a1a', + 'aa', + 'aab', + 'ab', + 'z', + '{', + '~' + ]; + + List sortedBy(int Function(String, String)? compare) => + strings.toList() + ..shuffle() + ..sort(compare); + + test('String.compareTo', () { + expect(sortedBy(null), strings); + }); + + test('compareAsciiLowerCase', () { + expect(sortedBy(compareAsciiLowerCase), sortedBy((a, b) { + var delta = a.toLowerCase().compareTo(b.toLowerCase()); + if (delta != 0) return delta; + if (a == b) return 0; + return a.compareTo(b); + })); + }); + + test('compareAsciiUpperCase', () { + expect(sortedBy(compareAsciiUpperCase), sortedBy((a, b) { + var delta = a.toUpperCase().compareTo(b.toUpperCase()); + if (delta != 0) return delta; + if (a == b) return 0; + return a.compareTo(b); + })); + }); + + // Replace any digit sequence by ("0", value, length) as char codes. + // This will sort alphabetically (by charcode) the way digits sort + // numerically, and the leading 0 means it sorts like a digit + // compared to non-digits. + String replaceNumbers(String string) => + string.replaceAllMapped(RegExp(r'\d+'), (m) { + var digits = m[0]!; + return String.fromCharCodes([0x30, int.parse(digits), digits.length]); + }); + + test('compareNatural', () { + expect(sortedBy(compareNatural), + sortedBy((a, b) => replaceNumbers(a).compareTo(replaceNumbers(b)))); + }); + + test('compareAsciiLowerCaseNatural', () { + expect(sortedBy(compareAsciiLowerCaseNatural), sortedBy((a, b) { + var delta = replaceNumbers(a.toLowerCase()) + .compareTo(replaceNumbers(b.toLowerCase())); + if (delta != 0) return delta; + if (a == b) return 0; + return a.compareTo(b); + })); + }); + + test('compareAsciiUpperCaseNatural', () { + expect(sortedBy(compareAsciiUpperCaseNatural), sortedBy((a, b) { + var delta = replaceNumbers(a.toUpperCase()) + .compareTo(replaceNumbers(b.toUpperCase())); + if (delta != 0) return delta; + if (a == b) return 0; + return a.compareTo(b); + })); + }); +} diff --git a/pkgs/collection/test/equality_map_test.dart b/pkgs/collection/test/equality_map_test.dart new file mode 100644 index 00000000..d3aa9fc9 --- /dev/null +++ b/pkgs/collection/test/equality_map_test.dart @@ -0,0 +1,37 @@ +// Copyright (c) 2016, 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() { + test('uses the given equality', () { + var map = EqualityMap(const IterableEquality()); + expect(map, isEmpty); + + map[[1, 2, 3]] = 1; + expect(map, containsPair([1, 2, 3], 1)); + + map[[1, 2, 3]] = 2; + expect(map, containsPair([1, 2, 3], 2)); + + map[[2, 3, 4]] = 3; + expect(map, containsPair([1, 2, 3], 2)); + expect(map, containsPair([2, 3, 4], 3)); + }); + + test('EqualityMap.from() prefers the lattermost equivalent key', () { + var map = EqualityMap.from(const IterableEquality(), { + [1, 2, 3]: 1, + [2, 3, 4]: 2, + [1, 2, 3]: 3, + [2, 3, 4]: 4, + [1, 2, 3]: 5, + [1, 2, 3]: 6, + }); + + expect(map, containsPair([1, 2, 3], 6)); + expect(map, containsPair([2, 3, 4], 4)); + }); +} diff --git a/pkgs/collection/test/equality_set_test.dart b/pkgs/collection/test/equality_set_test.dart new file mode 100644 index 00000000..1022726b --- /dev/null +++ b/pkgs/collection/test/equality_set_test.dart @@ -0,0 +1,48 @@ +// Copyright (c) 2016, 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() { + test('uses the given equality', () { + var set = EqualitySet(const IterableEquality()); + expect(set, isEmpty); + + var list1 = [1, 2, 3]; + expect(set.add(list1), isTrue); + expect(set, contains([1, 2, 3])); + expect(set, contains(same(list1))); + + var list2 = [1, 2, 3]; + expect(set.add(list2), isFalse); + expect(set, contains([1, 2, 3])); + expect(set, contains(same(list1))); + expect(set, isNot(contains(same(list2)))); + + var list3 = [2, 3, 4]; + expect(set.add(list3), isTrue); + expect(set, contains(same(list1))); + expect(set, contains(same(list3))); + }); + + test('EqualitySet.from() prefers the lattermost equivalent value', () { + var list1 = [1, 2, 3]; + var list2 = [2, 3, 4]; + var list3 = [1, 2, 3]; + var list4 = [2, 3, 4]; + var list5 = [1, 2, 3]; + var list6 = [1, 2, 3]; + + var set = EqualitySet.from( + const IterableEquality(), [list1, list2, list3, list4, list5, list6]); + + expect(set, contains(same(list1))); + expect(set, contains(same(list2))); + expect(set, isNot(contains(same(list3)))); + expect(set, isNot(contains(same(list4)))); + expect(set, isNot(contains(same(list5)))); + expect(set, isNot(contains(same(list6)))); + }); +} diff --git a/pkgs/collection/test/equality_test.dart b/pkgs/collection/test/equality_test.dart new file mode 100644 index 00000000..ce58df64 --- /dev/null +++ b/pkgs/collection/test/equality_test.dart @@ -0,0 +1,282 @@ +// Copyright (c) 2013, 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. + +// Tests equality utilities. + +import 'dart:collection'; + +import 'package:collection/collection.dart'; +import 'package:test/test.dart'; + +void main() { + Element o(Comparable id) => Element(id); + + // Lists that are point-wise equal, but not identical. + var list1 = [o(1), o(2), o(3), o(4), o(5)]; + var list2 = [o(1), o(2), o(3), o(4), o(5)]; + // Similar length list with equal elements in different order. + var list3 = [o(1), o(3), o(5), o(4), o(2)]; + + test('IterableEquality - List', () { + expect(const IterableEquality().equals(list1, list2), isTrue); + Equality iterId = const IterableEquality(IdentityEquality()); + expect(iterId.equals(list1, list2), isFalse); + }); + + test('IterableEquality - LinkedSet', () { + var l1 = LinkedHashSet.from(list1); + var l2 = LinkedHashSet.from(list2); + expect(const IterableEquality().equals(l1, l2), isTrue); + Equality iterId = const IterableEquality(IdentityEquality()); + expect(iterId.equals(l1, l2), isFalse); + }); + + test('ListEquality', () { + expect(const ListEquality().equals(list1, list2), isTrue); + Equality listId = const ListEquality(IdentityEquality()); + expect(listId.equals(list1, list2), isFalse); + }); + + test('ListInequality length', () { + var list4 = [o(1), o(2), o(3), o(4), o(5), o(6)]; + expect(const ListEquality().equals(list1, list4), isFalse); + expect( + const ListEquality(IdentityEquality()).equals(list1, list4), isFalse); + }); + + test('ListInequality value', () { + var list5 = [o(1), o(2), o(3), o(4), o(6)]; + expect(const ListEquality().equals(list1, list5), isFalse); + expect( + const ListEquality(IdentityEquality()).equals(list1, list5), isFalse); + }); + + test('UnorderedIterableEquality', () { + expect(const UnorderedIterableEquality().equals(list1, list3), isTrue); + Equality uniterId = const UnorderedIterableEquality(IdentityEquality()); + expect(uniterId.equals(list1, list3), isFalse); + }); + + test('UnorderedIterableInequality length', () { + var list6 = [o(1), o(3), o(5), o(4), o(2), o(1)]; + expect(const UnorderedIterableEquality().equals(list1, list6), isFalse); + expect( + const UnorderedIterableEquality(IdentityEquality()) + .equals(list1, list6), + isFalse); + }); + + test('UnorderedIterableInequality values', () { + var list7 = [o(1), o(3), o(5), o(4), o(6)]; + expect(const UnorderedIterableEquality().equals(list1, list7), isFalse); + expect( + const UnorderedIterableEquality(IdentityEquality()) + .equals(list1, list7), + isFalse); + }); + + test('SetEquality', () { + var set1 = HashSet.from(list1); + var set2 = LinkedHashSet.from(list3); + expect(const SetEquality().equals(set1, set2), isTrue); + Equality setId = const SetEquality(IdentityEquality()); + expect(setId.equals(set1, set2), isFalse); + }); + + test('SetInequality length', () { + var list8 = [o(1), o(3), o(5), o(4), o(2), o(6)]; + var set1 = HashSet.from(list1); + var set2 = LinkedHashSet.from(list8); + expect(const SetEquality().equals(set1, set2), isFalse); + expect(const SetEquality(IdentityEquality()).equals(set1, set2), isFalse); + }); + + test('SetInequality value', () { + var list7 = [o(1), o(3), o(5), o(4), o(6)]; + var set1 = HashSet.from(list1); + var set2 = LinkedHashSet.from(list7); + expect(const SetEquality().equals(set1, set2), isFalse); + expect(const SetEquality(IdentityEquality()).equals(set1, set2), isFalse); + }); + + var map1a = { + 'x': [o(1), o(2), o(3)], + 'y': [true, false, null] + }; + var map1b = { + 'x': [o(4), o(5), o(6)], + 'y': [false, true, null] + }; + var map2a = { + 'x': [o(3), o(2), o(1)], + 'y': [false, true, null] + }; + var map2b = { + 'x': [o(6), o(5), o(4)], + 'y': [null, false, true] + }; + var l1 = [map1a, map1b]; + var l2 = [map2a, map2b]; + var s1 = {...l1}; + var s2 = {map2b, map2a}; + + var i1 = Iterable.generate(l1.length, (i) => l1[i]); + + test('RecursiveEquality', () { + const unordered = UnorderedIterableEquality(); + expect(unordered.equals(map1a['x'], map2a['x']), isTrue); + expect(unordered.equals(map1a['y'], map2a['y']), isTrue); + expect(unordered.equals(map1b['x'], map2b['x']), isTrue); + expect(unordered.equals(map1b['y'], map2b['y']), isTrue); + const mapval = MapEquality(values: unordered); + expect(mapval.equals(map1a, map2a), isTrue); + expect(mapval.equals(map1b, map2b), isTrue); + const listmapval = ListEquality(mapval); + expect(listmapval.equals(l1, l2), isTrue); + const setmapval = SetEquality(mapval); + expect(setmapval.equals(s1, s2), isTrue); + }); + + group('DeepEquality', () { + group('unordered', () { + var colleq = const DeepCollectionEquality.unordered(); + + test('with identical collection types', () { + expect(colleq.equals(map1a['x'], map2a['x']), isTrue); + expect(colleq.equals(map1a['y'], map2a['y']), isTrue); + expect(colleq.equals(map1b['x'], map2b['x']), isTrue); + expect(colleq.equals(map1b['y'], map2b['y']), isTrue); + expect(colleq.equals(map1a, map2a), isTrue); + expect(colleq.equals(map1b, map2b), isTrue); + expect(colleq.equals(l1, l2), isTrue); + expect(colleq.equals(s1, s2), isTrue); + }); + + // TODO: https://github.com/dart-lang/collection/issues/208 + test('comparing collections and iterables', () { + expect(colleq.equals(l1, i1), isFalse); + expect(colleq.equals(i1, l1), isFalse); + expect(colleq.equals(s1, i1), isFalse); + expect(colleq.equals(i1, s1), isTrue); + }); + }); + + group('ordered', () { + var colleq = const DeepCollectionEquality(); + + test('with identical collection types', () { + expect(colleq.equals(l1, l1.toList()), isTrue); + expect(colleq.equals(s1, s1.toSet()), isTrue); + expect(colleq.equals(map1b, map1b.map(MapEntry.new)), isTrue); + expect(colleq.equals(i1, i1.map((i) => i)), isTrue); + expect(colleq.equals(map1a, map2a), isFalse); + expect(colleq.equals(map1b, map2b), isFalse); + expect(colleq.equals(l1, l2), isFalse); + expect(colleq.equals(s1, s2), isFalse); + }); + + // TODO: https://github.com/dart-lang/collection/issues/208 + test('comparing collections and iterables', () { + expect(colleq.equals(l1, i1), isFalse); + expect(colleq.equals(i1, l1), isTrue); + expect(colleq.equals(s1, i1), isFalse); + expect(colleq.equals(i1, s1), isTrue); + }); + }); + }); + + test('CaseInsensitiveEquality', () { + var equality = const CaseInsensitiveEquality(); + expect(equality.equals('foo', 'foo'), isTrue); + expect(equality.equals('fOo', 'FoO'), isTrue); + expect(equality.equals('FoO', 'fOo'), isTrue); + expect(equality.equals('foo', 'bar'), isFalse); + expect(equality.equals('fÕÕ', 'fõõ'), isFalse); + + expect(equality.hash('foo'), equals(equality.hash('foo'))); + expect(equality.hash('fOo'), equals(equality.hash('FoO'))); + expect(equality.hash('FoO'), equals(equality.hash('fOo'))); + expect(equality.hash('foo'), isNot(equals(equality.hash('bar')))); + expect(equality.hash('fÕÕ'), isNot(equals(equality.hash('fõõ')))); + }); + + group('EqualityBy should use a derived value for ', () { + var firstEquality = EqualityBy, String>((e) => e.first); + var firstInsensitiveEquality = EqualityBy, String>( + (e) => e.first, const CaseInsensitiveEquality()); + var firstObjectEquality = EqualityBy, Object>( + (e) => e.first, const IterableEquality()); + + test('equality', () { + expect(firstEquality.equals(['foo', 'foo'], ['foo', 'bar']), isTrue); + expect(firstEquality.equals(['foo', 'foo'], ['bar', 'bar']), isFalse); + }); + + test('equality with an inner equality', () { + expect(firstInsensitiveEquality.equals(['fOo'], ['FoO']), isTrue); + expect(firstInsensitiveEquality.equals(['foo'], ['ffõõ']), isFalse); + }); + + test('hash', () { + expect(firstEquality.hash(['foo', 'bar']), 'foo'.hashCode); + }); + + test('hash with an inner equality', () { + expect(firstInsensitiveEquality.hash(['fOo']), + const CaseInsensitiveEquality().hash('foo')); + }); + + test('isValidKey', () { + expect(firstEquality.isValidKey(['foo']), isTrue); + expect(firstEquality.isValidKey('foo'), isFalse); + expect(firstEquality.isValidKey([1]), isFalse); + }); + + test('isValidKey with an inner equality', () { + expect(firstObjectEquality.isValidKey([[]]), isTrue); + expect(firstObjectEquality.isValidKey([{}]), isFalse); + }); + }); + + test('Equality accepts null', () { + var ie = const IterableEquality(); + var le = const ListEquality(); + var se = const SetEquality(); + var me = const MapEquality(); + expect(ie.equals(null, null), true); + expect(ie.equals([], null), false); + expect(ie.equals(null, []), false); + expect(ie.hash(null), null.hashCode); + + expect(le.equals(null, null), true); + expect(le.equals([], null), false); + expect(le.equals(null, []), false); + expect(le.hash(null), null.hashCode); + + expect(se.equals(null, null), true); + expect(se.equals({}, null), false); + expect(se.equals(null, {}), false); + expect(se.hash(null), null.hashCode); + + expect(me.equals(null, null), true); + expect(me.equals({}, null), false); + expect(me.equals(null, {}), false); + expect(me.hash(null), null.hashCode); + }); +} + +/// Wrapper objects for an `id` value. +/// +/// Compares the `id` value by equality and for comparison. +/// Allows creating simple objects that are equal without being identical. +class Element implements Comparable { + final Comparable id; + const Element(this.id); + @override + int get hashCode => id.hashCode; + @override + bool operator ==(Object other) => other is Element && id == other.id; + @override + int compareTo(Element other) => id.compareTo(other.id); +} diff --git a/pkgs/collection/test/extensions_test.dart b/pkgs/collection/test/extensions_test.dart new file mode 100644 index 00000000..9940e1d4 --- /dev/null +++ b/pkgs/collection/test/extensions_test.dart @@ -0,0 +1,2084 @@ +// Copyright (c) 2020, 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 'dart:math' show Random, pow; + +import 'package:collection/collection.dart'; +import 'package:test/test.dart'; + +void main() { + group('Iterable', () { + group('of any', () { + group('.whereNot', () { + test('empty', () { + expect(iterable([]).whereNot(unreachable), isEmpty); + }); + test('none', () { + expect(iterable([1, 3, 5]).whereNot((e) => e.isOdd), isEmpty); + }); + test('all', () { + expect(iterable([1, 3, 5]).whereNot((e) => e.isEven), + iterable([1, 3, 5])); + }); + test('some', () { + expect(iterable([1, 2, 3, 4]).whereNot((e) => e.isEven), + iterable([1, 3])); + }); + }); + group('.sorted', () { + test('empty', () { + expect(iterable([]).sorted(unreachable), []); + }); + test('singleton', () { + expect(iterable([1]).sorted(unreachable), [1]); + }); + test('multiple', () { + expect(iterable([5, 2, 4, 3, 1]).sorted(cmpInt), [1, 2, 3, 4, 5]); + }); + }); + group('.shuffled', () { + test('empty', () { + expect(iterable([]).shuffled(), []); + }); + test('singleton', () { + expect(iterable([1]).shuffled(), [1]); + }); + test('multiple', () { + var input = iterable([1, 2, 3, 4, 5]); + var copy = [...input]; + var shuffled = input.shuffled(); + expect(const UnorderedIterableEquality().equals(input, shuffled), + isTrue); + // Check that the original list isn't touched + expect(input, copy); + }); + }); + group('.sortedBy', () { + test('empty', () { + expect(iterable([]).sortedBy(unreachable), []); + }); + test('singleton', () { + expect(iterable([1]).sortedBy(unreachable), [1]); + }); + test('multiple', () { + expect(iterable([3, 20, 100]).sortedBy(toString), [100, 20, 3]); + }); + }); + group('.sortedByCompare', () { + test('empty', () { + expect( + iterable([]).sortedByCompare(unreachable, unreachable), []); + }); + test('singleton', () { + expect(iterable([2]).sortedByCompare(unreachable, unreachable), + [2]); + }); + test('multiple', () { + expect( + iterable([30, 2, 100]) + .sortedByCompare(toString, cmpParseInverse), + [100, 30, 2]); + }); + }); + group('isSorted', () { + test('empty', () { + expect(iterable([]).isSorted(unreachable), true); + }); + test('single', () { + expect(iterable([1]).isSorted(unreachable), true); + }); + test('same', () { + expect(iterable([1, 1, 1, 1]).isSorted(cmpInt), true); + expect(iterable([1, 0, 1, 0]).isSorted(cmpMod(2)), true); + }); + test('multiple', () { + expect(iterable([1, 2, 3, 4]).isSorted(cmpInt), true); + expect(iterable([4, 3, 2, 1]).isSorted(cmpIntInverse), true); + expect(iterable([1, 2, 3, 0]).isSorted(cmpInt), false); + expect(iterable([4, 1, 2, 3]).isSorted(cmpInt), false); + expect(iterable([4, 3, 2, 1]).isSorted(cmpInt), false); + }); + }); + group('.isSortedBy', () { + test('empty', () { + expect(iterable([]).isSortedBy(unreachable), true); + }); + test('single', () { + expect(iterable([1]).isSortedBy(toString), true); + }); + test('same', () { + expect(iterable([1, 1, 1, 1]).isSortedBy(toString), true); + }); + test('multiple', () { + expect(iterable([1, 2, 3, 4]).isSortedBy(toString), true); + expect(iterable([4, 3, 2, 1]).isSortedBy(toString), false); + expect(iterable([1000, 200, 30, 4]).isSortedBy(toString), true); + expect(iterable([1, 2, 3, 0]).isSortedBy(toString), false); + expect(iterable([4, 1, 2, 3]).isSortedBy(toString), false); + expect(iterable([4, 3, 2, 1]).isSortedBy(toString), false); + }); + }); + group('.isSortedByCompare', () { + test('empty', () { + expect(iterable([]).isSortedByCompare(unreachable, unreachable), + true); + }); + test('single', () { + expect(iterable([1]).isSortedByCompare(toString, unreachable), true); + }); + test('same', () { + expect(iterable([1, 1, 1, 1]).isSortedByCompare(toString, cmpParse), + true); + }); + test('multiple', () { + expect(iterable([1, 2, 3, 4]).isSortedByCompare(toString, cmpParse), + true); + expect( + iterable([4, 3, 2, 1]) + .isSortedByCompare(toString, cmpParseInverse), + true); + expect( + iterable([1000, 200, 30, 4]) + .isSortedByCompare(toString, cmpString), + true); + expect(iterable([1, 2, 3, 0]).isSortedByCompare(toString, cmpParse), + false); + expect(iterable([4, 1, 2, 3]).isSortedByCompare(toString, cmpParse), + false); + expect(iterable([4, 3, 2, 1]).isSortedByCompare(toString, cmpParse), + false); + }); + }); + group('.forEachIndexed', () { + test('empty', () { + iterable([]).forEachIndexed(unreachable); + }); + test('single', () { + var log = []; + iterable(['a']).forEachIndexed((i, s) { + log + ..add(i) + ..add(s); + }); + expect(log, [0, 'a']); + }); + test('multiple', () { + var log = []; + iterable(['a', 'b', 'c']).forEachIndexed((i, s) { + log + ..add(i) + ..add(s); + }); + expect(log, [0, 'a', 1, 'b', 2, 'c']); + }); + }); + group('.forEachWhile', () { + test('empty', () { + iterable([]).forEachWhile(unreachable); + }); + test('single true', () { + var log = []; + iterable(['a']).forEachWhile((s) { + log.add(s); + return true; + }); + expect(log, ['a']); + }); + test('single false', () { + var log = []; + iterable(['a']).forEachWhile((s) { + log.add(s); + return false; + }); + expect(log, ['a']); + }); + test('multiple one', () { + var log = []; + iterable(['a', 'b', 'c']).forEachWhile((s) { + log.add(s); + return false; + }); + expect(log, ['a']); + }); + test('multiple all', () { + var log = []; + iterable(['a', 'b', 'c']).forEachWhile((s) { + log.add(s); + return true; + }); + expect(log, ['a', 'b', 'c']); + }); + test('multiple some', () { + var log = []; + iterable(['a', 'b', 'c']).forEachWhile((s) { + log.add(s); + return s != 'b'; + }); + expect(log, ['a', 'b']); + }); + }); + group('.forEachIndexedWhile', () { + test('empty', () { + iterable([]).forEachIndexedWhile(unreachable); + }); + test('single true', () { + var log = []; + iterable(['a']).forEachIndexedWhile((i, s) { + log + ..add(i) + ..add(s); + return true; + }); + expect(log, [0, 'a']); + }); + test('single false', () { + var log = []; + iterable(['a']).forEachIndexedWhile((i, s) { + log + ..add(i) + ..add(s); + return false; + }); + expect(log, [0, 'a']); + }); + test('multiple one', () { + var log = []; + iterable(['a', 'b', 'c']).forEachIndexedWhile((i, s) { + log + ..add(i) + ..add(s); + return false; + }); + expect(log, [0, 'a']); + }); + test('multiple all', () { + var log = []; + iterable(['a', 'b', 'c']).forEachIndexedWhile((i, s) { + log + ..add(i) + ..add(s); + return true; + }); + expect(log, [0, 'a', 1, 'b', 2, 'c']); + }); + test('multiple some', () { + var log = []; + iterable(['a', 'b', 'c']).forEachIndexedWhile((i, s) { + log + ..add(i) + ..add(s); + return s != 'b'; + }); + expect(log, [0, 'a', 1, 'b']); + }); + }); + group('.mapIndexed', () { + test('empty', () { + expect(iterable([]).mapIndexed(unreachable), isEmpty); + }); + test('multiple', () { + expect(iterable(['a', 'b']).mapIndexed((i, s) => [i, s]), [ + [0, 'a'], + [1, 'b'] + ]); + }); + }); + group('.whereIndexed', () { + test('empty', () { + expect(iterable([]).whereIndexed(unreachable), isEmpty); + }); + test('none', () { + var trace = []; + int log(int a, int b) { + trace + ..add(a) + ..add(b); + return b; + } + + expect( + iterable([1, 3, 5, 7]) + .whereIndexed((i, x) => log(i, x).isEven), + isEmpty); + expect(trace, [0, 1, 1, 3, 2, 5, 3, 7]); + }); + test('all', () { + expect(iterable([1, 3, 5, 7]).whereIndexed((i, x) => x.isOdd), + [1, 3, 5, 7]); + }); + test('some', () { + expect(iterable([1, 3, 5, 7]).whereIndexed((i, x) => i.isOdd), + [3, 7]); + }); + }); + group('.whereNotIndexed', () { + test('empty', () { + expect(iterable([]).whereNotIndexed(unreachable), isEmpty); + }); + test('none', () { + var trace = []; + int log(int a, int b) { + trace + ..add(a) + ..add(b); + return b; + } + + expect( + iterable([1, 3, 5, 7]) + .whereNotIndexed((i, x) => log(i, x).isOdd), + isEmpty); + expect(trace, [0, 1, 1, 3, 2, 5, 3, 7]); + }); + test('all', () { + expect( + iterable([1, 3, 5, 7]).whereNotIndexed((i, x) => x.isEven), + [1, 3, 5, 7]); + }); + test('some', () { + expect(iterable([1, 3, 5, 7]).whereNotIndexed((i, x) => i.isOdd), + [1, 5]); + }); + }); + group('.expandIndexed', () { + test('empty', () { + expect(iterable([]).expandIndexed(unreachable), isEmpty); + }); + test('empty result', () { + expect(iterable(['a', 'b']).expandIndexed((i, v) => []), isEmpty); + }); + test('larger result', () { + expect(iterable(['a', 'b']).expandIndexed((i, v) => ['$i', v]), + ['0', 'a', '1', 'b']); + }); + test('varying result', () { + expect( + iterable(['a', 'b']) + .expandIndexed((i, v) => i.isOdd ? ['$i', v] : []), + ['1', 'b']); + }); + }); + group('.reduceIndexed', () { + test('empty', () { + expect(() => iterable([]).reduceIndexed((i, a, b) => a), + throwsStateError); + }); + test('single', () { + expect(iterable([1]).reduceIndexed(unreachable), 1); + }); + test('multiple', () { + expect( + iterable([1, 4, 2]) + .reduceIndexed((i, p, v) => p + (pow(i + 1, v) as int)), + 1 + 16 + 9); + }); + }); + group('.foldIndexed', () { + test('empty', () { + expect(iterable([]).foldIndexed(0, unreachable), 0); + }); + test('single', () { + expect( + iterable([1]).foldIndexed('x', (i, a, b) => '$a:$i:$b'), 'x:0:1'); + }); + test('mulitple', () { + expect(iterable([1, 3, 9]).foldIndexed('x', (i, a, b) => '$a:$i:$b'), + 'x:0:1:1:3:2:9'); + }); + }); + group('.firstWhereOrNull', () { + test('empty', () { + expect(iterable([]).firstWhereOrNull(unreachable), null); + }); + test('none', () { + expect(iterable([1, 3, 7]).firstWhereOrNull(isEven), null); + }); + test('single', () { + expect(iterable([0, 1, 2]).firstWhereOrNull(isOdd), 1); + }); + test('first of multiple', () { + expect(iterable([0, 1, 3]).firstWhereOrNull(isOdd), 1); + }); + }); + group('.firstWhereIndexedOrNull', () { + test('empty', () { + expect(iterable([]).firstWhereIndexedOrNull(unreachable), null); + }); + test('none', () { + expect( + iterable([1, 3, 7]).firstWhereIndexedOrNull((i, x) => x.isEven), + null); + expect(iterable([1, 3, 7]).firstWhereIndexedOrNull((i, x) => i < 0), + null); + }); + test('single', () { + expect(iterable([0, 3, 6]).firstWhereIndexedOrNull((i, x) => x.isOdd), + 3); + expect( + iterable([0, 3, 6]).firstWhereIndexedOrNull((i, x) => i == 1), 3); + }); + test('first of multiple', () { + expect(iterable([0, 3, 7]).firstWhereIndexedOrNull((i, x) => x.isOdd), + 3); + expect( + iterable([0, 3, 7]).firstWhereIndexedOrNull((i, x) => i.isEven), + 0); + }); + }); + group('.firstOrNull', () { + test('empty', () { + expect(iterable([]).firstOrNull, null); + }); + test('single', () { + expect(iterable([1]).firstOrNull, 1); + }); + test('first of multiple', () { + expect(iterable([1, 3, 5]).firstOrNull, 1); + }); + }); + group('.lastWhereOrNull', () { + test('empty', () { + expect(iterable([]).lastWhereOrNull(unreachable), null); + }); + test('none', () { + expect(iterable([1, 3, 7]).lastWhereOrNull(isEven), null); + }); + test('single', () { + expect(iterable([0, 1, 2]).lastWhereOrNull(isOdd), 1); + }); + test('last of multiple', () { + expect(iterable([0, 1, 3]).lastWhereOrNull(isOdd), 3); + }); + }); + group('.lastWhereIndexedOrNull', () { + test('empty', () { + expect(iterable([]).lastWhereIndexedOrNull(unreachable), null); + }); + test('none', () { + expect(iterable([1, 3, 7]).lastWhereIndexedOrNull((i, x) => x.isEven), + null); + expect(iterable([1, 3, 7]).lastWhereIndexedOrNull((i, x) => i < 0), + null); + }); + test('single', () { + expect( + iterable([0, 3, 6]).lastWhereIndexedOrNull((i, x) => x.isOdd), 3); + expect( + iterable([0, 3, 6]).lastWhereIndexedOrNull((i, x) => i == 1), 3); + }); + test('last of multiple', () { + expect( + iterable([0, 3, 7]).lastWhereIndexedOrNull((i, x) => x.isOdd), 7); + expect(iterable([0, 3, 7]).lastWhereIndexedOrNull((i, x) => i.isEven), + 7); + }); + }); + group('.lastOrNull', () { + test('empty', () { + expect(iterable([]).lastOrNull, null); + }); + test('single', () { + expect(iterable([1]).lastOrNull, 1); + }); + test('last of multiple', () { + expect(iterable([1, 3, 5]).lastOrNull, 5); + }); + }); + group('.singleWhereOrNull', () { + test('empty', () { + expect(iterable([]).singleWhereOrNull(unreachable), null); + }); + test('none', () { + expect(iterable([1, 3, 7]).singleWhereOrNull(isEven), null); + }); + test('single', () { + expect(iterable([0, 1, 2]).singleWhereOrNull(isOdd), 1); + }); + test('multiple', () { + expect(iterable([0, 1, 3]).singleWhereOrNull(isOdd), null); + }); + }); + group('.singleWhereIndexedOrNull', () { + test('empty', () { + expect(iterable([]).singleWhereIndexedOrNull(unreachable), null); + }); + test('none', () { + expect( + iterable([1, 3, 7]).singleWhereIndexedOrNull((i, x) => x.isEven), + null); + expect(iterable([1, 3, 7]).singleWhereIndexedOrNull((i, x) => i < 0), + null); + }); + test('single', () { + expect( + iterable([0, 3, 6]).singleWhereIndexedOrNull((i, x) => x.isOdd), + 3); + expect(iterable([0, 3, 6]).singleWhereIndexedOrNull((i, x) => i == 1), + 3); + }); + test('multiple', () { + expect( + iterable([0, 3, 7]).singleWhereIndexedOrNull((i, x) => x.isOdd), + null); + expect( + iterable([0, 3, 7]).singleWhereIndexedOrNull((i, x) => i.isEven), + null); + }); + }); + group('.singleOrNull', () { + test('empty', () { + expect(iterable([]).singleOrNull, null); + }); + test('single', () { + expect(iterable([1]).singleOrNull, 1); + }); + test('multiple', () { + expect(iterable([1, 3, 5]).singleOrNull, null); + }); + }); + group('.lastBy', () { + test('empty', () { + expect(iterable([]).lastBy((dynamic _) {}), {}); + }); + test('single', () { + expect(iterable([1]).lastBy(toString), { + '1': 1, + }); + }); + test('multiple', () { + expect( + iterable([1, 2, 3, 4, 5]).lastBy((x) => x.isEven), + { + false: 5, + true: 4, + }, + ); + }); + }); + group('.groupFoldBy', () { + test('empty', () { + expect(iterable([]).groupFoldBy(unreachable, unreachable), {}); + }); + test('single', () { + expect(iterable([1]).groupFoldBy(toString, (p, v) => [p, v]), { + '1': [null, 1] + }); + }); + test('multiple', () { + expect( + iterable([1, 2, 3, 4, 5]).groupFoldBy( + (x) => x.isEven, (p, v) => p == null ? '$v' : '$p:$v'), + {true: '2:4', false: '1:3:5'}); + }); + }); + group('.groupSetsBy', () { + test('empty', () { + expect(iterable([]).groupSetsBy(unreachable), {}); + }); + test('multiple same', () { + expect(iterable([1, 1]).groupSetsBy(toString), { + '1': {1} + }); + }); + test('multiple', () { + expect(iterable([1, 2, 3, 4, 5, 1]).groupSetsBy((x) => x % 3), { + 1: {1, 4}, + 2: {2, 5}, + 0: {3} + }); + }); + }); + group('.groupListsBy', () { + test('empty', () { + expect(iterable([]).groupListsBy(unreachable), {}); + }); + test('multiple saame', () { + expect(iterable([1, 1]).groupListsBy(toString), { + '1': [1, 1] + }); + }); + test('multiple', () { + expect(iterable([1, 2, 3, 4, 5, 1]).groupListsBy((x) => x % 3), { + 1: [1, 4, 1], + 2: [2, 5], + 0: [3] + }); + }); + }); + group('.splitBefore', () { + test('empty', () { + expect(iterable([]).splitBefore(unreachable), []); + }); + test('single', () { + expect(iterable([1]).splitBefore(unreachable), [ + [1] + ]); + }); + test('no split', () { + var trace = []; + bool log(int x) { + trace.add(x); + return false; + } + + expect(iterable([1, 2, 3]).splitBefore(log), [ + [1, 2, 3] + ]); + expect(trace, [2, 3]); + }); + test('all splits', () { + expect(iterable([1, 2, 3]).splitBefore((x) => true), [ + [1], + [2], + [3] + ]); + }); + test('some splits', () { + expect(iterable([1, 2, 3]).splitBefore((x) => x.isEven), [ + [1], + [2, 3] + ]); + }); + }); + group('.splitBeforeIndexed', () { + test('empty', () { + expect(iterable([]).splitBeforeIndexed(unreachable), []); + }); + test('single', () { + expect(iterable([1]).splitBeforeIndexed(unreachable), [ + [1] + ]); + }); + test('no split', () { + var trace = []; + bool log(int i, int x) { + trace + ..add('$i') + ..add(x); + return false; + } + + expect(iterable([1, 2, 3]).splitBeforeIndexed(log), [ + [1, 2, 3] + ]); + expect(trace, ['1', 2, '2', 3]); + }); + test('all splits', () { + expect(iterable([1, 2, 3]).splitBeforeIndexed((i, x) => true), [ + [1], + [2], + [3] + ]); + }); + test('some splits', () { + expect(iterable([1, 2, 3]).splitBeforeIndexed((i, x) => x.isEven), [ + [1], + [2, 3] + ]); + expect(iterable([1, 2, 3]).splitBeforeIndexed((i, x) => i.isEven), [ + [1, 2], + [3] + ]); + }); + }); + group('.splitAfter', () { + test('empty', () { + expect(iterable([]).splitAfter(unreachable), []); + }); + test('single', () { + expect(iterable([1]).splitAfter((x) => false), [ + [1] + ]); + expect(iterable([1]).splitAfter((x) => true), [ + [1] + ]); + }); + test('no split', () { + var trace = []; + bool log(int x) { + trace.add(x); + return false; + } + + expect(iterable([1, 2, 3]).splitAfter(log), [ + [1, 2, 3] + ]); + expect(trace, [1, 2, 3]); + }); + test('all splits', () { + expect(iterable([1, 2, 3]).splitAfter((x) => true), [ + [1], + [2], + [3] + ]); + }); + test('some splits', () { + expect(iterable([1, 2, 3]).splitAfter((x) => x.isEven), [ + [1, 2], + [3] + ]); + }); + }); + group('.splitAfterIndexed', () { + test('empty', () { + expect(iterable([]).splitAfterIndexed(unreachable), []); + }); + test('single', () { + expect(iterable([1]).splitAfterIndexed((i, x) => true), [ + [1] + ]); + expect(iterable([1]).splitAfterIndexed((i, x) => false), [ + [1] + ]); + }); + test('no split', () { + var trace = []; + bool log(int i, int x) { + trace + ..add('$i') + ..add(x); + return false; + } + + expect(iterable([1, 2, 3]).splitAfterIndexed(log), [ + [1, 2, 3] + ]); + expect(trace, ['0', 1, '1', 2, '2', 3]); + }); + test('all splits', () { + expect(iterable([1, 2, 3]).splitAfterIndexed((i, x) => true), [ + [1], + [2], + [3] + ]); + }); + test('some splits', () { + expect(iterable([1, 2, 3]).splitAfterIndexed((i, x) => x.isEven), [ + [1, 2], + [3] + ]); + expect(iterable([1, 2, 3]).splitAfterIndexed((i, x) => i.isEven), [ + [1], + [2, 3] + ]); + }); + }); + group('.splitBetween', () { + test('empty', () { + expect(iterable([]).splitBetween(unreachable), []); + }); + test('single', () { + expect(iterable([1]).splitBetween(unreachable), [ + [1] + ]); + }); + test('no split', () { + var trace = []; + bool log(int x, int y) { + trace.add([x, y]); + return false; + } + + expect(iterable([1, 2, 3]).splitBetween(log), [ + [1, 2, 3] + ]); + expect(trace, [ + [1, 2], + [2, 3] + ]); + }); + test('all splits', () { + expect(iterable([1, 2, 3]).splitBetween((x, y) => true), [ + [1], + [2], + [3] + ]); + }); + test('some splits', () { + expect(iterable([1, 2, 4]).splitBetween((x, y) => (x ^ y).isEven), [ + [1, 2], + [4] + ]); + }); + }); + group('.splitBetweenIndexed', () { + test('empty', () { + expect(iterable([]).splitBetweenIndexed(unreachable), []); + }); + test('single', () { + expect(iterable([1]).splitBetweenIndexed(unreachable), [ + [1] + ]); + }); + test('no split', () { + var trace = []; + bool log(int i, int x, int y) { + trace.add([i, x, y]); + return false; + } + + expect(iterable([1, 2, 3]).splitBetweenIndexed(log), [ + [1, 2, 3] + ]); + expect(trace, [ + [1, 1, 2], + [2, 2, 3] + ]); + }); + test('all splits', () { + expect(iterable([1, 2, 3]).splitBetweenIndexed((i, x, y) => true), [ + [1], + [2], + [3] + ]); + }); + test('some splits', () { + expect( + iterable([1, 2, 4]) + .splitBetweenIndexed((i, x, y) => (x ^ y).isEven), + [ + [1, 2], + [4] + ]); + expect( + iterable([1, 2, 4]) + .splitBetweenIndexed((i, x, y) => (i ^ y).isEven), + [ + [1, 2], + [4] + ]); + }); + }); + group('none', () { + test('empty', () { + expect(iterable([]).none(unreachable), true); + }); + test('single', () { + expect(iterable([1]).none(isEven), true); + expect(iterable([1]).none(isOdd), false); + }); + test('multiple', () { + expect(iterable([1, 3, 5, 7, 9, 11]).none(isEven), true); + expect(iterable([1, 3, 5, 7, 9, 10]).none(isEven), false); + expect(iterable([0, 3, 5, 7, 9, 11]).none(isEven), false); + expect(iterable([0, 2, 4, 6, 8, 10]).none(isEven), false); + }); + }); + }); + group('of nullable', () { + group('.whereNotNull', () { + test('empty', () { + expect( + iterable([]) + .whereNotNull(), // ignore: deprecated_member_use_from_same_package + isEmpty); + }); + test('single', () { + expect( + iterable([ + null + ]).whereNotNull(), // ignore: deprecated_member_use_from_same_package + isEmpty); + expect( + iterable([ + 1 + ]).whereNotNull(), // ignore: deprecated_member_use_from_same_package + [1]); + }); + test('multiple', () { + expect( + iterable([ + 1, + 3, + 5 + ]).whereNotNull(), // ignore: deprecated_member_use_from_same_package + [1, 3, 5]); + expect( + iterable([ + null, + null, + null + ]).whereNotNull(), // ignore: deprecated_member_use_from_same_package + isEmpty); + expect( + iterable([ + 1, + null, + 3, + null, + 5 + ]).whereNotNull(), // ignore: deprecated_member_use_from_same_package + [1, 3, 5]); + }); + }); + }); + group('of number', () { + group('.sum', () { + test('empty', () { + expect(iterable([]).sum, same(0)); + expect(iterable([]).sum, same(0.0)); + expect(iterable([]).sum, same(0)); + }); + test('single', () { + expect(iterable([1]).sum, same(1)); + expect(iterable([1.2]).sum, same(1.2)); + expect(iterable([1]).sum, same(1)); + expect(iterable([1.2]).sum, same(1.2)); + }); + test('multiple', () { + expect(iterable([1, 2, 4]).sum, 7); + expect(iterable([1.2, 3.5]).sum, 4.7); + expect(iterable([1, 3, 5]).sum, same(9)); + expect(iterable([1.2, 3.5]).sum, 4.7); + expect(iterable([1.2, 2, 3.5]).sum, 6.7); + }); + }); + group('average', () { + test('empty', () { + expect(() => iterable([]).average, throwsStateError); + expect(() => iterable([]).average, throwsStateError); + expect(() => iterable([]).average, throwsStateError); + }); + test('single', () { + expect(iterable([4]).average, same(4.0)); + expect(iterable([3.5]).average, 3.5); + expect(iterable([4]).average, same(4.0)); + expect(iterable([3.5]).average, 3.5); + }); + test('multiple', () { + expect(iterable([1, 3, 5]).average, same(3.0)); + expect(iterable([1, 3, 5, 9]).average, 4.5); + expect(iterable([1.0, 3.0, 5.0, 9.0]).average, 4.5); + expect(iterable([1, 3, 5, 9]).average, 4.5); + }); + }); + group('.min', () { + test('empty', () { + expect(() => iterable([]).min, throwsStateError); + expect(() => iterable([]).min, throwsStateError); + expect(() => iterable([]).min, throwsStateError); + }); + test('single', () { + expect(iterable([1]).min, 1); + expect(iterable([1.0]).min, 1.0); + expect(iterable([1.0]).min, 1.0); + }); + test('multiple', () { + expect(iterable([3, 1, 2]).min, 1); + expect(iterable([3.0, 1.0, 2.5]).min, 1.0); + expect(iterable([3, 1, 2.5]).min, 1.0); + }); + test('nan', () { + expect(iterable([3.0, 1.0, double.nan]).min, isNaN); + expect(iterable([3.0, 1, double.nan]).min, isNaN); + }); + }); + group('.minOrNull', () { + test('empty', () { + expect(iterable([]).minOrNull, null); + expect(iterable([]).minOrNull, null); + expect(iterable([]).minOrNull, null); + }); + test('single', () { + expect(iterable([1]).minOrNull, 1); + expect(iterable([1.0]).minOrNull, 1.0); + expect(iterable([1.0]).minOrNull, 1.0); + }); + test('multiple', () { + expect(iterable([3, 1, 2]).minOrNull, 1); + expect(iterable([3.0, 1.0, 2.5]).minOrNull, 1.0); + expect(iterable([3, 1, 2.5]).minOrNull, 1.0); + }); + test('nan', () { + expect(iterable([3.0, 1.0, double.nan]).minOrNull, isNaN); + expect(iterable([3.0, 1, double.nan]).minOrNull, isNaN); + }); + }); + group('.max', () { + test('empty', () { + expect(() => iterable([]).max, throwsStateError); + expect(() => iterable([]).max, throwsStateError); + expect(() => iterable([]).max, throwsStateError); + }); + test('single', () { + expect(iterable([1]).max, 1); + expect(iterable([1.0]).max, 1.0); + expect(iterable([1.0]).max, 1.0); + }); + test('multiple', () { + expect(iterable([3, 1, 2]).max, 3); + expect(iterable([3.0, 1.0, 2.5]).max, 3.0); + expect(iterable([3, 1, 2.5]).max, 3); + }); + test('nan', () { + expect(iterable([3.0, 1.0, double.nan]).max, isNaN); + expect(iterable([3.0, 1, double.nan]).max, isNaN); + }); + }); + group('.maxOrNull', () { + test('empty', () { + expect(iterable([]).maxOrNull, null); + expect(iterable([]).maxOrNull, null); + expect(iterable([]).maxOrNull, null); + }); + test('single', () { + expect(iterable([1]).maxOrNull, 1); + expect(iterable([1.0]).maxOrNull, 1.0); + expect(iterable([1.0]).maxOrNull, 1.0); + }); + test('multiple', () { + expect(iterable([3, 1, 2]).maxOrNull, 3); + expect(iterable([3.0, 1.0, 2.5]).maxOrNull, 3.0); + expect(iterable([3, 1, 2.5]).maxOrNull, 3); + }); + test('nan', () { + expect(iterable([3.0, 1.0, double.nan]).maxOrNull, isNaN); + expect(iterable([3.0, 1, double.nan]).maxOrNull, isNaN); + }); + }); + }); + group('of iterable', () { + group('.flattened', () { + var empty = iterable([]); + test('empty', () { + expect(iterable(>[]).flattened, []); + }); + test('multiple empty', () { + expect(iterable([empty, empty, empty]).flattened, []); + }); + test('single value', () { + expect( + iterable([ + iterable([1]) + ]).flattened, + [1]); + }); + test('multiple', () { + expect( + iterable([ + iterable([1, 2]), + empty, + iterable([3, 4]) + ]).flattened, + [1, 2, 3, 4]); + }); + }); + group('.flattenedToList', () { + var empty = iterable([]); + test('empty', () { + expect(iterable(>[]).flattenedToList, []); + }); + test('multiple empty', () { + expect(iterable([empty, empty, empty]).flattenedToList, []); + }); + test('single value', () { + expect( + iterable([ + iterable([1]) + ]).flattenedToList, + [1]); + }); + test('multiple', () { + expect( + iterable([ + iterable([1, 2]), + empty, + iterable([3, 4]) + ]).flattenedToList, + [1, 2, 3, 4]); + }); + }); + group('.flattenedToSet', () { + var empty = iterable([]); + test('empty', () { + expect(iterable(>[]).flattenedToSet, {}); + }); + test('multiple empty', () { + expect(iterable([empty, empty, empty]).flattenedToSet, {}); + }); + test('single value', () { + expect( + iterable([ + iterable([1]) + ]).flattenedToSet, + {1}); + }); + test('multiple', () { + expect( + iterable([ + iterable([1, 2]), + empty, + iterable([3, 4]) + ]).flattenedToSet, + {1, 2, 3, 4}); + expect( + iterable([ + iterable([1, 2, 3]), + empty, + iterable([2, 3, 4]) + ]).flattenedToSet, + {1, 2, 3, 4}); + }); + }); + }); + group('of comparable', () { + group('.min', () { + test('empty', () { + expect(() => iterable([]).min, throwsStateError); + }); + test('single', () { + expect(iterable(['a']).min, 'a'); + }); + test('multiple', () { + expect(iterable(['c', 'a', 'b']).min, 'a'); + }); + }); + group('.minOrNull', () { + test('empty', () { + expect(iterable([]).minOrNull, null); + }); + test('single', () { + expect(iterable(['a']).minOrNull, 'a'); + }); + test('multiple', () { + expect(iterable(['c', 'a', 'b']).minOrNull, 'a'); + }); + }); + group('.max', () { + test('empty', () { + expect(() => iterable([]).max, throwsStateError); + }); + test('single', () { + expect(iterable(['a']).max, 'a'); + }); + test('multiple', () { + expect(iterable(['b', 'c', 'a']).max, 'c'); + }); + }); + group('.maxOrNull', () { + test('empty', () { + expect(iterable([]).maxOrNull, null); + }); + test('single', () { + expect(iterable(['a']).maxOrNull, 'a'); + }); + test('multiple', () { + expect(iterable(['b', 'c', 'a']).maxOrNull, 'c'); + }); + }); + }); + group('.sorted', () { + test('empty', () { + expect(iterable([]).sorted(unreachable), []); + expect(iterable([]).sorted(), []); + }); + test('singleton', () { + expect(iterable(['a']).sorted(unreachable), ['a']); + expect(iterable(['a']).sorted(), ['a']); + }); + test('multiple', () { + expect(iterable(['5', '2', '4', '3', '1']).sorted(cmpParse), + ['1', '2', '3', '4', '5']); + expect( + iterable(['5', '2', '4', '3', '1']).sorted(cmpParseInverse), + ['5', '4', '3', '2', '1']); + expect(iterable(['5', '2', '4', '3', '1']).sorted(), + ['1', '2', '3', '4', '5']); + // Large enough to trigger quicksort. + var i256 = Iterable.generate(256, (i) => i ^ 0x55); + var sorted256 = [...i256]..sort(); + expect(i256.sorted(cmpInt), sorted256); + }); + }); + group('.isSorted', () { + test('empty', () { + expect(iterable([]).isSorted(unreachable), true); + expect(iterable([]).isSorted(), true); + }); + test('single', () { + expect(iterable(['1']).isSorted(unreachable), true); + expect(iterable(['1']).isSorted(), true); + }); + test('same', () { + expect(iterable(['1', '1', '1', '1']).isSorted(cmpParse), true); + expect(iterable(['1', '2', '0', '3']).isSorted(cmpStringLength), true); + expect(iterable(['1', '1', '1', '1']).isSorted(), true); + }); + test('multiple', () { + expect(iterable(['1', '2', '3', '4']).isSorted(cmpParse), true); + expect(iterable(['1', '2', '3', '4']).isSorted(), true); + expect(iterable(['4', '3', '2', '1']).isSorted(cmpParseInverse), true); + expect(iterable(['1', '2', '3', '0']).isSorted(cmpParse), false); + expect(iterable(['1', '2', '3', '0']).isSorted(), false); + expect(iterable(['4', '1', '2', '3']).isSorted(cmpParse), false); + expect(iterable(['4', '1', '2', '3']).isSorted(), false); + expect(iterable(['4', '3', '2', '1']).isSorted(cmpParse), false); + expect(iterable(['4', '3', '2', '1']).isSorted(), false); + }); + }); + group('.sample', () { + test('errors', () { + expect(() => iterable([1]).sample(-1), throwsRangeError); + }); + test('empty', () { + var empty = iterable([]); + expect(empty.sample(0), []); + expect(empty.sample(5), []); + }); + test('single', () { + var single = iterable([1]); + expect(single.sample(0), []); + expect(single.sample(1), [1]); + expect(single.sample(5), [1]); + }); + test('multiple', () { + var multiple = iterable([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + expect(multiple.sample(0), []); + var one = multiple.sample(1); + expect(one, hasLength(1)); + expect(one.first, inInclusiveRange(1, 10)); + var some = multiple.sample(3); + expect(some, hasLength(3)); + expect(some[0], inInclusiveRange(1, 10)); + expect(some[1], inInclusiveRange(1, 10)); + expect(some[2], inInclusiveRange(1, 10)); + expect(some[0], isNot(some[1])); + expect(some[0], isNot(some[2])); + expect(some[1], isNot(some[2])); + + var seen = {}; + do { + seen.addAll(multiple.sample(3)); + } while (seen.length < 10); + // Should eventually terminate. + }); + test('random', () { + // Passing in a `Random` makes result deterministic. + var multiple = iterable([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + var seed = 12345; + var some = multiple.sample(5, Random(seed)); + for (var i = 0; i < 10; i++) { + var other = multiple.sample(5, Random(seed)); + expect(other, some); + } + }); + group('shuffles results', () { + late Random random; + late Iterable input; + setUp(() async { + input = iterable([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + random = Random(12345); + }); + test('for partial samples of input', () { + var result = input.sample(9, random); + expect(result.length, 9); + expect(result.isSorted(cmpInt), isFalse); + }); + test('for complete samples of input', () { + var result = input.sample(10, random); + expect(result.length, 10); + expect(result.isSorted(cmpInt), isFalse); + expect( + const UnorderedIterableEquality().equals(input, result), isTrue); + }); + test('for overlengthed samples of input', () { + var result = input.sample(20, random); + expect(result.length, 10); + expect(result.isSorted(cmpInt), isFalse); + expect( + const UnorderedIterableEquality().equals(input, result), isTrue); + }); + }); + }); + group('.elementAtOrNull', () { + test('empty', () async { + expect(iterable([]).elementAtOrNull(0), isNull); + }); + test('negative index', () async { + expect(() => iterable([1]).elementAtOrNull(-1), + throwsA(isA())); + }); + test('index within range', () async { + expect(iterable([1]).elementAtOrNull(0), 1); + }); + test('index too high', () async { + expect(iterable([1]).elementAtOrNull(1), isNull); + }); + }); + group('.slices', () { + test('empty', () { + expect(iterable([]).slices(1), []); + }); + test('with the same length as the iterable', () { + expect(iterable([1, 2, 3]).slices(3), [ + [1, 2, 3] + ]); + }); + test('with a longer length than the iterable', () { + expect(iterable([1, 2, 3]).slices(5), [ + [1, 2, 3] + ]); + }); + test('with a shorter length than the iterable', () { + expect(iterable([1, 2, 3]).slices(2), [ + [1, 2], + [3] + ]); + }); + test('with length divisible by the iterable\'s', () { + expect(iterable([1, 2, 3, 4]).slices(2), [ + [1, 2], + [3, 4] + ]); + }); + test('refuses negative length', () { + expect(() => iterable([1]).slices(-1), throwsRangeError); + }); + test('refuses length 0', () { + expect(() => iterable([1]).slices(0), throwsRangeError); + }); + test('regression #286, bug in slice', () { + var l1 = [1, 2, 3, 4, 5, 6]; + List l2 = l1.slice(1, 5); // (2..5) + // This call would stack-overflow due to a lacking type promotion + // which caused the extension method to keep calling itself, + // instead of switching to the instance method on `ListSlice`. + // + // If successful, it would use the `2` argument as offset, instead + // of the `1` offset from the `slice` call above. + var l3 = l2.slice(2, 4); // (4..5) + expect(l3, [4, 5]); + expect(l3.toList(), [4, 5]); + }); + }); + }); + + group('Comparator', () { + test('.inverse', () { + var cmpStringInv = cmpString.inverse; + expect(cmpString('a', 'b'), isNegative); + expect(cmpStringInv('a', 'b'), isPositive); + expect(cmpString('aa', 'a'), isPositive); + expect(cmpStringInv('aa', 'a'), isNegative); + expect(cmpString('a', 'a'), isZero); + expect(cmpStringInv('a', 'a'), isZero); + }); + test('.compareBy', () { + var cmpByLength = cmpInt.compareBy((String s) => s.length); + expect(cmpByLength('a', 'b'), 0); + expect(cmpByLength('aa', 'b'), isPositive); + expect(cmpByLength('b', 'aa'), isNegative); + var cmpByInverseLength = cmpIntInverse.compareBy((String s) => s.length); + expect(cmpByInverseLength('a', 'b'), 0); + expect(cmpByInverseLength('aa', 'b'), isNegative); + expect(cmpByInverseLength('b', 'aa'), isPositive); + }); + + test('.then', () { + var cmpLengthFirst = cmpStringLength.then(cmpString); + var strings = ['a', 'aa', 'ba', 'ab', 'b', 'aaa']; + strings.sort(cmpString); + expect(strings, ['a', 'aa', 'aaa', 'ab', 'b', 'ba']); + strings.sort(cmpLengthFirst); + expect(strings, ['a', 'b', 'aa', 'ab', 'ba', 'aaa']); + + int cmpFirstLetter(String s1, String s2) => + s1.runes.first - s2.runes.first; + var cmpLetterLength = cmpFirstLetter.then(cmpStringLength); + var cmpLengthLetter = cmpStringLength.then(cmpFirstLetter); + strings = ['a', 'ab', 'b', 'ba', 'aaa']; + strings.sort(cmpLetterLength); + expect(strings, ['a', 'ab', 'aaa', 'b', 'ba']); + strings.sort(cmpLengthLetter); + expect(strings, ['a', 'b', 'ab', 'ba', 'aaa']); + }); + }); + + group('List', () { + group('of any', () { + group('.binarySearch', () { + test('empty', () { + expect([].binarySearch(1, unreachable), -1); + }); + test('single', () { + expect([0].binarySearch(1, cmpInt), -1); + expect([1].binarySearch(1, cmpInt), 0); + expect([2].binarySearch(1, cmpInt), -1); + }); + test('multiple', () { + expect([1, 2, 3, 4, 5, 6].binarySearch(3, cmpInt), 2); + expect([6, 5, 4, 3, 2, 1].binarySearch(3, cmpIntInverse), 3); + }); + }); + group('.binarySearchByCompare', () { + test('empty', () { + expect([].binarySearchByCompare(1, toString, cmpParse), -1); + }); + test('single', () { + expect([0].binarySearchByCompare(1, toString, cmpParse), -1); + expect([1].binarySearchByCompare(1, toString, cmpParse), 0); + expect([2].binarySearchByCompare(1, toString, cmpParse), -1); + }); + test('multiple', () { + expect( + [1, 2, 3, 4, 5, 6].binarySearchByCompare(3, toString, cmpParse), + 2); + expect( + [6, 5, 4, 3, 2, 1] + .binarySearchByCompare(3, toString, cmpParseInverse), + 3); + }); + }); + group('.binarySearchBy', () { + test('empty', () { + expect([].binarySearchBy(1, toString), -1); + }); + test('single', () { + expect([0].binarySearchBy(1, toString), -1); + expect([1].binarySearchBy(1, toString), 0); + expect([2].binarySearchBy(1, toString), -1); + }); + test('multiple', () { + expect([1, 2, 3, 4, 5, 6].binarySearchBy(3, toString), 2); + }); + }); + + group('.lowerBound', () { + test('empty', () { + expect([].lowerBound(1, unreachable), 0); + }); + test('single', () { + expect([0].lowerBound(1, cmpInt), 1); + expect([1].lowerBound(1, cmpInt), 0); + expect([2].lowerBound(1, cmpInt), 0); + }); + test('multiple', () { + expect([1, 2, 3, 4, 5, 6].lowerBound(3, cmpInt), 2); + expect([6, 5, 4, 3, 2, 1].lowerBound(3, cmpIntInverse), 3); + expect([1, 2, 4, 5, 6].lowerBound(3, cmpInt), 2); + expect([6, 5, 4, 2, 1].lowerBound(3, cmpIntInverse), 3); + }); + }); + group('.lowerBoundByCompare', () { + test('empty', () { + expect([].lowerBoundByCompare(1, toString, cmpParse), 0); + }); + test('single', () { + expect([0].lowerBoundByCompare(1, toString, cmpParse), 1); + expect([1].lowerBoundByCompare(1, toString, cmpParse), 0); + expect([2].lowerBoundByCompare(1, toString, cmpParse), 0); + }); + test('multiple', () { + expect( + [1, 2, 3, 4, 5, 6].lowerBoundByCompare(3, toString, cmpParse), 2); + expect( + [6, 5, 4, 3, 2, 1] + .lowerBoundByCompare(3, toString, cmpParseInverse), + 3); + expect([1, 2, 4, 5, 6].lowerBoundByCompare(3, toString, cmpParse), 2); + expect( + [6, 5, 4, 2, 1].lowerBoundByCompare(3, toString, cmpParseInverse), + 3); + }); + }); + group('.lowerBoundBy', () { + test('empty', () { + expect([].lowerBoundBy(1, toString), 0); + }); + test('single', () { + expect([0].lowerBoundBy(1, toString), 1); + expect([1].lowerBoundBy(1, toString), 0); + expect([2].lowerBoundBy(1, toString), 0); + }); + test('multiple', () { + expect([1, 2, 3, 4, 5, 6].lowerBoundBy(3, toString), 2); + expect([1, 2, 4, 5, 6].lowerBoundBy(3, toString), 2); + }); + }); + group('sortRange', () { + test('errors', () { + expect(() => [1].sortRange(-1, 1, cmpInt), throwsArgumentError); + expect(() => [1].sortRange(0, 2, cmpInt), throwsArgumentError); + expect(() => [1].sortRange(1, 0, cmpInt), throwsArgumentError); + }); + test('empty range', () { + [].sortRange(0, 0, unreachable); + var list = [3, 2, 1]; + list.sortRange(0, 0, unreachable); + list.sortRange(3, 3, unreachable); + expect(list, [3, 2, 1]); + }); + test('single', () { + [1].sortRange(0, 1, unreachable); + var list = [3, 2, 1]; + list.sortRange(0, 1, unreachable); + list.sortRange(1, 2, unreachable); + list.sortRange(2, 3, unreachable); + }); + test('multiple', () { + var list = [9, 8, 7, 6, 5, 4, 3, 2, 1]; + list.sortRange(2, 5, cmpInt); + expect(list, [9, 8, 5, 6, 7, 4, 3, 2, 1]); + list.sortRange(4, 8, cmpInt); + expect(list, [9, 8, 5, 6, 2, 3, 4, 7, 1]); + list.sortRange(3, 6, cmpIntInverse); + expect(list, [9, 8, 5, 6, 3, 2, 4, 7, 1]); + }); + }); + group('.sortBy', () { + test('empty', () { + expect([]..sortBy(unreachable), []); + }); + test('singleton', () { + expect([1]..sortBy(unreachable), [1]); + }); + test('multiple', () { + expect([3, 20, 100]..sortBy(toString), [100, 20, 3]); + }); + group('range', () { + test('errors', () { + expect(() => [1].sortBy(toString, -1, 1), throwsArgumentError); + expect(() => [1].sortBy(toString, 0, 2), throwsArgumentError); + expect(() => [1].sortBy(toString, 1, 0), throwsArgumentError); + }); + test('empty', () { + expect([5, 7, 4, 2, 3]..sortBy(unreachable, 2, 2), [5, 7, 4, 2, 3]); + }); + test('singleton', () { + expect([5, 7, 4, 2, 3]..sortBy(unreachable, 2, 3), [5, 7, 4, 2, 3]); + }); + test('multiple', () { + expect( + [5, 7, 40, 2, 3]..sortBy((a) => '$a', 1, 4), [5, 2, 40, 7, 3]); + }); + }); + }); + group('.sortByCompare', () { + test('empty', () { + expect([]..sortByCompare(unreachable, unreachable), []); + }); + test('singleton', () { + expect([2]..sortByCompare(unreachable, unreachable), [2]); + }); + test('multiple', () { + expect([30, 2, 100]..sortByCompare(toString, cmpParseInverse), + [100, 30, 2]); + }); + group('range', () { + test('errors', () { + expect(() => [1].sortByCompare(toString, cmpParse, -1, 1), + throwsArgumentError); + expect(() => [1].sortByCompare(toString, cmpParse, 0, 2), + throwsArgumentError); + expect(() => [1].sortByCompare(toString, cmpParse, 1, 0), + throwsArgumentError); + }); + test('empty', () { + expect( + [3, 5, 7, 3, 1]..sortByCompare(unreachable, unreachable, 2, 2), + [3, 5, 7, 3, 1]); + }); + test('singleton', () { + expect( + [3, 5, 7, 3, 1]..sortByCompare(unreachable, unreachable, 2, 3), + [3, 5, 7, 3, 1]); + }); + test('multiple', () { + expect( + [3, 5, 7, 30, 1] + ..sortByCompare(toString, cmpParseInverse, 1, 4), + [3, 30, 7, 5, 1]); + }); + }); + }); + group('.shuffleRange', () { + test('errors', () { + expect(() => [1].shuffleRange(-1, 1), throwsArgumentError); + expect(() => [1].shuffleRange(0, 2), throwsArgumentError); + expect(() => [1].shuffleRange(1, 0), throwsArgumentError); + }); + test('empty range', () { + expect([]..shuffleRange(0, 0), []); + expect([1, 2, 3, 4]..shuffleRange(0, 0), [1, 2, 3, 4]); + expect([1, 2, 3, 4]..shuffleRange(4, 4), [1, 2, 3, 4]); + }); + test('singleton range', () { + expect([1, 2, 3, 4]..shuffleRange(0, 1), [1, 2, 3, 4]); + expect([1, 2, 3, 4]..shuffleRange(3, 4), [1, 2, 3, 4]); + }); + test('multiple', () { + var list = [1, 2, 3, 4, 5]; + do { + list.shuffleRange(0, 3); + expect(list.getRange(3, 5), [4, 5]); + expect(list.getRange(0, 3), unorderedEquals([1, 2, 3])); + } while (const ListEquality().equals(list.sublist(0, 3), [1, 2, 3])); + // Won't terminate if shuffle *never* moves a value. + }); + }); + group('.reverseRange', () { + test('errors', () { + expect(() => [1].reverseRange(-1, 1), throwsArgumentError); + expect(() => [1].reverseRange(0, 2), throwsArgumentError); + expect(() => [1].reverseRange(1, 0), throwsArgumentError); + }); + test('empty range', () { + expect([]..reverseRange(0, 0), []); + expect([1, 2, 3, 4]..reverseRange(0, 0), [1, 2, 3, 4]); + expect([1, 2, 3, 4]..reverseRange(4, 4), [1, 2, 3, 4]); + }); + test('singleton range', () { + expect([1, 2, 3, 4]..reverseRange(0, 1), [1, 2, 3, 4]); + expect([1, 2, 3, 4]..reverseRange(3, 4), [1, 2, 3, 4]); + }); + test('multiple', () { + var list = [1, 2, 3, 4, 5]; + list.reverseRange(0, 3); + expect(list, [3, 2, 1, 4, 5]); + list.reverseRange(3, 5); + expect(list, [3, 2, 1, 5, 4]); + list.reverseRange(0, 5); + expect(list, [4, 5, 1, 2, 3]); + }); + }); + group('.swap', () { + test('errors', () { + expect(() => [1].swap(0, 1), throwsArgumentError); + expect(() => [1].swap(1, 1), throwsArgumentError); + expect(() => [1].swap(1, 0), throwsArgumentError); + expect(() => [1].swap(-1, 0), throwsArgumentError); + }); + test('self swap', () { + expect([1]..swap(0, 0), [1]); + expect([1, 2, 3]..swap(1, 1), [1, 2, 3]); + }); + test('actual swap', () { + expect([1, 2, 3]..swap(0, 2), [3, 2, 1]); + expect([1, 2, 3]..swap(2, 0), [3, 2, 1]); + expect([1, 2, 3]..swap(2, 1), [1, 3, 2]); + expect([1, 2, 3]..swap(1, 2), [1, 3, 2]); + expect([1, 2, 3]..swap(0, 1), [2, 1, 3]); + expect([1, 2, 3]..swap(1, 0), [2, 1, 3]); + }); + }); + group('.slice', () { + test('errors', () { + expect(() => [1].slice(-1, 1), throwsArgumentError); + expect(() => [1].slice(0, 2), throwsArgumentError); + expect(() => [1].slice(1, 0), throwsArgumentError); + var l = [1]; + var slice = l.slice(0, 1); + l.removeLast(); + expect(() => slice.first, throwsConcurrentModificationError); + }); + test('empty', () { + expect([].slice(0, 0), isEmpty); + }); + test('modify', () { + var list = [1, 2, 3, 4, 5, 6, 7, 8, 9]; + var slice = list.slice(2, 6); + expect(slice, [3, 4, 5, 6]); + slice.sort(cmpIntInverse); + expect(slice, [6, 5, 4, 3]); + expect(list, [1, 2, 6, 5, 4, 3, 7, 8, 9]); + }); + }); + group('equals', () { + test('empty', () { + expect([].equals([]), true); + }); + test('non-empty', () { + expect([1, 2.5, 'a'].equals([1.0, 2.5, 'a']), true); + expect([1, 2.5, 'a'].equals([1.0, 2.5, 'b']), false); + expect( + [ + [1] + ].equals([ + [1] + ]), + false); + expect( + [ + [1] + ].equals([ + [1] + ], const ListEquality()), + true); + }); + }); + group('.forEachIndexed', () { + test('empty', () { + [].forEachIndexed(unreachable); + }); + test('single', () { + var log = []; + ['a'].forEachIndexed((i, s) { + log + ..add(i) + ..add(s); + }); + expect(log, [0, 'a']); + }); + test('multiple', () { + var log = []; + ['a', 'b', 'c'].forEachIndexed((i, s) { + log + ..add(i) + ..add(s); + }); + expect(log, [0, 'a', 1, 'b', 2, 'c']); + }); + }); + group('.forEachWhile', () { + test('empty', () { + [].forEachWhile(unreachable); + }); + test('single true', () { + var log = []; + ['a'].forEachWhile((s) { + log.add(s); + return true; + }); + expect(log, ['a']); + }); + test('single false', () { + var log = []; + ['a'].forEachWhile((s) { + log.add(s); + return false; + }); + expect(log, ['a']); + }); + test('multiple one', () { + var log = []; + ['a', 'b', 'c'].forEachWhile((s) { + log.add(s); + return false; + }); + expect(log, ['a']); + }); + test('multiple all', () { + var log = []; + ['a', 'b', 'c'].forEachWhile((s) { + log.add(s); + return true; + }); + expect(log, ['a', 'b', 'c']); + }); + test('multiple some', () { + var log = []; + ['a', 'b', 'c'].forEachWhile((s) { + log.add(s); + return s != 'b'; + }); + expect(log, ['a', 'b']); + }); + }); + group('.forEachIndexedWhile', () { + test('empty', () { + [].forEachIndexedWhile(unreachable); + }); + test('single true', () { + var log = []; + ['a'].forEachIndexedWhile((i, s) { + log + ..add(i) + ..add(s); + return true; + }); + expect(log, [0, 'a']); + }); + test('single false', () { + var log = []; + ['a'].forEachIndexedWhile((i, s) { + log + ..add(i) + ..add(s); + return false; + }); + expect(log, [0, 'a']); + }); + test('multiple one', () { + var log = []; + ['a', 'b', 'c'].forEachIndexedWhile((i, s) { + log + ..add(i) + ..add(s); + return false; + }); + expect(log, [0, 'a']); + }); + test('multiple all', () { + var log = []; + ['a', 'b', 'c'].forEachIndexedWhile((i, s) { + log + ..add(i) + ..add(s); + return true; + }); + expect(log, [0, 'a', 1, 'b', 2, 'c']); + }); + test('multiple some', () { + var log = []; + ['a', 'b', 'c'].forEachIndexedWhile((i, s) { + log + ..add(i) + ..add(s); + return s != 'b'; + }); + expect(log, [0, 'a', 1, 'b']); + }); + }); + group('.mapIndexed', () { + test('empty', () { + expect([].mapIndexed(unreachable), isEmpty); + }); + test('multiple', () { + expect(['a', 'b'].mapIndexed((i, s) => [i, s]), [ + [0, 'a'], + [1, 'b'] + ]); + }); + }); + group('.whereIndexed', () { + test('empty', () { + expect([].whereIndexed(unreachable), isEmpty); + }); + test('none', () { + var trace = []; + int log(int a, int b) { + trace + ..add(a) + ..add(b); + return b; + } + + expect([1, 3, 5, 7].whereIndexed((i, x) => log(i, x).isEven), + isEmpty); + expect(trace, [0, 1, 1, 3, 2, 5, 3, 7]); + }); + test('all', () { + expect( + [1, 3, 5, 7].whereIndexed((i, x) => x.isOdd), [1, 3, 5, 7]); + }); + test('some', () { + expect([1, 3, 5, 7].whereIndexed((i, x) => i.isOdd), [3, 7]); + }); + }); + group('.whereNotIndexed', () { + test('empty', () { + expect([].whereNotIndexed(unreachable), isEmpty); + }); + test('none', () { + var trace = []; + int log(int a, int b) { + trace + ..add(a) + ..add(b); + return b; + } + + expect([1, 3, 5, 7].whereNotIndexed((i, x) => log(i, x).isOdd), + isEmpty); + expect(trace, [0, 1, 1, 3, 2, 5, 3, 7]); + }); + test('all', () { + expect([1, 3, 5, 7].whereNotIndexed((i, x) => x.isEven), + [1, 3, 5, 7]); + }); + test('some', () { + expect([1, 3, 5, 7].whereNotIndexed((i, x) => i.isOdd), [1, 5]); + }); + }); + group('.expandIndexed', () { + test('empty', () { + expect([].expandIndexed(unreachable), isEmpty); + }); + test('empty result', () { + expect(['a', 'b'].expandIndexed((i, v) => []), isEmpty); + }); + test('larger result', () { + expect(['a', 'b'].expandIndexed((i, v) => ['$i', v]), + ['0', 'a', '1', 'b']); + }); + test('varying result', () { + expect(['a', 'b'].expandIndexed((i, v) => i.isOdd ? ['$i', v] : []), + ['1', 'b']); + }); + }); + group('.elementAtOrNull', () { + test('empty', () async { + expect([].elementAtOrNull(0), isNull); + }); + test('negative index', () async { + expect(() => [1].elementAtOrNull(-1), throwsA(isA())); + }); + test('index within range', () async { + expect([1].elementAtOrNull(0), 1); + }); + test('index too high', () async { + expect([1].elementAtOrNull(1), isNull); + }); + }); + group('.slices', () { + test('empty', () { + expect([].slices(1), []); + }); + test('with the same length as the iterable', () { + expect([1, 2, 3].slices(3), [ + [1, 2, 3] + ]); + }); + test('with a longer length than the iterable', () { + expect([1, 2, 3].slices(5), [ + [1, 2, 3] + ]); + }); + test('with a shorter length than the iterable', () { + expect([1, 2, 3].slices(2), [ + [1, 2], + [3] + ]); + }); + test('with length divisible by the iterable\'s', () { + expect([1, 2, 3, 4].slices(2), [ + [1, 2], + [3, 4] + ]); + }); + test('refuses negative length', () { + expect(() => [1].slices(-1), throwsRangeError); + }); + test('refuses length 0', () { + expect(() => [1].slices(0), throwsRangeError); + }); + }); + }); + group('on comparable', () { + group('.binarySearch', () { + test('empty', () { + expect([].binarySearch('1', unreachable), -1); + expect([].binarySearch('1'), -1); + }); + test('single', () { + expect(['0'].binarySearch('1', cmpString), -1); + expect(['1'].binarySearch('1', cmpString), 0); + expect(['2'].binarySearch('1', cmpString), -1); + expect( + ['0'].binarySearch( + '1', + ), + -1); + expect( + ['1'].binarySearch( + '1', + ), + 0); + expect( + ['2'].binarySearch( + '1', + ), + -1); + }); + test('multiple', () { + expect( + ['1', '2', '3', '4', '5', '6'].binarySearch('3', cmpString), 2); + expect(['1', '2', '3', '4', '5', '6'].binarySearch('3'), 2); + expect( + ['6', '5', '4', '3', '2', '1'].binarySearch('3', cmpParseInverse), + 3); + }); + }); + }); + group('.lowerBound', () { + test('empty', () { + expect([].lowerBound('1', unreachable), 0); + }); + test('single', () { + expect(['0'].lowerBound('1', cmpString), 1); + expect(['1'].lowerBound('1', cmpString), 0); + expect(['2'].lowerBound('1', cmpString), 0); + expect(['0'].lowerBound('1'), 1); + expect(['1'].lowerBound('1'), 0); + expect(['2'].lowerBound('1'), 0); + }); + test('multiple', () { + expect(['1', '2', '3', '4', '5', '6'].lowerBound('3', cmpParse), 2); + expect(['1', '2', '3', '4', '5', '6'].lowerBound('3'), 2); + expect( + ['6', '5', '4', '3', '2', '1'].lowerBound('3', cmpParseInverse), 3); + expect(['1', '2', '4', '5', '6'].lowerBound('3', cmpParse), 2); + expect(['1', '2', '4', '5', '6'].lowerBound('3'), 2); + expect(['6', '5', '4', '2', '1'].lowerBound('3', cmpParseInverse), 3); + }); + }); + group('sortRange', () { + test('errors', () { + expect(() => [1].sortRange(-1, 1, cmpInt), throwsArgumentError); + expect(() => [1].sortRange(0, 2, cmpInt), throwsArgumentError); + expect(() => [1].sortRange(1, 0, cmpInt), throwsArgumentError); + }); + test('empty range', () { + [].sortRange(0, 0, unreachable); + var list = [3, 2, 1]; + list.sortRange(0, 0, unreachable); + list.sortRange(3, 3, unreachable); + expect(list, [3, 2, 1]); + }); + test('single', () { + [1].sortRange(0, 1, unreachable); + var list = [3, 2, 1]; + list.sortRange(0, 1, unreachable); + list.sortRange(1, 2, unreachable); + list.sortRange(2, 3, unreachable); + }); + test('multiple', () { + var list = [9, 8, 7, 6, 5, 4, 3, 2, 1]; + list.sortRange(2, 5, cmpInt); + expect(list, [9, 8, 5, 6, 7, 4, 3, 2, 1]); + list.sortRange(4, 8, cmpInt); + expect(list, [9, 8, 5, 6, 2, 3, 4, 7, 1]); + list.sortRange(3, 6, cmpIntInverse); + expect(list, [9, 8, 5, 6, 3, 2, 4, 7, 1]); + }); + }); + }); +} + +/// Creates a plain iterable not implementing any other class. +Iterable iterable(Iterable values) sync* { + yield* values; +} + +Never unreachable([_, __, ___]) => fail('Unreachable'); + +String toString(Object? o) => '$o'; + +/// Compares values equal if they have the same remainder mod [mod]. +int Function(int, int) cmpMod(int mod) => (a, b) => a ~/ mod - b ~/ mod; + +/// Compares strings lexically. +int cmpString(String a, String b) => a.compareTo(b); + +/// Compares strings by length. +int cmpStringLength(String a, String b) => a.length - b.length; + +/// Compares strings by their integer numeral content. +int cmpParse(String s1, String s2) => cmpInt(int.parse(s1), int.parse(s2)); + +/// Compares strings inversely by their integer numeral content. +int cmpParseInverse(String s1, String s2) => + cmpIntInverse(int.parse(s1), int.parse(s2)); + +/// Compares integers by size. +int cmpInt(int a, int b) => a - b; + +/// Compares integers by inverse size. +int cmpIntInverse(int a, int b) => b - a; + +/// Tests an integer for being even. +bool isEven(int x) => x.isEven; + +/// Tests an integer for being odd. +bool isOdd(int x) => x.isOdd; diff --git a/pkgs/collection/test/functions_test.dart b/pkgs/collection/test/functions_test.dart new file mode 100644 index 00000000..f6023033 --- /dev/null +++ b/pkgs/collection/test/functions_test.dart @@ -0,0 +1,363 @@ +// Copyright (c) 2016, 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. + +// ignore_for_file: deprecated_member_use_from_same_package + +import 'package:collection/collection.dart'; +import 'package:test/test.dart'; + +void main() { + group('mapMap()', () { + test('with an empty map returns an empty map', () { + expect( + mapMap({}, + key: expectAsync2((_, __) {}, count: 0), + value: expectAsync2((_, __) {}, count: 0)), + isEmpty); + }); + + test('with no callbacks, returns a copy of the map', () { + var map = {'foo': 1, 'bar': 2}; + var result = mapMap(map); + expect(result, equals({'foo': 1, 'bar': 2})); + + // The resulting map should be a copy. + result['foo'] = 3; + expect(map, equals({'foo': 1, 'bar': 2})); + }); + + test("maps the map's keys", () { + expect( + mapMap({'foo': 1, 'bar': 2}, + key: (dynamic key, dynamic value) => key[value]), + equals({'o': 1, 'r': 2})); + }); + + test("maps the map's values", () { + expect( + mapMap({'foo': 1, 'bar': 2}, + value: (dynamic key, dynamic value) => key[value]), + equals({'foo': 'o', 'bar': 'r'})); + }); + + test("maps both the map's keys and values", () { + expect( + mapMap({'foo': 1, 'bar': 2}, + key: (dynamic key, dynamic value) => '$key$value', + value: (dynamic key, dynamic value) => key[value]), + equals({'foo1': 'o', 'bar2': 'r'})); + }); + }); + + group('mergeMaps()', () { + test('with empty maps returns an empty map', () { + expect( + mergeMaps({}, {}, + value: expectAsync2((dynamic _, dynamic __) {}, count: 0)), + isEmpty); + }); + + test('returns a map with all values in both input maps', () { + expect(mergeMaps({'foo': 1, 'bar': 2}, {'baz': 3, 'qux': 4}), + equals({'foo': 1, 'bar': 2, 'baz': 3, 'qux': 4})); + }); + + test("the second map's values win by default", () { + expect(mergeMaps({'foo': 1, 'bar': 2}, {'bar': 3, 'baz': 4}), + equals({'foo': 1, 'bar': 3, 'baz': 4})); + }); + + test('uses the callback to merge values', () { + expect( + mergeMaps({'foo': 1, 'bar': 2}, {'bar': 3, 'baz': 4}, + value: (dynamic value1, dynamic value2) => value1 + value2), + equals({'foo': 1, 'bar': 5, 'baz': 4})); + }); + }); + + group('lastBy()', () { + test('returns an empty map for an empty iterable', () { + expect( + lastBy([], (_) => fail('Must not be called for empty input')), + isEmpty, + ); + }); + + test("keeps the latest element for the function's return value", () { + expect( + lastBy(['foo', 'bar', 'baz', 'bop', 'qux'], + (String string) => string[1]), + equals({ + 'o': 'bop', + 'a': 'baz', + 'u': 'qux', + })); + }); + }); + + group('groupBy()', () { + test('returns an empty map for an empty iterable', () { + expect(groupBy([], expectAsync1((dynamic _) {}, count: 0)), isEmpty); + }); + + test("groups elements by the function's return value", () { + expect( + groupBy(['foo', 'bar', 'baz', 'bop', 'qux'], + (dynamic string) => string[1]), + equals({ + 'o': ['foo', 'bop'], + 'a': ['bar', 'baz'], + 'u': ['qux'] + })); + }); + }); + + group('minBy()', () { + test('returns null for an empty iterable', () { + expect( + minBy([], expectAsync1((dynamic _) {}, count: 0), + compare: expectAsync2((dynamic _, dynamic __) => -1, count: 0)), + isNull); + }); + + test( + 'returns the element for which the ordering function returns the ' + 'smallest value', () { + expect( + minBy([ + {'foo': 3}, + {'foo': 5}, + {'foo': 4}, + {'foo': 1}, + {'foo': 2} + ], (dynamic map) => map['foo']), + equals({'foo': 1})); + }); + + test('uses a custom comparator if provided', () { + expect( + minBy, Map>([ + {'foo': 3}, + {'foo': 5}, + {'foo': 4}, + {'foo': 1}, + {'foo': 2} + ], (map) => map, + compare: (map1, map2) => map1['foo']!.compareTo(map2['foo']!)), + equals({'foo': 1})); + }); + }); + + group('maxBy()', () { + test('returns null for an empty iterable', () { + expect( + maxBy([], expectAsync1((dynamic _) {}, count: 0), + compare: expectAsync2((dynamic _, dynamic __) => 0, count: 0)), + isNull); + }); + + test( + 'returns the element for which the ordering function returns the ' + 'largest value', () { + expect( + maxBy([ + {'foo': 3}, + {'foo': 5}, + {'foo': 4}, + {'foo': 1}, + {'foo': 2} + ], (dynamic map) => map['foo']), + equals({'foo': 5})); + }); + + test('uses a custom comparator if provided', () { + expect( + maxBy, Map>([ + {'foo': 3}, + {'foo': 5}, + {'foo': 4}, + {'foo': 1}, + {'foo': 2} + ], (map) => map, + compare: (map1, map2) => map1['foo']!.compareTo(map2['foo']!)), + equals({'foo': 5})); + }); + }); + + group('transitiveClosure()', () { + test('returns an empty map for an empty graph', () { + expect(transitiveClosure({}), isEmpty); + }); + + test('returns the input when there are no transitive connections', () { + expect( + transitiveClosure({ + 'foo': ['bar'], + 'bar': [], + 'bang': ['qux', 'zap'], + 'qux': [], + 'zap': [] + }), + equals({ + 'foo': ['bar'], + 'bar': [], + 'bang': ['qux', 'zap'], + 'qux': [], + 'zap': [] + })); + }); + + test('flattens transitive connections', () { + expect( + transitiveClosure({ + 'qux': [], + 'bar': ['baz'], + 'baz': ['qux'], + 'foo': ['bar'] + }), + equals({ + 'foo': ['bar', 'baz', 'qux'], + 'bar': ['baz', 'qux'], + 'baz': ['qux'], + 'qux': [] + })); + }); + + test('handles loops', () { + expect( + transitiveClosure({ + 'foo': ['bar'], + 'bar': ['baz'], + 'baz': ['foo'] + }), + equals({ + 'foo': ['bar', 'baz', 'foo'], + 'bar': ['baz', 'foo', 'bar'], + 'baz': ['foo', 'bar', 'baz'] + })); + }); + }); + + group('stronglyConnectedComponents()', () { + test('returns an empty list for an empty graph', () { + expect(stronglyConnectedComponents({}), isEmpty); + }); + + test('returns one set for a singleton graph', () { + expect( + stronglyConnectedComponents({'a': []}), + equals([ + {'a'} + ])); + }); + + test('returns two sets for a two-element tree', () { + expect( + stronglyConnectedComponents({ + 'a': ['b'], + 'b': [] + }), + equals([ + {'a'}, + {'b'} + ])); + }); + + test('returns one set for a two-element loop', () { + expect( + stronglyConnectedComponents({ + 'a': ['b'], + 'b': ['a'] + }), + equals([ + {'a', 'b'} + ])); + }); + + test('returns individual vertices for a tree', () { + expect( + stronglyConnectedComponents({ + 'foo': ['bar'], + 'bar': ['baz', 'bang'], + 'baz': ['qux'], + 'bang': ['zap'], + 'qux': [], + 'zap': [] + }), + equals([ + // This is expected to return *a* topological ordering, but this isn't + // the only valid one. If the function implementation changes in the + // future, this test may need to be updated. + {'foo'}, + {'bar'}, + {'bang'}, + {'zap'}, + {'baz'}, + {'qux'} + ]), + ); + }); + + test('returns a single set for a fully cyclic graph', () { + expect( + stronglyConnectedComponents({ + 'foo': ['bar'], + 'bar': ['baz'], + 'baz': ['bang'], + 'bang': ['foo'] + }), + equals([ + {'foo', 'bar', 'baz', 'bang'} + ])); + }); + + test('returns separate sets for each strongly connected component', () { + // https://en.wikipedia.org/wiki/Strongly_connected_component#/media/File:Scc.png + expect( + stronglyConnectedComponents({ + 'a': ['b'], + 'b': ['c', 'e', 'f'], + 'c': ['d', 'g'], + 'd': ['c', 'h'], + 'e': ['a', 'f'], + 'f': ['g'], + 'g': ['f'], + 'h': ['g', 'd'] + }), + equals([ + // This is expected to return *a* topological ordering, but this isn't + // the only valid one. If the function implementation changes in the + // future, this test may need to be updated. + {'a', 'b', 'e'}, + {'c', 'd', 'h'}, + {'f', 'g'}, + ]), + ); + }); + + test('always returns components in topological order', () { + expect( + stronglyConnectedComponents({ + 'bar': ['baz', 'bang'], + 'zap': [], + 'baz': ['qux'], + 'qux': [], + 'foo': ['bar'], + 'bang': ['zap'] + }), + equals([ + // This is expected to return *a* topological ordering, but this isn't + // the only valid one. If the function implementation changes in the + // future, this test may need to be updated. + {'foo'}, + {'bar'}, + {'bang'}, + {'zap'}, + {'baz'}, + {'qux'} + ]), + ); + }); + }); +} diff --git a/pkgs/collection/test/ignore_ascii_case_test.dart b/pkgs/collection/test/ignore_ascii_case_test.dart new file mode 100644 index 00000000..78f54a2a --- /dev/null +++ b/pkgs/collection/test/ignore_ascii_case_test.dart @@ -0,0 +1,60 @@ +// Copyright (c) 2016, 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. + +/// Tests case-ignoring compare and equality. +library; + +import 'package:collection/collection.dart'; +import 'package:test/test.dart'; + +void main() { + test('equality ignore ASCII case', () { + var strings = [ + '0@`aopz[{', + '0@`aopz[{', + '0@`Aopz[{', + '0@`aOpz[{', + '0@`AOpz[{', + '0@`aoPz[{', + '0@`AoPz[{', + '0@`aOPz[{', + '0@`AOPz[{', + '0@`aopZ[{', + '0@`AopZ[{', + '0@`aOpZ[{', + '0@`AOpZ[{', + '0@`aoPZ[{', + '0@`AoPZ[{', + '0@`aOPZ[{', + '0@`AOPZ[{', + ]; + + for (var s1 in strings) { + for (var s2 in strings) { + var reason = '$s1 =?= $s2'; + expect(equalsIgnoreAsciiCase(s1, s2), true, reason: reason); + expect(hashIgnoreAsciiCase(s1), hashIgnoreAsciiCase(s2), + reason: reason); + } + } + + var upperCaseLetters = '@`abcdefghijklmnopqrstuvwxyz[{åÅ'; + var lowerCaseLetters = '@`ABCDEFGHIJKLMNOPQRSTUVWXYZ[{åÅ'; + expect(equalsIgnoreAsciiCase(upperCaseLetters, lowerCaseLetters), true); + + void testChars(String char1, String char2, bool areEqual) { + expect(equalsIgnoreAsciiCase(char1, char2), areEqual, + reason: "$char1 ${areEqual ? "=" : "!"}= $char2"); + } + + for (var i = 0; i < upperCaseLetters.length; i++) { + for (var j = 0; i < upperCaseLetters.length; i++) { + testChars(upperCaseLetters[i], upperCaseLetters[j], i == j); + testChars(lowerCaseLetters[i], upperCaseLetters[j], i == j); + testChars(upperCaseLetters[i], lowerCaseLetters[j], i == j); + testChars(lowerCaseLetters[i], lowerCaseLetters[j], i == j); + } + } + }); +} diff --git a/pkgs/collection/test/iterable_zip_test.dart b/pkgs/collection/test/iterable_zip_test.dart new file mode 100644 index 00000000..3881c6af --- /dev/null +++ b/pkgs/collection/test/iterable_zip_test.dart @@ -0,0 +1,209 @@ +// Copyright (c) 2013, 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'; + +/// Iterable like [base] except that it throws when value equals [errorValue]. +Iterable iterError(Iterable base, int errorValue) { + // ignore: only_throw_errors + return base.map((x) => x == errorValue ? throw 'BAD' : x); +} + +void main() { + test('Basic', () { + expect( + IterableZip([ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9] + ]), + equals([ + [1, 4, 7], + [2, 5, 8], + [3, 6, 9] + ])); + }); + + test('Uneven length 1', () { + expect( + IterableZip([ + [1, 2, 3, 99, 100], + [4, 5, 6], + [7, 8, 9] + ]), + equals([ + [1, 4, 7], + [2, 5, 8], + [3, 6, 9] + ])); + }); + + test('Uneven length 2', () { + expect( + IterableZip([ + [1, 2, 3], + [4, 5, 6, 99, 100], + [7, 8, 9] + ]), + equals([ + [1, 4, 7], + [2, 5, 8], + [3, 6, 9] + ])); + }); + + test('Uneven length 3', () { + expect( + IterableZip([ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9, 99, 100] + ]), + equals([ + [1, 4, 7], + [2, 5, 8], + [3, 6, 9] + ])); + }); + + test('Uneven length 3', () { + expect( + IterableZip([ + [1, 2, 3, 98], + [4, 5, 6], + [7, 8, 9, 99, 100] + ]), + equals([ + [1, 4, 7], + [2, 5, 8], + [3, 6, 9] + ])); + }); + + test('Empty 1', () { + expect( + IterableZip([ + [], + [4, 5, 6], + [7, 8, 9] + ]), + equals([])); + }); + + test('Empty 2', () { + expect( + IterableZip([ + [1, 2, 3], + [], + [7, 8, 9] + ]), + equals([])); + }); + + test('Empty 3', () { + expect( + IterableZip([ + [1, 2, 3], + [4, 5, 6], + [] + ]), + equals([])); + }); + + test('Empty source', () { + expect(IterableZip([]), equals([])); + }); + + test('Single Source', () { + expect( + IterableZip([ + [1, 2, 3] + ]), + equals([ + [1], + [2], + [3] + ])); + }); + + test('Not-lists', () { + // Use other iterables than list literals. + var it1 = [1, 2, 3, 4, 5, 6].where((x) => x < 4); + var it2 = {4, 5, 6}; + var it3 = {7: 0, 8: 0, 9: 0}.keys; + var allIts = Iterable.generate(3, (i) => [it1, it2, it3][i]); + expect( + IterableZip(allIts), + equals([ + [1, 4, 7], + [2, 5, 8], + [3, 6, 9] + ])); + }); + + test('Error 1', () { + expect( + () => IterableZip([ + iterError([1, 2, 3], 2), + [4, 5, 6], + [7, 8, 9] + ]).toList(), + throwsA(equals('BAD'))); + }); + + test('Error 2', () { + expect( + () => IterableZip([ + [1, 2, 3], + iterError([4, 5, 6], 5), + [7, 8, 9] + ]).toList(), + throwsA(equals('BAD'))); + }); + + test('Error 3', () { + expect( + () => IterableZip([ + [1, 2, 3], + [4, 5, 6], + iterError([7, 8, 9], 8) + ]).toList(), + throwsA(equals('BAD'))); + }); + + test('Error at end', () { + expect( + () => IterableZip([ + [1, 2, 3], + iterError([4, 5, 6], 6), + [7, 8, 9] + ]).toList(), + throwsA(equals('BAD'))); + }); + + test('Error before first end', () { + expect( + () => IterableZip([ + iterError([1, 2, 3, 4], 4), + [4, 5, 6], + [7, 8, 9] + ]).toList(), + throwsA(equals('BAD'))); + }); + + test('Error after first end', () { + expect( + IterableZip([ + [1, 2, 3], + [4, 5, 6], + iterError([7, 8, 9, 10], 10) + ]), + equals([ + [1, 4, 7], + [2, 5, 8], + [3, 6, 9] + ])); + }); +} diff --git a/pkgs/collection/test/priority_queue_test.dart b/pkgs/collection/test/priority_queue_test.dart new file mode 100644 index 00000000..f07a1a39 --- /dev/null +++ b/pkgs/collection/test/priority_queue_test.dart @@ -0,0 +1,383 @@ +// Copyright (c) 2013, 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. + +/// Tests priority queue implementations utilities. +library; + +import 'package:collection/src/priority_queue.dart'; +import 'package:test/test.dart'; + +void main() { + testDefault(); + testInt(HeapPriorityQueue.new); + testCustom(HeapPriorityQueue.new); + testDuplicates(); + testNullable(); + testConcurrentModification(); +} + +void testDefault() { + test('PriorityQueue() returns a HeapPriorityQueue', () { + expect(PriorityQueue(), const TypeMatcher>()); + }); + testInt(PriorityQueue.new); + testCustom(PriorityQueue.new); +} + +void testInt(PriorityQueue Function() create) { + for (var count in [1, 5, 127, 128]) { + testQueue('int:$count', create, List.generate(count, (x) => x), count); + } +} + +void testCustom( + PriorityQueue Function(int Function(C, C)? comparator) create) { + for (var count in [1, 5, 127, 128]) { + testQueue('Custom:$count/null', () => create(null), + List.generate(count, C.new), C(count)); + testQueue('Custom:$count/compare', () => create(compare), + List.generate(count, C.new), C(count)); + testQueue('Custom:$count/compareNeg', () => create(compareNeg), + List.generate(count, (x) => C(count - x)), const C(0)); + } +} + +/// Test that a queue behaves correctly. +/// +/// The elements must be in priority order, from highest to lowest. +void testQueue( + String name, + PriorityQueue Function() create, + List elements, + T notElement, +) { + test(name, () => testQueueBody(create, elements, notElement)); +} + +void testQueueBody( + PriorityQueue Function() create, List elements, T notElement) { + var q = create(); + expect(q.isEmpty, isTrue); + expect(q, hasLength(0)); + expect(() { + q.first; + }, throwsStateError); + expect(() { + q.removeFirst(); + }, throwsStateError); + + // Tests removeFirst, first, contains, toList and toSet. + void testElements() { + expect(q.isNotEmpty, isTrue); + expect(q, hasLength(elements.length)); + + expect(q.toList(), equals(elements)); + expect(q.toSet().toList(), equals(elements)); + expect(q.toUnorderedList(), unorderedEquals(elements)); + expect(q.unorderedElements, unorderedEquals(elements)); + + var allElements = q.removeAll(); + q.addAll(allElements); + + for (var i = 0; i < elements.length; i++) { + expect(q.contains(elements[i]), isTrue); + } + expect(q.contains(notElement), isFalse); + + var all = []; + while (q.isNotEmpty) { + var expected = q.first; + var actual = q.removeFirst(); + expect(actual, same(expected)); + all.add(actual); + } + + expect(all.length, elements.length); + for (var i = 0; i < all.length; i++) { + expect(all[i], same(elements[i])); + } + + expect(q.isEmpty, isTrue); + } + + q.addAll(elements); + testElements(); + + q.addAll(elements.reversed); + testElements(); + + // Add elements in a non-linear order (gray order). + for (var i = 0, j = 0; i < elements.length; i++) { + int gray; + do { + gray = j ^ (j >> 1); + j++; + } while (gray >= elements.length); + q.add(elements[gray]); + } + testElements(); + + // Add elements by picking the middle element first, and then recursing + // on each side. + void addRec(int min, int max) { + var mid = min + ((max - min) >> 1); + q.add(elements[mid]); + if (mid + 1 < max) addRec(mid + 1, max); + if (mid > min) addRec(min, mid); + } + + addRec(0, elements.length); + testElements(); + + // Test removeAll. + q.addAll(elements); + expect(q, hasLength(elements.length)); + var all = q.removeAll(); + expect(q.isEmpty, isTrue); + expect(all, hasLength(elements.length)); + for (var i = 0; i < elements.length; i++) { + expect(all, contains(elements[i])); + } + + // Test the same element more than once in queue. + q.addAll(elements); + q.addAll(elements.reversed); + expect(q, hasLength(elements.length * 2)); + for (var i = 0; i < elements.length; i++) { + var element = elements[i]; + expect(q.contains(element), isTrue); + expect(q.removeFirst(), element); + expect(q.removeFirst(), element); + } + + // Test queue with all same element. + var a = elements[0]; + for (var i = 0; i < elements.length; i++) { + q.add(a); + } + expect(q, hasLength(elements.length)); + expect(q.contains(a), isTrue); + expect(q.contains(notElement), isFalse); + q.removeAll().forEach((x) => expect(x, same(a))); + + // Test remove. + q.addAll(elements); + for (var element in elements.reversed) { + expect(q.remove(element), isTrue); + } + expect(q.isEmpty, isTrue); +} + +void testDuplicates() { + // Check how the heap handles duplicate, or equal-but-not-identical, values. + test('duplicates', () { + var q = HeapPriorityQueue(compare); + var c1 = const C(0); + // ignore: prefer_const_constructors + var c2 = C(0); + + // Can contain the same element more than once. + expect(c1, equals(c2)); + expect(c1, isNot(same(c2))); + q.add(c1); + q.add(c1); + expect(q.length, 2); + expect(q.contains(c1), true); + expect(q.contains(c2), true); + expect(q.remove(c2), true); + expect(q.length, 1); + expect(q.removeFirst(), same(c1)); + + // Can contain equal elements. + q.add(c1); + q.add(c2); + expect(q.length, 2); + expect(q.contains(c1), true); + expect(q.contains(c2), true); + expect(q.remove(c1), true); + expect(q.length, 1); + expect(q.first, anyOf(same(c1), same(c2))); + }); +} + +void testNullable() { + // Check that the queue works with a nullable type, and a comparator + // which accepts `null`. + // Compares `null` before instances of `C`. + int nullCompareFirst(C? a, C? b) => a == null + ? b == null + ? 0 + : -1 + : b == null + ? 1 + : compare(a, b); + + int nullCompareLast(C? a, C? b) => a == null + ? b == null + ? 0 + : 1 + : b == null + ? -1 + : compare(a, b); + + var c1 = const C(1); + var c2 = const C(2); + var c3 = const C(3); + + test('nulls first', () { + var q = HeapPriorityQueue(nullCompareFirst); + q.add(c2); + q.add(c1); + q.add(null); + expect(q.length, 3); + expect(q.contains(null), true); + expect(q.contains(c1), true); + expect(q.contains(c3), false); + + expect(q.removeFirst(), null); + expect(q.length, 2); + expect(q.contains(null), false); + q.add(null); + expect(q.length, 3); + expect(q.contains(null), true); + q.add(null); + expect(q.length, 4); + expect(q.contains(null), true); + expect(q.remove(null), true); + expect(q.length, 3); + expect(q.toList(), [null, c1, c2]); + }); + + test('nulls last', () { + var q = HeapPriorityQueue(nullCompareLast); + q.add(c2); + q.add(c1); + q.add(null); + expect(q.length, 3); + expect(q.contains(null), true); + expect(q.contains(c1), true); + expect(q.contains(c3), false); + expect(q.first, c1); + + q.add(null); + expect(q.length, 4); + expect(q.contains(null), true); + q.add(null); + expect(q.length, 5); + expect(q.contains(null), true); + expect(q.remove(null), true); + expect(q.length, 4); + expect(q.toList(), [c1, c2, null, null]); + }); +} + +void testConcurrentModification() { + group('concurrent modification for', () { + test('add', () { + var q = HeapPriorityQueue((a, b) => a - b) + ..addAll([6, 4, 2, 3, 5, 8]); + var e = q.unorderedElements; + q.add(12); // Modifiation before creating iterator is not a problem. + var it = e.iterator; + q.add(7); // Modification after creatig iterator is a problem. + expect(it.moveNext, throwsConcurrentModificationError); + + it = e.iterator; // New iterator is not affected. + expect(it.moveNext(), true); + expect(it.moveNext(), true); + q.add(9); // Modification during iteration is a problem. + expect(it.moveNext, throwsConcurrentModificationError); + }); + + test('addAll', () { + var q = HeapPriorityQueue((a, b) => a - b) + ..addAll([6, 4, 2, 3, 5, 8]); + var e = q.unorderedElements; + q.addAll([12]); // Modifiation before creating iterator is not a problem. + var it = e.iterator; + q.addAll([7]); // Modification after creatig iterator is a problem. + expect(it.moveNext, throwsConcurrentModificationError); + it = e.iterator; // New iterator is not affected. + expect(it.moveNext(), true); + q.addAll([]); // Adding nothing is not a modification. + expect(it.moveNext(), true); + q.addAll([9]); // Modification during iteration is a problem. + expect(it.moveNext, throwsConcurrentModificationError); + }); + + test('removeFirst', () { + var q = HeapPriorityQueue((a, b) => a - b) + ..addAll([6, 4, 2, 3, 5, 8]); + var e = q.unorderedElements; + expect(q.removeFirst(), + 2); // Modifiation before creating iterator is not a problem. + var it = e.iterator; + expect(q.removeFirst(), + 3); // Modification after creatig iterator is a problem. + expect(it.moveNext, throwsConcurrentModificationError); + + it = e.iterator; // New iterator is not affected. + expect(it.moveNext(), true); + expect(it.moveNext(), true); + expect(q.removeFirst(), 4); // Modification during iteration is a problem. + expect(it.moveNext, throwsConcurrentModificationError); + }); + + test('remove', () { + var q = HeapPriorityQueue((a, b) => a - b) + ..addAll([6, 4, 2, 3, 5, 8]); + var e = q.unorderedElements; + expect(q.remove(3), true); + var it = e.iterator; + expect(q.remove(2), true); + expect(it.moveNext, throwsConcurrentModificationError); + it = e.iterator; + expect(q.remove(99), false); + expect(it.moveNext(), true); + expect(it.moveNext(), true); + expect(q.remove(5), true); + expect(it.moveNext, throwsConcurrentModificationError); + }); + + test('removeAll', () { + var q = HeapPriorityQueue((a, b) => a - b) + ..addAll([6, 4, 2, 3, 5, 8]); + var e = q.unorderedElements; + var it = e.iterator; + expect(it.moveNext(), true); + expect(it.moveNext(), true); + expect(q.removeAll(), hasLength(6)); + expect(it.moveNext, throwsConcurrentModificationError); + }); + + test('clear', () { + var q = HeapPriorityQueue((a, b) => a - b) + ..addAll([6, 4, 2, 3, 5, 8]); + var e = q.unorderedElements; + var it = e.iterator; + expect(it.moveNext(), true); + expect(it.moveNext(), true); + q.clear(); + expect(it.moveNext, throwsConcurrentModificationError); + }); + }); +} + +// Custom class. +// Class is comparable, comparators match normal and inverse order. +int compare(C c1, C c2) => c1.value - c2.value; +int compareNeg(C c1, C c2) => c2.value - c1.value; + +class C implements Comparable { + final int value; + const C(this.value); + @override + int get hashCode => value; + @override + bool operator ==(Object other) => other is C && value == other.value; + @override + int compareTo(C other) => value - other.value; + @override + String toString() => 'C($value)'; +} diff --git a/pkgs/collection/test/queue_list_test.dart b/pkgs/collection/test/queue_list_test.dart new file mode 100644 index 00000000..1550f927 --- /dev/null +++ b/pkgs/collection/test/queue_list_test.dart @@ -0,0 +1,307 @@ +// Copyright (c) 2014, 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('QueueList()', () { + test('creates an empty QueueList', () { + expect(QueueList(), isEmpty); + }); + + test('takes an initial capacity', () { + expect(QueueList(100), isEmpty); + }); + }); + + test('QueueList.from() copies the contents of an iterable', () { + expect(QueueList.from([1, 2, 3].skip(1)), equals([2, 3])); + }); + + group('add()', () { + test('adds an element to the end of the queue', () { + var queue = QueueList.from([1, 2, 3]); + queue.add(4); + expect(queue, equals([1, 2, 3, 4])); + }); + + test('expands a full queue', () { + var queue = atCapacity(); + queue.add(8); + expect(queue, equals([1, 2, 3, 4, 5, 6, 7, 8])); + }); + }); + + group('addAll()', () { + test('adds elements to the end of the queue', () { + var queue = QueueList.from([1, 2, 3]); + queue.addAll([4, 5, 6]); + expect(queue, equals([1, 2, 3, 4, 5, 6])); + }); + + test('expands a full queue', () { + var queue = atCapacity(); + queue.addAll([8, 9]); + expect(queue, equals([1, 2, 3, 4, 5, 6, 7, 8, 9])); + }); + }); + + group('addFirst()', () { + test('adds an element to the beginning of the queue', () { + var queue = QueueList.from([1, 2, 3]); + queue.addFirst(0); + expect(queue, equals([0, 1, 2, 3])); + }); + + test('expands a full queue', () { + var queue = atCapacity(); + queue.addFirst(0); + expect(queue, equals([0, 1, 2, 3, 4, 5, 6, 7])); + }); + }); + + group('removeFirst()', () { + test('removes an element from the beginning of the queue', () { + var queue = QueueList.from([1, 2, 3]); + expect(queue.removeFirst(), equals(1)); + expect(queue, equals([2, 3])); + }); + + test( + 'removes an element from the beginning of a queue with an internal ' + 'gap', () { + var queue = withInternalGap(); + expect(queue.removeFirst(), equals(1)); + expect(queue, equals([2, 3, 4, 5, 6, 7])); + }); + + test('removes an element from the beginning of a queue at capacity', () { + var queue = atCapacity(); + expect(queue.removeFirst(), equals(1)); + expect(queue, equals([2, 3, 4, 5, 6, 7])); + }); + + test('throws a StateError for an empty queue', () { + expect(QueueList().removeFirst, throwsStateError); + }); + }); + + group('removeLast()', () { + test('removes an element from the end of the queue', () { + var queue = QueueList.from([1, 2, 3]); + expect(queue.removeLast(), equals(3)); + expect(queue, equals([1, 2])); + }); + + test('removes an element from the end of a queue with an internal gap', () { + var queue = withInternalGap(); + expect(queue.removeLast(), equals(7)); + expect(queue, equals([1, 2, 3, 4, 5, 6])); + }); + + test('removes an element from the end of a queue at capacity', () { + var queue = atCapacity(); + expect(queue.removeLast(), equals(7)); + expect(queue, equals([1, 2, 3, 4, 5, 6])); + }); + + test('throws a StateError for an empty queue', () { + expect(QueueList().removeLast, throwsStateError); + }); + }); + + group('length', () { + test('returns the length of a queue', () { + expect(QueueList.from([1, 2, 3]).length, equals(3)); + }); + + test('returns the length of a queue with an internal gap', () { + expect(withInternalGap().length, equals(7)); + }); + + test('returns the length of a queue at capacity', () { + expect(atCapacity().length, equals(7)); + }); + }); + + group('length=', () { + test('shrinks a larger queue', () { + var queue = QueueList.from([1, 2, 3]); + queue.length = 1; + expect(queue, equals([1])); + }); + + test('grows a smaller queue', () { + var queue = QueueList.from([1, 2, 3]); + queue.length = 5; + expect(queue, equals([1, 2, 3, null, null])); + }); + + test('throws a RangeError if length is less than 0', () { + expect(() => QueueList().length = -1, throwsRangeError); + }); + + test('throws an UnsupportedError if element type is non-nullable', () { + expect(() => QueueList().length = 1, throwsUnsupportedError); + }); + }); + + group('[]', () { + test('returns individual entries in the queue', () { + var queue = QueueList.from([1, 2, 3]); + expect(queue[0], equals(1)); + expect(queue[1], equals(2)); + expect(queue[2], equals(3)); + }); + + test('returns individual entries in a queue with an internal gap', () { + var queue = withInternalGap(); + expect(queue[0], equals(1)); + expect(queue[1], equals(2)); + expect(queue[2], equals(3)); + expect(queue[3], equals(4)); + expect(queue[4], equals(5)); + expect(queue[5], equals(6)); + expect(queue[6], equals(7)); + }); + + test('throws a RangeError if the index is less than 0', () { + var queue = QueueList.from([1, 2, 3]); + expect(() => queue[-1], throwsRangeError); + }); + + test( + 'throws a RangeError if the index is greater than or equal to the ' + 'length', () { + var queue = QueueList.from([1, 2, 3]); + expect(() => queue[3], throwsRangeError); + }); + }); + + group('[]=', () { + test('sets individual entries in the queue', () { + var queue = QueueList.from([1, 2, 3]); + queue[0] = 'a'; + queue[1] = 'b'; + queue[2] = 'c'; + expect(queue, equals(['a', 'b', 'c'])); + }); + + test('sets individual entries in a queue with an internal gap', () { + var queue = withInternalGap(); + queue[0] = 'a'; + queue[1] = 'b'; + queue[2] = 'c'; + queue[3] = 'd'; + queue[4] = 'e'; + queue[5] = 'f'; + queue[6] = 'g'; + expect(queue, equals(['a', 'b', 'c', 'd', 'e', 'f', 'g'])); + }); + + test('throws a RangeError if the index is less than 0', () { + var queue = QueueList.from([1, 2, 3]); + expect(() { + queue[-1] = 0; + }, throwsRangeError); + }); + + test( + 'throws a RangeError if the index is greater than or equal to the ' + 'length', () { + var queue = QueueList.from([1, 2, 3]); + expect(() { + queue[3] = 4; + }, throwsRangeError); + }); + }); + + group('throws a modification error for', () { + dynamic queue; + setUp(() { + queue = QueueList.from([1, 2, 3]); + }); + + test('add', () { + expect(() => queue.forEach((_) => queue.add(4)), + throwsConcurrentModificationError); + }); + + test('addAll', () { + expect(() => queue.forEach((_) => queue.addAll([4, 5, 6])), + throwsConcurrentModificationError); + }); + + test('addFirst', () { + expect(() => queue.forEach((_) => queue.addFirst(0)), + throwsConcurrentModificationError); + }); + + test('removeFirst', () { + expect(() => queue.forEach((_) => queue.removeFirst()), + throwsConcurrentModificationError); + }); + + test('removeLast', () { + expect(() => queue.forEach((_) => queue.removeLast()), + throwsConcurrentModificationError); + }); + + test('length=', () { + expect(() => queue.forEach((_) => queue.length = 1), + throwsConcurrentModificationError); + }); + }); + + test('cast does not throw on mutation when the type is valid', () { + var patternQueue = QueueList()..addAll(['a', 'b']); + var stringQueue = patternQueue.cast(); + stringQueue.addAll(['c', 'd']); + expect(stringQueue, const TypeMatcher>(), + reason: 'Expected QueueList, got ${stringQueue.runtimeType}'); + + expect(stringQueue, ['a', 'b', 'c', 'd']); + + expect(patternQueue, stringQueue, reason: 'Should forward to original'); + }); + + test('cast throws on mutation when the type is not valid', () { + QueueList stringQueue = QueueList(); + var numQueue = stringQueue.cast(); + expect(numQueue, const TypeMatcher>(), + reason: 'Expected QueueList, got ${numQueue.runtimeType}'); + expect(() => numQueue.add(1), throwsA(isA())); + }); + + test('cast returns a new QueueList', () { + var queue = QueueList(); + expect(queue.cast(), isNot(same(queue))); + }); +} + +/// Returns a queue whose internal ring buffer is full enough that adding a new +/// element will expand it. +QueueList atCapacity() { + // Use addAll because `QueueList.from(list)` won't use the default initial + // capacity of 8. + return QueueList()..addAll([1, 2, 3, 4, 5, 6, 7]); +} + +/// Returns a queue whose internal tail has a lower index than its head. +QueueList withInternalGap() { + var queue = QueueList.from([null, null, null, null, 1, 2, 3, 4]); + for (var i = 0; i < 4; i++) { + queue.removeFirst(); + } + for (var i = 5; i < 8; i++) { + queue.addLast(i); + } + return queue; +} + +/// Returns a matcher that expects that a closure throws a +/// [ConcurrentModificationError]. +final throwsConcurrentModificationError = + throwsA(const TypeMatcher()); diff --git a/pkgs/collection/test/union_set_controller_test.dart b/pkgs/collection/test/union_set_controller_test.dart new file mode 100644 index 00000000..5d947529 --- /dev/null +++ b/pkgs/collection/test/union_set_controller_test.dart @@ -0,0 +1,35 @@ +// Copyright (c) 2016, 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() { + late UnionSetController controller; + late Set innerSet; + setUp(() { + innerSet = {1, 2, 3}; + controller = UnionSetController()..add(innerSet); + }); + + test('exposes a union set', () { + expect(controller.set, unorderedEquals([1, 2, 3])); + + controller.add({3, 4, 5}); + expect(controller.set, unorderedEquals([1, 2, 3, 4, 5])); + + controller.remove(innerSet); + expect(controller.set, unorderedEquals([3, 4, 5])); + }); + + test('exposes a disjoint union set', () { + expect(controller.set, unorderedEquals([1, 2, 3])); + + controller.add({4, 5, 6}); + expect(controller.set, unorderedEquals([1, 2, 3, 4, 5, 6])); + + controller.remove(innerSet); + expect(controller.set, unorderedEquals([4, 5, 6])); + }); +} diff --git a/pkgs/collection/test/union_set_test.dart b/pkgs/collection/test/union_set_test.dart new file mode 100644 index 00000000..d06faf3d --- /dev/null +++ b/pkgs/collection/test/union_set_test.dart @@ -0,0 +1,222 @@ +// Copyright (c) 2016, 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('with an empty outer set', () { + dynamic set; + setUp(() { + set = UnionSet({}); + }); + + test('length returns 0', () { + expect(set.length, equals(0)); + }); + + test('contains() returns false', () { + expect(set.contains(0), isFalse); + expect(set.contains(null), isFalse); + expect(set.contains('foo'), isFalse); + }); + + test('lookup() returns null', () { + expect(set.lookup(0), isNull); + expect(set.lookup(null), isNull); + expect(set.lookup('foo'), isNull); + }); + + test('toSet() returns an empty set', () { + expect(set.toSet(), isEmpty); + expect(set.toSet(), isNot(same(set))); + }); + + test("map() doesn't run on any elements", () { + expect(set.map(expectAsync1((dynamic _) {}, count: 0)), isEmpty); + }); + }); + + group('with multiple disjoint sets', () { + late Set set; + setUp(() { + set = UnionSet.from([ + {1, 2}, + {3, 4}, + {5}, + {}, + ], disjoint: true); + }); + + test('length returns the total length', () { + expect(set.length, equals(5)); + }); + + test('contains() returns whether any set contains the element', () { + expect(set.contains(1), isTrue); + expect(set.contains(4), isTrue); + expect(set.contains(5), isTrue); + expect(set.contains(6), isFalse); + }); + + test('lookup() returns elements that are in any set', () { + expect(set.lookup(1), equals(1)); + expect(set.lookup(4), equals(4)); + expect(set.lookup(5), equals(5)); + expect(set.lookup(6), isNull); + }); + + test('toSet() returns the union of all the sets', () { + expect(set.toSet(), unorderedEquals([1, 2, 3, 4, 5])); + expect(set.toSet(), isNot(same(set))); + }); + + test('map() maps the elements', () { + expect(set.map((i) => i * 2), unorderedEquals([2, 4, 6, 8, 10])); + }); + }); + + group('with multiple overlapping sets', () { + late Set set; + setUp(() { + set = UnionSet.from([ + {1, 2, 3}, + {3, 4}, + {5, 1}, + {}, + ]); + }); + + test('length returns the total length', () { + expect(set.length, equals(5)); + }); + + test('contains() returns whether any set contains the element', () { + expect(set.contains(1), isTrue); + expect(set.contains(4), isTrue); + expect(set.contains(5), isTrue); + expect(set.contains(6), isFalse); + }); + + test('lookup() returns elements that are in any set', () { + expect(set.lookup(1), equals(1)); + expect(set.lookup(4), equals(4)); + expect(set.lookup(5), equals(5)); + expect(set.lookup(6), isNull); + }); + + test('lookup() returns the first element in an ordered context', () { + var duration1 = const Duration(seconds: 0); + // ignore: prefer_const_constructors + var duration2 = Duration(seconds: 0); + expect(duration1, equals(duration2)); + expect(duration1, isNot(same(duration2))); + + var set = UnionSet.from([ + {duration1}, + {duration2} + ]); + + expect(set.lookup(const Duration(seconds: 0)), same(duration1)); + }); + + test('toSet() returns the union of all the sets', () { + expect(set.toSet(), unorderedEquals([1, 2, 3, 4, 5])); + expect(set.toSet(), isNot(same(set))); + }); + + test('map() maps the elements', () { + expect(set.map((i) => i * 2), unorderedEquals([2, 4, 6, 8, 10])); + }); + }); + + group('after an inner set was modified', () { + late Set set; + setUp(() { + var innerSet = {3, 7}; + set = UnionSet.from([ + {1, 2}, + {5}, + innerSet + ]); + + innerSet.add(4); + innerSet.remove(7); + }); + + test('length returns the total length', () { + expect(set.length, equals(5)); + }); + + test('contains() returns true for a new element', () { + expect(set.contains(4), isTrue); + }); + + test('contains() returns false for a removed element', () { + expect(set.contains(7), isFalse); + }); + + test('lookup() returns a new element', () { + expect(set.lookup(4), equals(4)); + }); + + test("lookup() doesn't returns a removed element", () { + expect(set.lookup(7), isNull); + }); + + test('toSet() returns the union of all the sets', () { + expect(set.toSet(), unorderedEquals([1, 2, 3, 4, 5])); + expect(set.toSet(), isNot(same(set))); + }); + + test('map() maps the elements', () { + expect(set.map((i) => i * 2), unorderedEquals([2, 4, 6, 8, 10])); + }); + }); + + group('after the outer set was modified', () { + late Set set; + setUp(() { + var innerSet = {6}; + var outerSet = { + {1, 2}, + {5}, + innerSet + }; + + set = UnionSet(outerSet); + outerSet.remove(innerSet); + outerSet.add({3, 4}); + }); + + test('length returns the total length', () { + expect(set.length, equals(5)); + }); + + test('contains() returns true for a new element', () { + expect(set.contains(4), isTrue); + }); + + test('contains() returns false for a removed element', () { + expect(set.contains(6), isFalse); + }); + + test('lookup() returns a new element', () { + expect(set.lookup(4), equals(4)); + }); + + test("lookup() doesn't returns a removed element", () { + expect(set.lookup(6), isNull); + }); + + test('toSet() returns the union of all the sets', () { + expect(set.toSet(), unorderedEquals([1, 2, 3, 4, 5])); + expect(set.toSet(), isNot(same(set))); + }); + + test('map() maps the elements', () { + expect(set.map((i) => i * 2), unorderedEquals([2, 4, 6, 8, 10])); + }); + }); +} diff --git a/pkgs/collection/test/unmodifiable_collection_test.dart b/pkgs/collection/test/unmodifiable_collection_test.dart new file mode 100644 index 00000000..12a9a0a7 --- /dev/null +++ b/pkgs/collection/test/unmodifiable_collection_test.dart @@ -0,0 +1,541 @@ +// Copyright (c) 2013, 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'; + +// Test unmodifiable collection views. +// The collections should pass through the operations that are allowed, +// an throw on the ones that aren't without affecting the original. + +void main() { + var list = []; + testUnmodifiableList(list, UnmodifiableListView(list), 'empty'); + list = [42]; + testUnmodifiableList(list, UnmodifiableListView(list), 'single-42'); + list = [7]; + testUnmodifiableList(list, UnmodifiableListView(list), 'single!42'); + list = [1, 42, 10]; + testUnmodifiableList(list, UnmodifiableListView(list), 'three-42'); + list = [1, 7, 10]; + testUnmodifiableList(list, UnmodifiableListView(list), 'three!42'); + + list = []; + testNonGrowableList(list, NonGrowableListView(list), 'empty'); + list = [42]; + testNonGrowableList(list, NonGrowableListView(list), 'single-42'); + list = [7]; + testNonGrowableList(list, NonGrowableListView(list), 'single!42'); + list = [1, 42, 10]; + testNonGrowableList(list, NonGrowableListView(list), 'three-42'); + list = [1, 7, 10]; + testNonGrowableList(list, NonGrowableListView(list), 'three!42'); + + var aSet = {}; + testUnmodifiableSet(aSet, UnmodifiableSetView(aSet), 'empty'); + aSet = {}; + testUnmodifiableSet(aSet, const UnmodifiableSetView.empty(), 'const empty'); + aSet = {42}; + testUnmodifiableSet(aSet, UnmodifiableSetView(aSet), 'single-42'); + aSet = {7}; + testUnmodifiableSet(aSet, UnmodifiableSetView(aSet), 'single!42'); + aSet = {1, 42, 10}; + testUnmodifiableSet(aSet, UnmodifiableSetView(aSet), 'three-42'); + aSet = {1, 7, 10}; + testUnmodifiableSet(aSet, UnmodifiableSetView(aSet), 'three!42'); +} + +void testUnmodifiableList(List original, List wrapped, String name) { + name = 'unmodifiable-list-$name'; + testIterable(original, wrapped, name); + testReadList(original, wrapped, name); + testNoWriteList(original, wrapped, name); + testNoChangeLengthList(original, wrapped, name); +} + +void testNonGrowableList(List original, List wrapped, String name) { + name = 'nongrowable-list-$name'; + testIterable(original, wrapped, name); + testReadList(original, wrapped, name); + testWriteList(original, wrapped, name); + testNoChangeLengthList(original, wrapped, name); +} + +void testUnmodifiableSet(Set original, Set wrapped, String name) { + name = 'unmodifiable-set-$name'; + testIterable(original, wrapped, name); + testReadSet(original, wrapped, name); + testNoChangeSet(original, wrapped, name); +} + +void testIterable(Iterable original, Iterable wrapped, String name) { + test('$name - any', () { + expect(wrapped.any((x) => true), equals(original.any((x) => true))); + expect(wrapped.any((x) => false), equals(original.any((x) => false))); + }); + + test('$name - contains', () { + expect(wrapped.contains(0), equals(original.contains(0))); + }); + + test('$name - elementAt', () { + if (original.isEmpty) { + expect(() => wrapped.elementAt(0), throwsRangeError); + } else { + expect(wrapped.elementAt(0), equals(original.elementAt(0))); + } + }); + + test('$name - every', () { + expect(wrapped.every((x) => true), equals(original.every((x) => true))); + expect(wrapped.every((x) => false), equals(original.every((x) => false))); + }); + + test('$name - expand', () { + expect( + wrapped.expand((x) => [x, x]), equals(original.expand((x) => [x, x]))); + }); + + test('$name - first', () { + if (original.isEmpty) { + expect(() => wrapped.first, throwsStateError); + } else { + expect(wrapped.first, equals(original.first)); + } + }); + + test('$name - firstWhere', () { + if (original.isEmpty) { + expect(() => wrapped.firstWhere((_) => true), throwsStateError); + } else { + expect(wrapped.firstWhere((_) => true), + equals(original.firstWhere((_) => true))); + } + expect(() => wrapped.firstWhere((_) => false), throwsStateError); + }); + + test('$name - fold', () { + expect(wrapped.fold(0, (dynamic x, y) => x + y), + equals(original.fold(0, (dynamic x, y) => x + y))); + }); + + test('$name - forEach', () { + var wrapCtr = 0; + var origCtr = 0; + // ignore: avoid_function_literals_in_foreach_calls + wrapped.forEach((x) { + wrapCtr += x; + }); + // ignore: avoid_function_literals_in_foreach_calls + original.forEach((x) { + origCtr += x; + }); + expect(wrapCtr, equals(origCtr)); + }); + + test('$name - isEmpty', () { + expect(wrapped.isEmpty, equals(original.isEmpty)); + }); + + test('$name - isNotEmpty', () { + expect(wrapped.isNotEmpty, equals(original.isNotEmpty)); + }); + + test('$name - iterator', () { + Iterator wrapIter = wrapped.iterator; + Iterator origIter = original.iterator; + while (origIter.moveNext()) { + expect(wrapIter.moveNext(), equals(true)); + expect(wrapIter.current, equals(origIter.current)); + } + expect(wrapIter.moveNext(), equals(false)); + }); + + test('$name - join', () { + expect(wrapped.join(''), equals(original.join(''))); + expect(wrapped.join('-'), equals(original.join('-'))); + }); + + test('$name - last', () { + if (original.isEmpty) { + expect(() => wrapped.last, throwsStateError); + } else { + expect(wrapped.last, equals(original.last)); + } + }); + + test('$name - lastWhere', () { + if (original.isEmpty) { + expect(() => wrapped.lastWhere((_) => true), throwsStateError); + } else { + expect(wrapped.lastWhere((_) => true), + equals(original.lastWhere((_) => true))); + } + expect(() => wrapped.lastWhere((_) => false), throwsStateError); + }); + + test('$name - length', () { + expect(wrapped.length, equals(original.length)); + }); + + test('$name - map', () { + expect(wrapped.map((x) => '[$x]'), equals(original.map((x) => '[$x]'))); + }); + + test('$name - reduce', () { + if (original.isEmpty) { + expect(() => wrapped.reduce((x, y) => x + y), throwsStateError); + } else { + expect(wrapped.reduce((x, y) => x + y), + equals(original.reduce((x, y) => x + y))); + } + }); + + test('$name - single', () { + if (original.length != 1) { + expect(() => wrapped.single, throwsStateError); + } else { + expect(wrapped.single, equals(original.single)); + } + }); + + test('$name - singleWhere', () { + if (original.length != 1) { + expect(() => wrapped.singleWhere((_) => true), throwsStateError); + } else { + expect(wrapped.singleWhere((_) => true), + equals(original.singleWhere((_) => true))); + } + expect(() => wrapped.singleWhere((_) => false), throwsStateError); + }); + + test('$name - skip', () { + expect(wrapped.skip(0), orderedEquals(original.skip(0))); + expect(wrapped.skip(1), orderedEquals(original.skip(1))); + expect(wrapped.skip(5), orderedEquals(original.skip(5))); + }); + + test('$name - skipWhile', () { + expect(wrapped.skipWhile((x) => true), + orderedEquals(original.skipWhile((x) => true))); + expect(wrapped.skipWhile((x) => false), + orderedEquals(original.skipWhile((x) => false))); + expect(wrapped.skipWhile((x) => x != 42), + orderedEquals(original.skipWhile((x) => x != 42))); + }); + + test('$name - take', () { + expect(wrapped.take(0), orderedEquals(original.take(0))); + expect(wrapped.take(1), orderedEquals(original.take(1))); + expect(wrapped.take(5), orderedEquals(original.take(5))); + }); + + test('$name - takeWhile', () { + expect(wrapped.takeWhile((x) => true), + orderedEquals(original.takeWhile((x) => true))); + expect(wrapped.takeWhile((x) => false), + orderedEquals(original.takeWhile((x) => false))); + expect(wrapped.takeWhile((x) => x != 42), + orderedEquals(original.takeWhile((x) => x != 42))); + }); + + test('$name - toList', () { + expect(wrapped.toList(), orderedEquals(original.toList())); + expect(wrapped.toList(growable: false), + orderedEquals(original.toList(growable: false))); + }); + + test('$name - toSet', () { + expect(wrapped.toSet(), unorderedEquals(original.toSet())); + }); + + test('$name - where', () { + expect( + wrapped.where((x) => true), orderedEquals(original.where((x) => true))); + expect(wrapped.where((x) => false), + orderedEquals(original.where((x) => false))); + expect(wrapped.where((x) => x != 42), + orderedEquals(original.where((x) => x != 42))); + }); +} + +void testReadList(List original, List wrapped, String name) { + test('$name - length', () { + expect(wrapped.length, equals(original.length)); + }); + + test('$name - isEmpty', () { + expect(wrapped.isEmpty, equals(original.isEmpty)); + }); + + test('$name - isNotEmpty', () { + expect(wrapped.isNotEmpty, equals(original.isNotEmpty)); + }); + + test('$name - []', () { + if (original.isEmpty) { + expect(() { + // ignore: unnecessary_statements + wrapped[0]; + }, throwsRangeError); + } else { + expect(wrapped[0], equals(original[0])); + } + }); + + test('$name - indexOf', () { + expect(wrapped.indexOf(42), equals(original.indexOf(42))); + }); + + test('$name - lastIndexOf', () { + expect(wrapped.lastIndexOf(42), equals(original.lastIndexOf(42))); + }); + + test('$name - getRange', () { + var len = original.length; + expect(wrapped.getRange(0, len), equals(original.getRange(0, len))); + expect(wrapped.getRange(len ~/ 2, len), + equals(original.getRange(len ~/ 2, len))); + expect( + wrapped.getRange(0, len ~/ 2), equals(original.getRange(0, len ~/ 2))); + }); + + test('$name - sublist', () { + var len = original.length; + expect(wrapped.sublist(0), equals(original.sublist(0))); + expect(wrapped.sublist(len ~/ 2), equals(original.sublist(len ~/ 2))); + expect(wrapped.sublist(0, len ~/ 2), equals(original.sublist(0, len ~/ 2))); + }); + + test('$name - asMap', () { + expect(wrapped.asMap(), equals(original.asMap())); + }); +} + +void testNoWriteList(List original, List wrapped, String name) { + var copy = List.of(original); + + void testThrows(String name, void Function() thunk) { + test(name, () { + expect(thunk, throwsUnsupportedError); + // No modifications happened. + expect(original, equals(copy)); + }); + } + + testThrows('$name - []= throws', () { + wrapped[0] = 42; + }); + + testThrows('$name - sort throws', () { + wrapped.sort(); + }); + + testThrows('$name - fillRange throws', () { + wrapped.fillRange(0, wrapped.length, 42); + }); + + testThrows('$name - setRange throws', () { + wrapped.setRange( + 0, wrapped.length, Iterable.generate(wrapped.length, (i) => i)); + }); + + testThrows('$name - setAll throws', () { + wrapped.setAll(0, Iterable.generate(wrapped.length, (i) => i)); + }); +} + +void testWriteList(List original, List wrapped, String name) { + var copy = List.of(original); + + test('$name - []=', () { + if (original.isNotEmpty) { + var originalFirst = original[0]; + wrapped[0] = originalFirst + 1; + expect(original[0], equals(originalFirst + 1)); + original[0] = originalFirst; + } else { + expect(() { + wrapped[0] = 42; + }, throwsRangeError); + } + }); + + test('$name - sort', () { + var sortCopy = List.of(original); + sortCopy.sort(); + wrapped.sort(); + expect(original, orderedEquals(sortCopy)); + original.setAll(0, copy); + }); + + test('$name - fillRange', () { + wrapped.fillRange(0, wrapped.length, 37); + for (var i = 0; i < original.length; i++) { + expect(original[i], equals(37)); + } + original.setAll(0, copy); + }); + + test('$name - setRange', () { + List reverseList = original.reversed.toList(); + wrapped.setRange(0, wrapped.length, reverseList); + expect(original, equals(reverseList)); + original.setAll(0, copy); + }); + + test('$name - setAll', () { + List reverseList = original.reversed.toList(); + wrapped.setAll(0, reverseList); + expect(original, equals(reverseList)); + original.setAll(0, copy); + }); +} + +void testNoChangeLengthList( + List original, List wrapped, String name) { + var copy = List.of(original); + + void testThrows(String name, void Function() thunk) { + test(name, () { + expect(thunk, throwsUnsupportedError); + // No modifications happened. + expect(original, equals(copy)); + }); + } + + testThrows('$name - length= throws', () { + wrapped.length = 100; + }); + + testThrows('$name - add throws', () { + wrapped.add(42); + }); + + testThrows('$name - addAll throws', () { + wrapped.addAll([42]); + }); + + testThrows('$name - insert throws', () { + wrapped.insert(0, 42); + }); + + testThrows('$name - insertAll throws', () { + wrapped.insertAll(0, [42]); + }); + + testThrows('$name - remove throws', () { + wrapped.remove(42); + }); + + testThrows('$name - removeAt throws', () { + wrapped.removeAt(0); + }); + + testThrows('$name - removeLast throws', () { + wrapped.removeLast(); + }); + + testThrows('$name - removeWhere throws', () { + wrapped.removeWhere((element) => false); + }); + + testThrows('$name - retainWhere throws', () { + wrapped.retainWhere((element) => true); + }); + + testThrows('$name - removeRange throws', () { + wrapped.removeRange(0, wrapped.length); + }); + + testThrows('$name - replaceRange throws', () { + wrapped.replaceRange(0, wrapped.length, [42]); + }); + + testThrows('$name - clear throws', () { + wrapped.clear(); + }); +} + +void testReadSet(Set original, Set wrapped, String name) { + var copy = Set.of(original); + + test('$name - containsAll', () { + expect(wrapped.containsAll(copy), isTrue); + expect(wrapped.containsAll(copy.toList()), isTrue); + expect(wrapped.containsAll([]), isTrue); + expect(wrapped.containsAll([42]), equals(original.containsAll([42]))); + }); + + test('$name - intersection', () { + expect(wrapped.intersection({}), isEmpty); + expect(wrapped.intersection(copy), unorderedEquals(original)); + expect( + wrapped.intersection({42}), Set.of(original.contains(42) ? [42] : [])); + }); + + test('$name - union', () { + expect(wrapped.union({}), unorderedEquals(original)); + expect(wrapped.union(copy), unorderedEquals(original)); + expect(wrapped.union({42}), equals(original.union({42}))); + }); + + test('$name - difference', () { + expect(wrapped.difference({}), unorderedEquals(original)); + expect(wrapped.difference(copy), isEmpty); + expect(wrapped.difference({42}), equals(original.difference({42}))); + }); +} + +void testNoChangeSet(Set original, Set wrapped, String name) { + var originalElements = original.toList(); + + void testThrows(String name, void Function() thunk) { + test(name, () { + expect(thunk, throwsUnsupportedError); + // No modifications happened. + expect(original.toList(), equals(originalElements)); + }); + } + + testThrows('$name - add throws', () { + wrapped.add(42); + }); + + testThrows('$name - addAll throws', () { + wrapped.addAll([42]); + }); + + testThrows('$name - addAll empty throws', () { + wrapped.addAll([]); + }); + + testThrows('$name - remove throws', () { + wrapped.remove(42); + }); + + testThrows('$name - removeAll throws', () { + wrapped.removeAll([42]); + }); + + testThrows('$name - removeAll empty throws', () { + wrapped.removeAll([]); + }); + + testThrows('$name - retainAll throws', () { + wrapped.retainAll([42]); + }); + + testThrows('$name - removeWhere throws', () { + wrapped.removeWhere((_) => false); + }); + + testThrows('$name - retainWhere throws', () { + wrapped.retainWhere((_) => true); + }); + + testThrows('$name - clear throws', () { + wrapped.clear(); + }); +} diff --git a/pkgs/collection/test/wrapper_test.dart b/pkgs/collection/test/wrapper_test.dart new file mode 100644 index 00000000..65a693f7 --- /dev/null +++ b/pkgs/collection/test/wrapper_test.dart @@ -0,0 +1,696 @@ +// Copyright (c) 2013, 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. + +// ignore_for_file: unnecessary_statements + +/// Tests wrapper utilities. +@TestOn('vm') +library; + +import 'dart:collection'; +import 'dart:mirrors'; + +import 'package:collection/collection.dart'; +import 'package:test/test.dart'; + +// Test that any member access/call on the wrapper object is equal to +// an expected access on the wrapped object. +// This is implemented by capturing accesses using noSuchMethod and comparing +// them to expected accesses captured previously. + +// Compare two Invocations for having equal type and arguments. +void testInvocations(Invocation i1, Invocation i2) { + var name = '${i1.memberName}'; + expect(i1.isGetter, equals(i2.isGetter), reason: name); + expect(i1.isSetter, equals(i2.isSetter), reason: name); + expect(i1.memberName, equals(i2.memberName), reason: name); + expect(i1.positionalArguments, equals(i2.positionalArguments), reason: name); + expect(i1.namedArguments, equals(i2.namedArguments), reason: name); +} + +/// Utility class to record a member access and a member access on a wrapped +/// object, and compare them for equality. +/// +/// Use as `(expector..someAccess()).equals.someAccess();`. +/// Alle the intercepted member accesses returns `null`. +abstract class Expector { + dynamic wrappedChecker(Invocation i); + // After calling any member on the Expector, equals is an object that expects + // the *same* invocation on the wrapped object. + dynamic equals; + + InstanceMirror get mirror; + + @override + dynamic noSuchMethod(Invocation actual) { + equals = wrappedChecker(actual); + return mirror.delegate(actual); + } + + @override + String toString() { + // Cannot return an _Equals object since toString must return a String. + // Just set equals and return a string. + equals = wrappedChecker(toStringInvocation); + return ''; + } +} + +// Parameterization of noSuchMethod. Calls [_action] on every +// member invocation. +class InvocationChecker { + final Invocation _expected; + final InstanceMirror _instanceMirror; + + InvocationChecker(this._expected, this._instanceMirror); + + @override + dynamic noSuchMethod(Invocation actual) { + testInvocations(_expected, actual); + return _instanceMirror.delegate(actual); + } + + @override + String toString() { + testInvocations(_expected, toStringInvocation); + return ''; + } + // Could also handle runtimeType, hashCode and == the same way as + // toString, but we are not testing them since collections generally + // don't override those and so the wrappers don't forward those. +} + +final toStringInvocation = Invocation.method(#toString, const []); + +// InvocationCheckers with types Queue, Set, List or Iterable to allow them as +// argument to DelegatingIterable/Set/List/Queue. +class IterableInvocationChecker extends InvocationChecker + implements Iterable { + IterableInvocationChecker(super.expected, super.mirror); +} + +class ListInvocationChecker extends InvocationChecker implements List { + ListInvocationChecker(super.expected, super.mirror); +} + +class SetInvocationChecker extends InvocationChecker implements Set { + SetInvocationChecker(super.expected, super.mirror); +} + +class QueueInvocationChecker extends InvocationChecker implements Queue { + QueueInvocationChecker(super.expected, super.mirror); +} + +class MapInvocationChecker extends InvocationChecker + implements Map { + MapInvocationChecker(super.expected, super.mirror); +} + +// Expector that wraps in DelegatingIterable. +class IterableExpector extends Expector implements Iterable { + @override + final InstanceMirror mirror; + + IterableExpector(Iterable realInstance) : mirror = reflect(realInstance); + + @override + dynamic wrappedChecker(Invocation i) => + DelegatingIterable(IterableInvocationChecker(i, mirror)); +} + +// Expector that wraps in DelegatingList. +class ListExpector extends IterableExpector implements List { + ListExpector(List super.realInstance); + + @override + dynamic wrappedChecker(Invocation i) => + DelegatingList(ListInvocationChecker(i, mirror)); +} + +// Expector that wraps in DelegatingSet. +class SetExpector extends IterableExpector implements Set { + SetExpector(Set super.realInstance); + + @override + dynamic wrappedChecker(Invocation i) => + DelegatingSet(SetInvocationChecker(i, mirror)); +} + +// Expector that wraps in DelegatingSet. +class QueueExpector extends IterableExpector implements Queue { + QueueExpector(Queue super.realInstance); + + @override + dynamic wrappedChecker(Invocation i) => + DelegatingQueue(QueueInvocationChecker(i, mirror)); +} + +// Expector that wraps in DelegatingMap. +class MapExpector extends Expector implements Map { + @override + final InstanceMirror mirror; + + MapExpector(Map realInstance) : mirror = reflect(realInstance); + + @override + dynamic wrappedChecker(Invocation i) => + DelegatingMap(MapInvocationChecker(i, mirror)); +} + +// Utility values to use as arguments in calls. +// ignore: prefer_void_to_null +Null func0() => null; +dynamic func1(dynamic x) => null; +dynamic func2(dynamic x, dynamic y) => null; +bool boolFunc(dynamic x) => true; +Iterable expandFunc(dynamic x) => [x]; +dynamic foldFunc(dynamic previous, dynamic next) => previous; +int compareFunc(dynamic x, dynamic y) => 0; +int val = 10; + +void main() { + void testIterable(IterableExpector expect) { + (expect..any(boolFunc)).equals.any(boolFunc); + (expect..contains(val)).equals.contains(val); + (expect..elementAt(0)).equals.elementAt(0); + (expect..every(boolFunc)).equals.every(boolFunc); + (expect..expand(expandFunc)).equals.expand(expandFunc); + (expect..first).equals.first; + // Default values of the Iterable interface will be added in the + // second call to firstWhere, so we must record them in our + // expectation (which doesn't have the interface implemented or + // its default values). + (expect..firstWhere(boolFunc, orElse: null)).equals.firstWhere(boolFunc); + (expect..firstWhere(boolFunc, orElse: func0)) + .equals + .firstWhere(boolFunc, orElse: func0); + (expect..fold(42, foldFunc)).equals.fold(42, foldFunc); + (expect..forEach(boolFunc)).equals.forEach(boolFunc); + (expect..isEmpty).equals.isEmpty; + (expect..isNotEmpty).equals.isNotEmpty; + (expect..iterator).equals.iterator; + (expect..join('')).equals.join(); + (expect..join('X')).equals.join('X'); + (expect..last).equals.last; + (expect..lastWhere(boolFunc, orElse: null)).equals.lastWhere(boolFunc); + (expect..lastWhere(boolFunc, orElse: func0)) + .equals + .lastWhere(boolFunc, orElse: func0); + (expect..length).equals.length; + (expect..map(func1)).equals.map(func1); + (expect..reduce(func2)).equals.reduce(func2); + (expect..single).equals.single; + (expect..singleWhere(boolFunc, orElse: null)).equals.singleWhere(boolFunc); + (expect..skip(5)).equals.skip(5); + (expect..skipWhile(boolFunc)).equals.skipWhile(boolFunc); + (expect..take(5)).equals.take(5); + (expect..takeWhile(boolFunc)).equals.takeWhile(boolFunc); + (expect..toList(growable: true)).equals.toList(); + (expect..toList(growable: true)).equals.toList(growable: true); + (expect..toList(growable: false)).equals.toList(growable: false); + (expect..toSet()).equals.toSet(); + (expect..toString()).equals.toString(); + (expect..where(boolFunc)).equals.where(boolFunc); + } + + void testList(ListExpector expect) { + testIterable(expect); + // Later expects require at least 5 items + (expect..add(val)).equals.add(val); + (expect..addAll([val, val, val, val])).equals.addAll([val, val, val, val]); + + (expect..[4]).equals[4]; + (expect..[4] = 5).equals[4] = 5; + + (expect..asMap()).equals.asMap(); + (expect..fillRange(4, 5, null)).equals.fillRange(4, 5); + (expect..fillRange(4, 5, val)).equals.fillRange(4, 5, val); + (expect..getRange(4, 5)).equals.getRange(4, 5); + (expect..indexOf(val, 0)).equals.indexOf(val); + (expect..indexOf(val, 4)).equals.indexOf(val, 4); + (expect..insert(4, val)).equals.insert(4, val); + (expect..insertAll(4, [val])).equals.insertAll(4, [val]); + (expect..lastIndexOf(val, null)).equals.lastIndexOf(val); + (expect..lastIndexOf(val, 4)).equals.lastIndexOf(val, 4); + (expect..replaceRange(4, 5, [val])).equals.replaceRange(4, 5, [val]); + (expect..retainWhere(boolFunc)).equals.retainWhere(boolFunc); + (expect..reversed).equals.reversed; + (expect..setAll(4, [val])).equals.setAll(4, [val]); + (expect..setRange(4, 5, [val], 0)).equals.setRange(4, 5, [val]); + (expect..setRange(4, 5, [val, val], 1)) + .equals + .setRange(4, 5, [val, val], 1); + (expect..sort()).equals.sort(); + (expect..sort(compareFunc)).equals.sort(compareFunc); + (expect..sublist(4, null)).equals.sublist(4); + (expect..sublist(4, 5)).equals.sublist(4, 5); + + // Do destructive apis last so other ones can work properly + (expect..removeAt(4)).equals.removeAt(4); + (expect..remove(val)).equals.remove(val); + (expect..removeLast()).equals.removeLast(); + (expect..removeRange(4, 5)).equals.removeRange(4, 5); + (expect..removeWhere(boolFunc)).equals.removeWhere(boolFunc); + (expect..length = 5).equals.length = 5; + (expect..clear()).equals.clear(); + } + + void testSet(SetExpector expect) { + testIterable(expect); + var set = {}; + (expect..add(val)).equals.add(val); + (expect..addAll([val])).equals.addAll([val]); + (expect..clear()).equals.clear(); + (expect..containsAll([val])).equals.containsAll([val]); + (expect..difference(set)).equals.difference(set); + (expect..intersection(set)).equals.intersection(set); + (expect..remove(val)).equals.remove(val); + (expect..removeAll([val])).equals.removeAll([val]); + (expect..removeWhere(boolFunc)).equals.removeWhere(boolFunc); + (expect..retainAll([val])).equals.retainAll([val]); + (expect..retainWhere(boolFunc)).equals.retainWhere(boolFunc); + (expect..union(set)).equals.union(set); + } + + void testQueue(QueueExpector expect) { + testIterable(expect); + (expect..add(val)).equals.add(val); + (expect..addAll([val])).equals.addAll([val]); + (expect..addFirst(val)).equals.addFirst(val); + (expect..addLast(val)).equals.addLast(val); + (expect..remove(val)).equals.remove(val); + (expect..removeFirst()).equals.removeFirst(); + (expect..removeLast()).equals.removeLast(); + (expect..clear()).equals.clear(); + } + + void testMap(MapExpector expect) { + var map = {}; + (expect..[val]).equals[val]; + (expect..[val] = val).equals[val] = val; + (expect..addAll(map)).equals.addAll(map); + (expect..clear()).equals.clear(); + (expect..containsKey(val)).equals.containsKey(val); + (expect..containsValue(val)).equals.containsValue(val); + (expect..forEach(func2)).equals.forEach(func2); + (expect..isEmpty).equals.isEmpty; + (expect..isNotEmpty).equals.isNotEmpty; + (expect..keys).equals.keys; + (expect..length).equals.length; + (expect..putIfAbsent(val, func0)).equals.putIfAbsent(val, func0); + (expect..remove(val)).equals.remove(val); + (expect..values).equals.values; + (expect..toString()).equals.toString(); + } + + // Runs tests of Set behavior. + // + // [setUpSet] should return a set with two elements: "foo" and "bar". + void testTwoElementSet(Set Function() setUpSet) { + group('with two elements', () { + late Set set; + setUp(() => set = setUpSet()); + + test('.any', () { + expect(set.any((element) => element == 'foo'), isTrue); + expect(set.any((element) => element == 'baz'), isFalse); + }); + + test('.elementAt', () { + expect(set.elementAt(0), equals('foo')); + expect(set.elementAt(1), equals('bar')); + expect(() => set.elementAt(2), throwsRangeError); + }); + + test('.every', () { + expect(set.every((element) => element == 'foo'), isFalse); + expect(set.every((element) => true), isTrue); + }); + + test('.expand', () { + expect(set.expand((element) { + return [element.substring(0, 1), element.substring(1)]; + }), equals(['f', 'oo', 'b', 'ar'])); + }); + + test('.first', () { + expect(set.first, equals('foo')); + }); + + test('.firstWhere', () { + expect(set.firstWhere((element) => true), equals('foo')); + expect(set.firstWhere((element) => element.startsWith('b')), + equals('bar')); + expect(() => set.firstWhere((element) => element is int), + throwsStateError); + expect(set.firstWhere((element) => element is int, orElse: () => 'baz'), + equals('baz')); + }); + + test('.fold', () { + expect( + set.fold( + 'start', (dynamic previous, element) => previous + element), + equals('startfoobar')); + }); + + test('.forEach', () { + var values = []; + set.forEach(values.add); + expect(values, equals(['foo', 'bar'])); + }); + + test('.iterator', () { + var values = []; + for (var element in set) { + values.add(element); + } + expect(values, equals(['foo', 'bar'])); + }); + + test('.join', () { + expect(set.join(', '), equals('foo, bar')); + }); + + test('.last', () { + expect(set.last, equals('bar')); + }); + + test('.lastWhere', () { + expect(set.lastWhere((element) => true), equals('bar')); + expect( + set.lastWhere((element) => element.startsWith('f')), equals('foo')); + expect( + () => set.lastWhere((element) => element is int), throwsStateError); + expect(set.lastWhere((element) => element is int, orElse: () => 'baz'), + equals('baz')); + }); + + test('.map', () { + expect( + set.map((element) => element.substring(1)), equals(['oo', 'ar'])); + }); + + test('.reduce', () { + expect(set.reduce((previous, element) => previous + element), + equals('foobar')); + }); + + test('.singleWhere', () { + expect(() => set.singleWhere((element) => element == 'baz'), + throwsStateError); + expect(set.singleWhere((element) => element == 'foo'), 'foo'); + expect(() => set.singleWhere((element) => true), throwsStateError); + }); + + test('.skip', () { + expect(set.skip(0), equals(['foo', 'bar'])); + expect(set.skip(1), equals(['bar'])); + expect(set.skip(2), equals([])); + }); + + test('.skipWhile', () { + expect(set.skipWhile((element) => element.startsWith('f')), + equals(['bar'])); + expect(set.skipWhile((element) => element.startsWith('z')), + equals(['foo', 'bar'])); + expect(set.skipWhile((element) => true), equals([])); + }); + + test('.take', () { + expect(set.take(0), equals([])); + expect(set.take(1), equals(['foo'])); + expect(set.take(2), equals(['foo', 'bar'])); + }); + + test('.takeWhile', () { + expect(set.takeWhile((element) => element.startsWith('f')), + equals(['foo'])); + expect(set.takeWhile((element) => element.startsWith('z')), equals([])); + expect(set.takeWhile((element) => true), equals(['foo', 'bar'])); + }); + + test('.toList', () { + expect(set.toList(), equals(['foo', 'bar'])); + expect(() => set.toList(growable: false).add('baz'), + throwsUnsupportedError); + expect(set.toList()..add('baz'), equals(['foo', 'bar', 'baz'])); + }); + + test('.toSet', () { + expect(set.toSet(), equals({'foo', 'bar'})); + }); + + test('.where', () { + expect( + set.where((element) => element.startsWith('f')), equals(['foo'])); + expect(set.where((element) => element.startsWith('z')), equals([])); + expect(set.whereType(), equals(['foo', 'bar'])); + }); + + test('.containsAll', () { + expect(set.containsAll(['foo', 'bar']), isTrue); + expect(set.containsAll(['foo']), isTrue); + expect(set.containsAll(['foo', 'bar', 'qux']), isFalse); + }); + + test('.difference', () { + expect(set.difference({'foo', 'baz'}), equals({'bar'})); + }); + + test('.intersection', () { + expect(set.intersection({'foo', 'baz'}), equals({'foo'})); + }); + + test('.union', () { + expect(set.union({'foo', 'baz'}), equals({'foo', 'bar', 'baz'})); + }); + }); + } + + test('Iterable', () { + testIterable(IterableExpector([1])); + }); + + test('List', () { + testList(ListExpector([1])); + }); + + test('Set', () { + testSet(SetExpector({1})); + }); + + test('Queue', () { + testQueue(QueueExpector(Queue.of([1]))); + }); + + test('Map', () { + testMap(MapExpector({'a': 'b'})); + }); + + group('MapKeySet', () { + late Map map; + late Set set; + + setUp(() { + map = {}; + set = MapKeySet(map); + }); + + testTwoElementSet(() { + map['foo'] = 1; + map['bar'] = 2; + return set; + }); + + test('.single', () { + expect(() => set.single, throwsStateError); + map['foo'] = 1; + expect(set.single, equals('foo')); + map['bar'] = 1; + expect(() => set.single, throwsStateError); + }); + + test('.toString', () { + expect(set.toString(), equals('{}')); + map['foo'] = 1; + map['bar'] = 2; + expect(set.toString(), equals('{foo, bar}')); + }); + + test('.contains', () { + expect(set.contains('foo'), isFalse); + map['foo'] = 1; + expect(set.contains('foo'), isTrue); + }); + + test('.isEmpty', () { + expect(set.isEmpty, isTrue); + map['foo'] = 1; + expect(set.isEmpty, isFalse); + }); + + test('.isNotEmpty', () { + expect(set.isNotEmpty, isFalse); + map['foo'] = 1; + expect(set.isNotEmpty, isTrue); + }); + + test('.length', () { + expect(set, hasLength(0)); + map['foo'] = 1; + expect(set, hasLength(1)); + map['bar'] = 2; + expect(set, hasLength(2)); + }); + + test('is unmodifiable', () { + expect(() => set.add('baz'), throwsUnsupportedError); + expect(() => set.addAll(['baz', 'bang']), throwsUnsupportedError); + expect(() => set.remove('foo'), throwsUnsupportedError); + expect(() => set.removeAll(['baz', 'bang']), throwsUnsupportedError); + expect(() => set.retainAll(['foo']), throwsUnsupportedError); + expect(() => set.removeWhere((_) => true), throwsUnsupportedError); + expect(() => set.retainWhere((_) => true), throwsUnsupportedError); + expect(() => set.clear(), throwsUnsupportedError); + }); + }); + + group('MapValueSet', () { + late Map map; + late Set set; + + setUp(() { + map = {}; + set = + MapValueSet(map, (string) => string.substring(0, 1)); + }); + + testTwoElementSet(() { + map['f'] = 'foo'; + map['b'] = 'bar'; + return set; + }); + + test('.single', () { + expect(() => set.single, throwsStateError); + map['f'] = 'foo'; + expect(set.single, equals('foo')); + map['b'] = 'bar'; + expect(() => set.single, throwsStateError); + }); + + test('.toString', () { + expect(set.toString(), equals('{}')); + map['f'] = 'foo'; + map['b'] = 'bar'; + expect(set.toString(), equals('{foo, bar}')); + }); + + test('.contains', () { + expect(set.contains('foo'), isFalse); + map['f'] = 'foo'; + expect(set.contains('foo'), isTrue); + expect(set.contains('fblthp'), isTrue); + }); + + test('.isEmpty', () { + expect(set.isEmpty, isTrue); + map['f'] = 'foo'; + expect(set.isEmpty, isFalse); + }); + + test('.isNotEmpty', () { + expect(set.isNotEmpty, isFalse); + map['f'] = 'foo'; + expect(set.isNotEmpty, isTrue); + }); + + test('.length', () { + expect(set, hasLength(0)); + map['f'] = 'foo'; + expect(set, hasLength(1)); + map['b'] = 'bar'; + expect(set, hasLength(2)); + }); + + test('.lookup', () { + map['f'] = 'foo'; + expect(set.lookup('fblthp'), equals('foo')); + expect(set.lookup('bar'), isNull); + }); + + test('.add', () { + set.add('foo'); + set.add('bar'); + expect(map, equals({'f': 'foo', 'b': 'bar'})); + }); + + test('.addAll', () { + set.addAll(['foo', 'bar']); + expect(map, equals({'f': 'foo', 'b': 'bar'})); + }); + + test('.clear', () { + map['f'] = 'foo'; + map['b'] = 'bar'; + set.clear(); + expect(map, isEmpty); + }); + + test('.remove', () { + map['f'] = 'foo'; + map['b'] = 'bar'; + set.remove('fblthp'); + expect(map, equals({'b': 'bar'})); + }); + + test('.removeAll', () { + map['f'] = 'foo'; + map['b'] = 'bar'; + map['q'] = 'qux'; + set.removeAll(['fblthp', 'qux']); + expect(map, equals({'b': 'bar'})); + }); + + test('.removeWhere', () { + map['f'] = 'foo'; + map['b'] = 'bar'; + map['q'] = 'qoo'; + set.removeWhere((element) => element.endsWith('o')); + expect(map, equals({'b': 'bar'})); + }); + + test('.retainAll', () { + map['f'] = 'foo'; + map['b'] = 'bar'; + map['q'] = 'qux'; + set.retainAll(['fblthp', 'qux']); + expect(map, equals({'f': 'foo', 'q': 'qux'})); + }); + + test('.retainAll respects an unusual notion of equality', () { + map = HashMap( + equals: (value1, value2) => + value1.toLowerCase() == value2.toLowerCase(), + hashCode: (value) => value.toLowerCase().hashCode); + set = + MapValueSet(map, (string) => string.substring(0, 1)); + + map['f'] = 'foo'; + map['B'] = 'bar'; + map['Q'] = 'qux'; + set.retainAll(['fblthp', 'qux']); + expect(map, equals({'f': 'foo', 'Q': 'qux'})); + }); + + test('.retainWhere', () { + map['f'] = 'foo'; + map['b'] = 'bar'; + map['q'] = 'qoo'; + set.retainWhere((element) => element.endsWith('o')); + expect(map, equals({'f': 'foo', 'q': 'qoo'})); + }); + }); +}