├── .github ├── dependabot.yaml └── workflows │ ├── ci.yml │ ├── no-response.yml │ └── publish.yaml ├── .gitignore ├── AUTHORS ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── lib ├── algorithms.dart ├── collection.dart ├── equality.dart ├── iterable_zip.dart ├── priority_queue.dart ├── src │ ├── algorithms.dart │ ├── boollist.dart │ ├── canonicalized_map.dart │ ├── combined_wrappers │ │ ├── combined_iterable.dart │ │ ├── combined_iterator.dart │ │ ├── combined_list.dart │ │ └── combined_map.dart │ ├── comparators.dart │ ├── empty_unmodifiable_set.dart │ ├── equality.dart │ ├── equality_map.dart │ ├── equality_set.dart │ ├── functions.dart │ ├── iterable_extensions.dart │ ├── iterable_zip.dart │ ├── list_extensions.dart │ ├── priority_queue.dart │ ├── queue_list.dart │ ├── union_set.dart │ ├── union_set_controller.dart │ ├── unmodifiable_wrappers.dart │ ├── utils.dart │ └── wrappers.dart └── wrappers.dart ├── pubspec.yaml └── test ├── algorithms_test.dart ├── analysis_options.yaml ├── boollist_test.dart ├── canonicalized_map_test.dart ├── combined_wrapper ├── iterable_test.dart ├── list_test.dart └── map_test.dart ├── comparators_test.dart ├── equality_map_test.dart ├── equality_set_test.dart ├── equality_test.dart ├── extensions_test.dart ├── functions_test.dart ├── ignore_ascii_case_test.dart ├── iterable_zip_test.dart ├── priority_queue_test.dart ├── queue_list_test.dart ├── union_set_controller_test.dart ├── union_set_test.dart ├── unmodifiable_collection_test.dart └── wrapper_test.dart /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | # Dependabot configuration file. 2 | version: 2 3 | 4 | updates: 5 | - package-ecosystem: github-actions 6 | directory: / 7 | schedule: 8 | interval: monthly 9 | labels: 10 | - autosubmit 11 | groups: 12 | github-actions: 13 | patterns: 14 | - "*" 15 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Dart CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | schedule: 9 | - cron: "0 0 * * 0" 10 | 11 | env: 12 | PUB_ENVIRONMENT: bot.github 13 | 14 | jobs: 15 | # Check code formatting and static analysis on a single OS (linux) 16 | # against Dart dev. 17 | analyze: 18 | runs-on: ubuntu-latest 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | sdk: [dev] 23 | steps: 24 | - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 25 | - uses: dart-lang/setup-dart@0a8a0fc875eb934c15d08629302413c671d3f672 26 | with: 27 | sdk: ${{ matrix.sdk }} 28 | - id: install 29 | name: Install dependencies 30 | run: dart pub get 31 | - name: Check formatting 32 | run: dart format --output=none --set-exit-if-changed . 33 | if: always() && steps.install.outcome == 'success' 34 | - name: Analyze code 35 | run: dart analyze --fatal-infos 36 | if: always() && steps.install.outcome == 'success' 37 | 38 | # Run tests on a matrix consisting of two dimensions: 39 | # 1. OS: ubuntu-latest, (macos-latest, windows-latest) 40 | # 2. release channel: dev 41 | test: 42 | needs: analyze 43 | runs-on: ${{ matrix.os }} 44 | strategy: 45 | fail-fast: false 46 | matrix: 47 | # Add macos-latest and/or windows-latest if relevant for this package. 48 | os: [ubuntu-latest] 49 | sdk: [3.4, dev] 50 | steps: 51 | - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 52 | - uses: dart-lang/setup-dart@0a8a0fc875eb934c15d08629302413c671d3f672 53 | with: 54 | sdk: ${{ matrix.sdk }} 55 | - id: install 56 | name: Install dependencies 57 | run: dart pub get 58 | - name: Run VM tests 59 | run: dart test --platform vm --test-randomize-ordering-seed=random 60 | if: always() && steps.install.outcome == 'success' 61 | - name: Run Chrome tests 62 | run: dart test --platform chrome --test-randomize-ordering-seed=random 63 | if: always() && steps.install.outcome == 'success' 64 | - name: Run Chrome tests - wasm 65 | run: dart test --platform chrome --compiler dart2wasm 66 | if: always() && steps.install.outcome == 'success' 67 | -------------------------------------------------------------------------------- /.github/workflows/no-response.yml: -------------------------------------------------------------------------------- 1 | # A workflow to close issues where the author hasn't responded to a request for 2 | # more information; see https://github.com/actions/stale. 3 | 4 | name: No Response 5 | 6 | # Run as a daily cron. 7 | on: 8 | schedule: 9 | # Every day at 8am 10 | - cron: '0 8 * * *' 11 | 12 | # All permissions not specified are set to 'none'. 13 | permissions: 14 | issues: write 15 | pull-requests: write 16 | 17 | jobs: 18 | no-response: 19 | runs-on: ubuntu-latest 20 | if: ${{ github.repository_owner == 'dart-lang' }} 21 | steps: 22 | - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e 23 | with: 24 | # Don't automatically mark inactive issues+PRs as stale. 25 | days-before-stale: -1 26 | # Close needs-info issues and PRs after 14 days of inactivity. 27 | days-before-close: 14 28 | stale-issue-label: "needs-info" 29 | close-issue-message: > 30 | Without additional information we're not able to resolve this issue. 31 | Feel free to add more info or respond to any questions above and we 32 | can reopen the case. Thanks for your contribution! 33 | stale-pr-label: "needs-info" 34 | close-pr-message: > 35 | Without additional information we're not able to resolve this PR. 36 | Feel free to add more info or respond to any questions above. 37 | Thanks for your contribution! 38 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | # A CI configuration to auto-publish pub packages. 2 | 3 | name: Publish 4 | 5 | on: 6 | pull_request: 7 | branches: [ master ] 8 | push: 9 | tags: [ 'v[0-9]+.[0-9]+.[0-9]+' ] 10 | 11 | jobs: 12 | publish: 13 | if: ${{ github.repository_owner == 'dart-lang' }} 14 | uses: dart-lang/ecosystem/.github/workflows/publish.yaml@main 15 | permissions: 16 | id-token: write # Required for authentication using OIDC 17 | pull-requests: write # Required for writing the pull request note 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .buildlog 2 | .DS_Store 3 | .idea 4 | .pub/ 5 | .dart_tool/ 6 | .settings/ 7 | build/ 8 | packages 9 | .packages 10 | pubspec.lock 11 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # Below is a list of people and organizations that have contributed 2 | # to the project. Names should be added to the list like so: 3 | # 4 | # Name/Organization 5 | 6 | Google Inc. 7 | AAABramenko (https://github.com/AAAbramenko) 8 | TimWhiting tim@whitings.org 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.19.0 2 | 3 | - Adds `shuffled` to `IterableExtension`. 4 | - Shuffle `IterableExtension.sample` results. 5 | - Fix `mergeSort` when the runtime iterable generic is a subtype of the static 6 | generic. 7 | - `CanonicalizedMap`: added constructor `fromEntries`. 8 | - Mark "mixin" classes as `mixin`. 9 | - `extension IterableIterableExtension on Iterable>` 10 | - Add `flattenedToList` as a performance improvement over `flattened.` 11 | - Add `flattenedToSet` as new behavior for flattening to unique elements. 12 | - Deprecate `transitiveClosure`. Consider using `package:graphs`. 13 | - Deprecate `whereNotNull()` from `IterableNullableExtension`. Use `nonNulls` 14 | instead - this is an equivalent extension available in Dart core since 15 | version 3.0. 16 | - Require Dart `^3.4.0` 17 | 18 | ## 1.18.0 19 | 20 | - `CanonicalizedMap`: 21 | - Added methods: 22 | - `copy`: copies an instance without recalculating the canonical values of the keys. 23 | - `toMap`: creates a `Map` (with the original key values). 24 | - `toMapOfCanonicalKeys`: creates a `Map` (with the canonicalized keys). 25 | - Fixes bugs in `ListSlice.slice` and `ListExtensions.slice`. 26 | - Update to `package:lints` 2.0.1. 27 | 28 | ## 1.17.2 29 | 30 | * Accept Dart SDK versions above 3.0. 31 | 32 | ## 1.17.1 33 | 34 | * Require Dart 2.18. 35 | * Improve docs for `splitAfter` and `splitBefore`. 36 | 37 | ## 1.17.0 38 | 39 | * Add `Iterable.elementAtOrNull` and `List.elementAtOrNull` extension methods. 40 | * Add a top-level `lastBy()` function that converts an `Iterable` to a `Map` by 41 | grouping its elements using a function, keeping the last element for each 42 | computed key. Also available as an extension method on `Iterable`. 43 | 44 | ## 1.16.0 45 | 46 | * Add an `Iterable.slices` extension method. 47 | * Add `BoolList` class for space-efficient lists of boolean values. 48 | * Use a stable sort algorithm in the `IterableExtension.sortedBy` method. 49 | * Add `min`, `max`, `minOrNull` and `maxOrNull` getters to 50 | `IterableDoubleExtension`, `IterableNumberExtension` and 51 | `IterableIntegerExtension` 52 | * Change `UnorderedIterableEquality` and `SetEquality` to implement `Equality` 53 | with a non-nullable generic to allows assignment to variables with that type. 54 | Assignment to `Equality` with a nullable type is still allowed because of 55 | covariance. The `equals` and `hash` methods continue to accept nullable 56 | arguments. 57 | * Enable the `avoid_dynamic_calls` lint. 58 | 59 | ## 1.15.0 60 | 61 | * Stable release for null safety. 62 | 63 | ## 1.15.0-nullsafety.5 64 | 65 | * Fix typo in extension method `expandIndexed`. 66 | * Update sdk constraints to `>=2.12.0-0 <3.0.0` based on beta release 67 | guidelines. 68 | 69 | ## 1.15.0-nullsafety.4 70 | 71 | * Allow prerelease versions of the `2.12.x` sdk. 72 | 73 | * Remove the unusable setter `UnionSetController.set=`. This was mistakenly 74 | added to the public API but could never be called. 75 | 76 | * Add extra optional `Random` argument to `shuffle`. 77 | 78 | * Add a large number of extension methods on `Iterable` and `List` types, 79 | and on a few other types. 80 | These either provide easy access to the operations from `algorithms.dart`, 81 | or provide convenience variants of existing `Iterable` and `List` methods 82 | like `singleWhereOrNull` or `forEachIndexed`. 83 | 84 | ## 1.15.0-nullsafety.3 85 | 86 | * Allow 2.10 stable and 2.11.0 dev SDK versions. 87 | * Add `toUnorderedList` method on `PriorityQueue`. 88 | * Make `HeapPriorityQueue`'s `remove` and `contains` methods 89 | use `==` for equality checks. 90 | Previously used `comparison(a, b) == 0` as criteria, but it's possible 91 | to have multiple elements with the same priority in a queue, so that 92 | could remove the wrong element. 93 | Still requires that objects that are `==` also have the same priority. 94 | 95 | ## 1.15.0-nullsafety.2 96 | 97 | Update for the 2.10 dev sdk. 98 | 99 | ## 1.15.0-nullsafety.1 100 | 101 | * Allow the <=2.9.10 stable sdks. 102 | 103 | ## 1.15.0-nullsafety 104 | 105 | Pre-release for the null safety migration of this package. 106 | 107 | Note that `1.15.0` may not be the final stable null safety release version, 108 | we reserve the right to release it as a `2.0.0` breaking change. 109 | 110 | This release will be pinned to only allow pre-release sdk versions starting 111 | from `2.9.0-dev.18.0`, which is the first version where this package will 112 | appear in the null safety allow list. 113 | 114 | ## 1.14.13 115 | 116 | * Deprecate `mapMap`. The Map interface has a `map` call and map literals can 117 | use for-loop elements which supersede this method. 118 | 119 | ## 1.14.12 120 | 121 | * Fix `CombinedMapView.keys`, `CombinedMapView.length`, 122 | `CombinedMapView.forEach`, and `CombinedMapView.values` to work as specified 123 | and not repeat duplicate items from the maps. 124 | * As a result of this fix the `length` getter now must iterate all maps in 125 | order to remove duplicates and return an accurate length, so it is no 126 | longer `O(maps)`. 127 | 128 | ## 1.14.11 129 | 130 | * Set max SDK version to `<3.0.0`. 131 | 132 | ## 1.14.10 133 | 134 | * Fix the parameter names in overridden methods to match the source. 135 | * Make tests Dart 2 type-safe. 136 | * Stop depending on SDK `retype` and deprecate methods. 137 | 138 | ## 1.14.9 139 | 140 | * Fixed bugs where `QueueList`, `MapKeySet`, and `MapValueSet` did not adhere to 141 | the contract laid out by `List.cast`, `Set.cast` and `Map.cast` respectively. 142 | The returned instances of these methods now correctly forward to the existing 143 | instance instead of always creating a new copy. 144 | 145 | ## 1.14.8 146 | 147 | * Deprecated `Delegating{Name}.typed` static methods in favor of the new Dart 2 148 | `cast` methods. For example, `DelegatingList.typed(list)` can now be 149 | written as `list.cast()`. 150 | 151 | ## 1.14.7 152 | 153 | * Only the Dart 2 dev SDK (`>=2.0.0-dev.22.0`) is now supported. 154 | * Added support for all Dart 2 SDK methods that threw `UnimplementedError`. 155 | 156 | ## 1.14.6 157 | 158 | * Make `DefaultEquality`'s `equals()` and `hash()` methods take any `Object` 159 | rather than objects of type `E`. This makes `const DefaultEquality()` 160 | usable as `Equality` for any `E`, which means it can be used in a const 161 | context which expects `Equality`. 162 | 163 | This makes the default arguments of various other const equality constructors 164 | work in strong mode. 165 | 166 | ## 1.14.5 167 | 168 | * Fix issue with `EmptyUnmodifiableSet`'s stubs that were introduced in 1.14.4. 169 | 170 | ## 1.14.4 171 | 172 | * Add implementation stubs of upcoming Dart 2.0 core library methods, namely 173 | new methods for classes that implement `Iterable`, `List`, `Map`, `Queue`, 174 | and `Set`. 175 | 176 | ## 1.14.3 177 | 178 | * Fix `MapKeySet.lookup` to be a valid override in strong mode. 179 | 180 | ## 1.14.2 181 | 182 | * Add type arguments to `SyntheticInvocation`. 183 | 184 | ## 1.14.1 185 | 186 | * Make `Equality` implementations accept `null` as argument to `hash`. 187 | 188 | ## 1.14.0 189 | 190 | * Add `CombinedListView`, a view of several lists concatenated together. 191 | * Add `CombinedIterableView`, a view of several iterables concatenated together. 192 | * Add `CombinedMapView`, a view of several maps concatenated together. 193 | 194 | ## 1.13.0 195 | 196 | * Add `EqualityBy` 197 | 198 | ## 1.12.0 199 | 200 | * Add `CaseInsensitiveEquality`. 201 | 202 | * Fix bug in `equalsIgnoreAsciiCase`. 203 | 204 | ## 1.11.0 205 | 206 | * Add `EqualityMap` and `EqualitySet` classes which use `Equality` objects for 207 | key and element equality, respectively. 208 | 209 | ## 1.10.1 210 | 211 | * `Set.difference` now takes a `Set` as argument. 212 | 213 | ## 1.9.1 214 | 215 | * Fix some documentation bugs. 216 | 217 | ## 1.9.0 218 | 219 | * Add a top-level `stronglyConnectedComponents()` function that returns the 220 | strongly connected components in a directed graph. 221 | 222 | ## 1.8.0 223 | 224 | * Add a top-level `mapMap()` function that works like `Iterable.map()` on a 225 | `Map`. 226 | 227 | * Add a top-level `mergeMaps()` function that creates a new map with the 228 | combined contents of two existing maps. 229 | 230 | * Add a top-level `groupBy()` function that converts an `Iterable` to a `Map` by 231 | grouping its elements using a function. 232 | 233 | * Add top-level `minBy()` and `maxBy()` functions that return the minimum and 234 | maximum values in an `Iterable`, respectively, ordered by a derived value. 235 | 236 | * Add a top-level `transitiveClosure()` function that returns the transitive 237 | closure of a directed graph. 238 | 239 | ## 1.7.0 240 | 241 | * Add a `const UnmodifiableSetView.empty()` constructor. 242 | 243 | ## 1.6.0 244 | 245 | * Add a `UnionSet` class that provides a view of the union of a set of sets. 246 | 247 | * Add a `UnionSetController` class that provides a convenient way to manage the 248 | contents of a `UnionSet`. 249 | 250 | * Fix another incorrectly-declared generic type. 251 | 252 | ## 1.5.1 253 | 254 | * Fix an incorrectly-declared generic type. 255 | 256 | ## 1.5.0 257 | 258 | * Add `DelegatingIterable.typed()`, `DelegatingList.typed()`, 259 | `DelegatingSet.typed()`, `DelegatingMap.typed()`, and 260 | `DelegatingQueue.typed()` static methods. These wrap untyped instances of 261 | these classes with the correct type parameter, and assert the types of values 262 | as they're accessed. 263 | 264 | * Fix the types for `binarySearch()` and `lowerBound()` so they no longer 265 | require all arguments to be comparable. 266 | 267 | * Add generic annotations to `insertionSort()` and `mergeSort()`. 268 | 269 | ## 1.4.1 270 | 271 | * Fix all strong mode warnings. 272 | 273 | ## 1.4.0 274 | 275 | * Add a `new PriorityQueue()` constructor that forwards to `new 276 | HeapPriorityQueue()`. 277 | 278 | * Deprecate top-level libraries other than `package:collection/collection.dart`, 279 | which exports these libraries' interfaces. 280 | 281 | ## 1.3.0 282 | 283 | * Add `lowerBound` to binary search for values that might not be present. 284 | 285 | * Verify that the is valid for `CanonicalMap.[]`. 286 | 287 | ## 1.2.0 288 | 289 | * Add string comparators that ignore ASCII case and sort numbers numerically. 290 | 291 | ## 1.1.3 292 | 293 | * Fix type inconsistencies with `Map` and `Set`. 294 | 295 | ## 1.1.2 296 | 297 | * Export `UnmodifiableMapView` from the Dart core libraries. 298 | 299 | ## 1.1.1 300 | 301 | * Bug-fix for signatures of `isValidKey` arguments of `CanonicalizedMap`. 302 | 303 | ## 1.1.0 304 | 305 | * Add a `QueueList` class that implements both `Queue` and `List`. 306 | 307 | ## 0.9.4 308 | 309 | * Add a `CanonicalizedMap` class that canonicalizes its keys to provide a custom 310 | equality relation. 311 | 312 | ## 0.9.3+1 313 | 314 | * Fix all analyzer hints. 315 | 316 | ## 0.9.3 317 | 318 | * Add a `MapKeySet` class that exposes an unmodifiable `Set` view of a `Map`'s 319 | keys. 320 | 321 | * Add a `MapValueSet` class that takes a function from values to keys and uses 322 | it to expose a `Set` view of a `Map`'s values. 323 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Want to contribute? Great! First, read this page (including the small print at 2 | the end). 3 | 4 | ### Before you contribute 5 | Before we can use your code, you must sign the 6 | [Google Individual Contributor License Agreement](https://cla.developers.google.com/about/google-individual) 7 | (CLA), which you can do online. The CLA is necessary mainly because you own the 8 | copyright to your changes, even after your contribution becomes part of our 9 | codebase, so we need your permission to use and distribute your code. We also 10 | need to be sure of various other things—for instance that you'll tell us if you 11 | know that your code infringes on other people's patents. You don't have to sign 12 | the CLA until after you've submitted your code for review and a member has 13 | approved it, but you must do it before we can put your code into our codebase. 14 | 15 | Before you start working on a larger contribution, you should get in touch with 16 | us first through the issue tracker with your idea so that we can help out and 17 | possibly guide you. Coordinating up front makes it much easier to avoid 18 | frustration later on. 19 | 20 | ### Code reviews 21 | All submissions, including submissions by project members, require review. 22 | 23 | ### Presubmit testing 24 | * All code must pass analysis by `dart analyze --fatal-infos`. 25 | * All code must be formatted by `dart format`. 26 | * _NOTE_: We currently require formatting by the `dev` channel SDK. 27 | * All code must pass unit tests for the VM, Dart2JS, and DartDevC (`pub run build_runner test`). 28 | * _NOTE_: We currently use `build_runner` for compilation with DartDevC. It's 29 | possible to run only Dart2JS and the VM without it using `pub run test` 30 | directly. 31 | 32 | ### File headers 33 | All files in the project must start with the following header. 34 | 35 | // Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file 36 | // for details. All rights reserved. Use of this source code is governed by a 37 | // BSD-style license that can be found in the LICENSE file. 38 | 39 | ### The small print 40 | Contributions made by corporations are covered by a different agreement than the 41 | one above, the 42 | [Software Grant and Corporate Contributor License Agreement](https://developers.google.com/open-source/cla/corporate). 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2015, the Dart project authors. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following 11 | disclaimer in the documentation and/or other materials provided 12 | with the distribution. 13 | * Neither the name of Google LLC nor the names of its 14 | contributors may be used to endorse or promote products derived 15 | from this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!IMPORTANT] 2 | > This repo has moved to https://github.com/dart-lang/core/tree/main/pkgs/collection 3 | 4 | [![Dart CI](https://github.com/dart-lang/collection/actions/workflows/ci.yml/badge.svg)](https://github.com/dart-lang/collection/actions/workflows/ci.yml) 5 | [![pub package](https://img.shields.io/pub/v/collection.svg)](https://pub.dev/packages/collection) 6 | [![package publisher](https://img.shields.io/pub/publisher/collection.svg)](https://pub.dev/packages/collection/publisher) 7 | 8 | Contains utility functions and classes in the style of `dart:collection` to make 9 | working with collections easier. 10 | 11 | ## Algorithms 12 | 13 | The package contains functions that operate on lists. 14 | 15 | It contains ways to shuffle a `List`, do binary search on a sorted `List`, and 16 | various sorting algorithms. 17 | 18 | ## Equality 19 | 20 | The package provides a way to specify the equality of elements and collections. 21 | 22 | Collections in Dart have no inherent equality. Two sets are not equal, even 23 | if they contain exactly the same objects as elements. 24 | 25 | The `Equality` interface provides a way to define such an equality. In this 26 | case, for example, `const SetEquality(IdentityEquality())` is an equality 27 | that considers two sets equal exactly if they contain identical elements. 28 | 29 | Equalities are provided for `Iterable`s, `List`s, `Set`s, and `Map`s, as well as 30 | combinations of these, such as: 31 | 32 | ```dart 33 | const MapEquality(IdentityEquality(), ListEquality()); 34 | ``` 35 | 36 | This equality considers maps equal if they have identical keys, and the 37 | corresponding values are lists with equal (`operator==`) values. 38 | 39 | ## Iterable Zip 40 | 41 | Utilities for "zipping" a list of iterables into an iterable of lists. 42 | 43 | ## Priority Queue 44 | 45 | An interface and implementation of a priority queue. 46 | 47 | ## Wrappers 48 | 49 | The package contains classes that "wrap" a collection. 50 | 51 | A wrapper class contains an object of the same type, and it forwards all 52 | methods to the wrapped object. 53 | 54 | Wrapper classes can be used in various ways, for example to restrict the type 55 | of an object to that of a supertype, or to change the behavior of selected 56 | functions on an existing object. 57 | 58 | ## Features and bugs 59 | 60 | Please file feature requests and bugs at the [issue tracker][tracker]. 61 | 62 | [tracker]: https://github.com/dart-lang/collection/issues 63 | 64 | ## Publishing automation 65 | 66 | For information about our publishing automation and release process, see 67 | https://github.com/dart-lang/ecosystem/wiki/Publishing-automation. 68 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:dart_flutter_team_lints/analysis_options.yaml 2 | 3 | analyzer: 4 | language: 5 | strict-casts: true 6 | 7 | linter: 8 | rules: 9 | - avoid_unused_constructor_parameters 10 | - cancel_subscriptions 11 | - literal_only_boolean_expressions 12 | - missing_whitespace_between_adjacent_strings 13 | - no_adjacent_strings_in_list 14 | - no_runtimeType_toString 15 | - package_api_docs 16 | - unnecessary_await_in_return 17 | -------------------------------------------------------------------------------- /lib/algorithms.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | /// Import `collection.dart` instead. 6 | @Deprecated('Will be removed in collection 2.0.0.') 7 | library; 8 | 9 | export 'src/algorithms.dart' 10 | show binarySearch, insertionSort, lowerBound, mergeSort, reverse, shuffle; 11 | -------------------------------------------------------------------------------- /lib/collection.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | export 'src/algorithms.dart' 6 | show binarySearch, insertionSort, lowerBound, mergeSort, reverse, shuffle; 7 | export 'src/boollist.dart'; 8 | export 'src/canonicalized_map.dart'; 9 | export 'src/combined_wrappers/combined_iterable.dart'; 10 | export 'src/combined_wrappers/combined_list.dart'; 11 | export 'src/combined_wrappers/combined_map.dart'; 12 | export 'src/comparators.dart'; 13 | export 'src/equality.dart'; 14 | export 'src/equality_map.dart'; 15 | export 'src/equality_set.dart'; 16 | export 'src/functions.dart'; 17 | export 'src/iterable_extensions.dart'; 18 | export 'src/iterable_zip.dart'; 19 | export 'src/list_extensions.dart'; 20 | export 'src/priority_queue.dart'; 21 | export 'src/queue_list.dart'; 22 | export 'src/union_set.dart'; 23 | export 'src/union_set_controller.dart'; 24 | export 'src/unmodifiable_wrappers.dart'; 25 | export 'src/wrappers.dart'; 26 | -------------------------------------------------------------------------------- /lib/equality.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | /// Import `collection.dart` instead. 6 | @Deprecated('Will be removed in collection 2.0.0.') 7 | library; 8 | 9 | export 'src/equality.dart'; 10 | -------------------------------------------------------------------------------- /lib/iterable_zip.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | /// Import `collection.dart` instead. 6 | @Deprecated('Will be removed in collection 2.0.0.') 7 | library; 8 | 9 | export 'src/iterable_zip.dart'; 10 | -------------------------------------------------------------------------------- /lib/priority_queue.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | /// Import `collection.dart` instead. 6 | @Deprecated('Will be removed in collection 2.0.0.') 7 | library; 8 | 9 | export 'src/priority_queue.dart'; 10 | -------------------------------------------------------------------------------- /lib/src/boollist.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:collection' show ListMixin; 6 | import 'dart:typed_data' show Uint32List; 7 | 8 | import 'unmodifiable_wrappers.dart' show NonGrowableListMixin; 9 | 10 | /// A space-efficient list of boolean values. 11 | /// 12 | /// Uses list of integers as internal storage to reduce memory usage. 13 | abstract /*mixin*/ class BoolList with ListMixin { 14 | static const int _entryShift = 5; 15 | 16 | static const int _bitsPerEntry = 32; 17 | 18 | static const int _entrySignBitIndex = 31; 19 | 20 | /// The length of the list. 21 | /// 22 | /// Maybe be shorter than the capacity of the backing store. 23 | int _length; 24 | 25 | /// Backing store for bits. 26 | Uint32List _data; 27 | 28 | BoolList._(this._data, this._length); 29 | 30 | factory BoolList._selectType(int length, bool growable) { 31 | if (growable) { 32 | return _GrowableBoolList(length); 33 | } else { 34 | return _NonGrowableBoolList(length); 35 | } 36 | } 37 | 38 | /// Creates a list of booleans with the provided length. 39 | /// 40 | /// The list is initially filled with the [fill] value, and 41 | /// the list is growable if [growable] is true. 42 | factory BoolList(int length, {bool fill = false, bool growable = false}) { 43 | RangeError.checkNotNegative(length, 'length'); 44 | 45 | BoolList boolList; 46 | if (growable) { 47 | boolList = _GrowableBoolList(length); 48 | } else { 49 | boolList = _NonGrowableBoolList(length); 50 | } 51 | 52 | if (fill) { 53 | boolList.fillRange(0, length, true); 54 | } 55 | 56 | return boolList; 57 | } 58 | 59 | /// Creates an empty list of booleans. 60 | /// 61 | /// The list defaults to being growable unless [growable] is `false`. 62 | /// If [capacity] is provided, and [growable] is not `false`, 63 | /// the implementation will attempt to make space for that 64 | /// many elements before needing to grow its internal storage. 65 | factory BoolList.empty({bool growable = true, int capacity = 0}) { 66 | RangeError.checkNotNegative(capacity, 'length'); 67 | 68 | if (growable) { 69 | return _GrowableBoolList._withCapacity(0, capacity); 70 | } else { 71 | return _NonGrowableBoolList._withCapacity(0, capacity); 72 | } 73 | } 74 | 75 | /// Generates a [BoolList] of values. 76 | /// 77 | /// Creates a [BoolList] with [length] positions and fills it with values 78 | /// created by calling [generator] for each index in the range 79 | /// `0` .. `length - 1` in increasing order. 80 | /// 81 | /// The created list is fixed-length unless [growable] is true. 82 | factory BoolList.generate( 83 | int length, 84 | bool Function(int) generator, { 85 | bool growable = true, 86 | }) { 87 | RangeError.checkNotNegative(length, 'length'); 88 | 89 | var instance = BoolList._selectType(length, growable); 90 | for (var i = 0; i < length; i++) { 91 | instance._setBit(i, generator(i)); 92 | } 93 | return instance; 94 | } 95 | 96 | /// Creates a list containing all [elements]. 97 | /// 98 | /// The [Iterator] of [elements] provides the order of the elements. 99 | /// 100 | /// This constructor creates a growable [BoolList] when [growable] is true; 101 | /// otherwise, it returns a fixed-length list. 102 | factory BoolList.of(Iterable elements, {bool growable = false}) { 103 | return BoolList._selectType(elements.length, growable)..setAll(0, elements); 104 | } 105 | 106 | /// The number of boolean values in this list. 107 | /// 108 | /// The valid indices for a list are `0` through `length - 1`. 109 | /// 110 | /// If the list is growable, setting the length will change the 111 | /// number of values. 112 | /// Setting the length to a smaller number will remove all 113 | /// values with indices greater than or equal to the new length. 114 | /// Setting the length to a larger number will increase the number of 115 | /// values, and all the new values will be `false`. 116 | @override 117 | int get length => _length; 118 | 119 | @override 120 | bool operator [](int index) { 121 | RangeError.checkValidIndex(index, this, 'index', _length); 122 | return (_data[index >> _entryShift] & 123 | (1 << (index & _entrySignBitIndex))) != 124 | 0; 125 | } 126 | 127 | @override 128 | void operator []=(int index, bool value) { 129 | RangeError.checkValidIndex(index, this, 'index', _length); 130 | _setBit(index, value); 131 | } 132 | 133 | @override 134 | void fillRange(int start, int end, [bool? fill]) { 135 | RangeError.checkValidRange(start, end, _length); 136 | fill ??= false; 137 | 138 | var startWord = start >> _entryShift; 139 | var endWord = (end - 1) >> _entryShift; 140 | 141 | var startBit = start & _entrySignBitIndex; 142 | var endBit = (end - 1) & _entrySignBitIndex; 143 | 144 | if (startWord < endWord) { 145 | if (fill) { 146 | _data[startWord] |= -1 << startBit; 147 | _data.fillRange(startWord + 1, endWord, -1); 148 | _data[endWord] |= (1 << (endBit + 1)) - 1; 149 | } else { 150 | _data[startWord] &= (1 << startBit) - 1; 151 | _data.fillRange(startWord + 1, endWord, 0); 152 | _data[endWord] &= -1 << (endBit + 1); 153 | } 154 | } else { 155 | if (fill) { 156 | _data[startWord] |= ((1 << (endBit - startBit + 1)) - 1) << startBit; 157 | } else { 158 | _data[startWord] &= ((1 << startBit) - 1) | (-1 << (endBit + 1)); 159 | } 160 | } 161 | } 162 | 163 | /// Creates an iterator for the elements of this [BoolList]. 164 | /// 165 | /// The [Iterator.current] getter of the returned iterator 166 | /// is `false` when the iterator has no current element. 167 | @override 168 | Iterator get iterator => _BoolListIterator(this); 169 | 170 | void _setBit(int index, bool value) { 171 | if (value) { 172 | _data[index >> _entryShift] |= 1 << (index & _entrySignBitIndex); 173 | } else { 174 | _data[index >> _entryShift] &= ~(1 << (index & _entrySignBitIndex)); 175 | } 176 | } 177 | 178 | static int _lengthInWords(int bitLength) { 179 | return (bitLength + (_bitsPerEntry - 1)) >> _entryShift; 180 | } 181 | } 182 | 183 | class _GrowableBoolList extends BoolList { 184 | static const int _growthFactor = 2; 185 | 186 | _GrowableBoolList._withCapacity(int length, int capacity) 187 | : super._( 188 | Uint32List(BoolList._lengthInWords(capacity)), 189 | length, 190 | ); 191 | 192 | _GrowableBoolList(int length) 193 | : super._( 194 | Uint32List(BoolList._lengthInWords(length * _growthFactor)), 195 | length, 196 | ); 197 | 198 | @override 199 | set length(int length) { 200 | RangeError.checkNotNegative(length, 'length'); 201 | if (length > _length) { 202 | _expand(length); 203 | } else if (length < _length) { 204 | _shrink(length); 205 | } 206 | } 207 | 208 | void _expand(int length) { 209 | if (length > _data.length * BoolList._bitsPerEntry) { 210 | _data = Uint32List( 211 | BoolList._lengthInWords(length * _growthFactor), 212 | )..setRange(0, _data.length, _data); 213 | } 214 | _length = length; 215 | } 216 | 217 | void _shrink(int length) { 218 | if (length < _length ~/ _growthFactor) { 219 | var newDataLength = BoolList._lengthInWords(length); 220 | _data = Uint32List(newDataLength)..setRange(0, newDataLength, _data); 221 | } 222 | 223 | for (var i = length; i < _data.length * BoolList._bitsPerEntry; i++) { 224 | _setBit(i, false); 225 | } 226 | 227 | _length = length; 228 | } 229 | } 230 | 231 | class _NonGrowableBoolList extends BoolList with NonGrowableListMixin { 232 | _NonGrowableBoolList._withCapacity(int length, int capacity) 233 | : super._( 234 | Uint32List(BoolList._lengthInWords(capacity)), 235 | length, 236 | ); 237 | 238 | _NonGrowableBoolList(int length) 239 | : super._( 240 | Uint32List(BoolList._lengthInWords(length)), 241 | length, 242 | ); 243 | } 244 | 245 | class _BoolListIterator implements Iterator { 246 | bool _current = false; 247 | int _pos = 0; 248 | final int _length; 249 | 250 | final BoolList _boolList; 251 | 252 | _BoolListIterator(this._boolList) : _length = _boolList._length; 253 | 254 | @override 255 | bool get current => _current; 256 | 257 | @override 258 | bool moveNext() { 259 | if (_boolList._length != _length) { 260 | throw ConcurrentModificationError(_boolList); 261 | } 262 | 263 | if (_pos < _boolList.length) { 264 | var pos = _pos++; 265 | _current = _boolList._data[pos >> BoolList._entryShift] & 266 | (1 << (pos & BoolList._entrySignBitIndex)) != 267 | 0; 268 | return true; 269 | } 270 | _current = false; 271 | return false; 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /lib/src/canonicalized_map.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:collection'; 6 | 7 | /// A map whose keys are converted to canonical values of type `C`. 8 | /// 9 | /// This is useful for using case-insensitive String keys, for example. It's 10 | /// more efficient than a [LinkedHashMap] with a custom equality operator 11 | /// because it only canonicalizes each key once, rather than doing so for each 12 | /// comparison. 13 | class CanonicalizedMap implements Map { 14 | final C Function(K) _canonicalize; 15 | 16 | final bool Function(K)? _isValidKeyFn; 17 | 18 | final _base = >{}; 19 | 20 | /// Creates an empty canonicalized map. 21 | /// 22 | /// The [canonicalize] function should return the canonical value for the 23 | /// given key. Keys with the same canonical value are considered equivalent. 24 | /// 25 | /// The [isValidKey] function is called before calling [canonicalize] for 26 | /// methods that take arbitrary objects. It can be used to filter out keys 27 | /// that can't be canonicalized. 28 | CanonicalizedMap(C Function(K key) canonicalize, 29 | {bool Function(K key)? isValidKey}) 30 | : _canonicalize = canonicalize, 31 | _isValidKeyFn = isValidKey; 32 | 33 | /// Creates a canonicalized map that is initialized with the key/value pairs 34 | /// of [other]. 35 | /// 36 | /// The [canonicalize] function should return the canonical value for the 37 | /// given key. Keys with the same canonical value are considered equivalent. 38 | /// 39 | /// The [isValidKey] function is called before calling [canonicalize] for 40 | /// methods that take arbitrary objects. It can be used to filter out keys 41 | /// that can't be canonicalized. 42 | CanonicalizedMap.from(Map other, C Function(K key) canonicalize, 43 | {bool Function(K key)? isValidKey}) 44 | : _canonicalize = canonicalize, 45 | _isValidKeyFn = isValidKey { 46 | addAll(other); 47 | } 48 | 49 | /// Creates a canonicalized map that is initialized with the key/value pairs 50 | /// of [entries]. 51 | /// 52 | /// The [canonicalize] function should return the canonical value for the 53 | /// given key. Keys with the same canonical value are considered equivalent. 54 | /// 55 | /// The [isValidKey] function is called before calling [canonicalize] for 56 | /// methods that take arbitrary objects. It can be used to filter out keys 57 | /// that can't be canonicalized. 58 | CanonicalizedMap.fromEntries( 59 | Iterable> entries, C Function(K key) canonicalize, 60 | {bool Function(K key)? isValidKey}) 61 | : _canonicalize = canonicalize, 62 | _isValidKeyFn = isValidKey { 63 | addEntries(entries); 64 | } 65 | 66 | CanonicalizedMap._( 67 | this._canonicalize, this._isValidKeyFn, Map> base) { 68 | _base.addAll(base); 69 | } 70 | 71 | /// Copies this [CanonicalizedMap] instance without recalculating the 72 | /// canonical values of the keys. 73 | CanonicalizedMap copy() => 74 | CanonicalizedMap._(_canonicalize, _isValidKeyFn, _base); 75 | 76 | @override 77 | V? operator [](Object? key) { 78 | if (!_isValidKey(key)) return null; 79 | var pair = _base[_canonicalize(key as K)]; 80 | return pair?.value; 81 | } 82 | 83 | @override 84 | void operator []=(K key, V value) { 85 | if (!_isValidKey(key)) return; 86 | _base[_canonicalize(key)] = MapEntry(key, value); 87 | } 88 | 89 | @override 90 | void addAll(Map other) { 91 | other.forEach((key, value) => this[key] = value); 92 | } 93 | 94 | @override 95 | void addEntries(Iterable> entries) => _base.addEntries(entries 96 | .map((e) => MapEntry(_canonicalize(e.key), MapEntry(e.key, e.value)))); 97 | 98 | @override 99 | Map cast() => _base.cast(); 100 | 101 | @override 102 | void clear() { 103 | _base.clear(); 104 | } 105 | 106 | @override 107 | bool containsKey(Object? key) { 108 | if (!_isValidKey(key)) return false; 109 | return _base.containsKey(_canonicalize(key as K)); 110 | } 111 | 112 | @override 113 | bool containsValue(Object? value) => 114 | _base.values.any((pair) => pair.value == value); 115 | 116 | @override 117 | Iterable> get entries => 118 | _base.entries.map((e) => MapEntry(e.value.key, e.value.value)); 119 | 120 | @override 121 | void forEach(void Function(K, V) f) { 122 | _base.forEach((key, pair) => f(pair.key, pair.value)); 123 | } 124 | 125 | @override 126 | bool get isEmpty => _base.isEmpty; 127 | 128 | @override 129 | bool get isNotEmpty => _base.isNotEmpty; 130 | 131 | @override 132 | Iterable get keys => _base.values.map((pair) => pair.key); 133 | 134 | @override 135 | int get length => _base.length; 136 | 137 | @override 138 | Map map(MapEntry Function(K, V) transform) => 139 | _base.map((_, pair) => transform(pair.key, pair.value)); 140 | 141 | @override 142 | V putIfAbsent(K key, V Function() ifAbsent) { 143 | return _base 144 | .putIfAbsent(_canonicalize(key), () => MapEntry(key, ifAbsent())) 145 | .value; 146 | } 147 | 148 | @override 149 | V? remove(Object? key) { 150 | if (!_isValidKey(key)) return null; 151 | var pair = _base.remove(_canonicalize(key as K)); 152 | return pair?.value; 153 | } 154 | 155 | @override 156 | void removeWhere(bool Function(K key, V value) test) => 157 | _base.removeWhere((_, pair) => test(pair.key, pair.value)); 158 | 159 | @Deprecated('Use cast instead') 160 | Map retype() => cast(); 161 | 162 | @override 163 | V update(K key, V Function(V) update, {V Function()? ifAbsent}) => 164 | _base.update(_canonicalize(key), (pair) { 165 | var value = pair.value; 166 | var newValue = update(value); 167 | if (identical(newValue, value)) return pair; 168 | return MapEntry(key, newValue); 169 | }, 170 | ifAbsent: 171 | ifAbsent == null ? null : () => MapEntry(key, ifAbsent())).value; 172 | 173 | @override 174 | void updateAll(V Function(K key, V value) update) => 175 | _base.updateAll((_, pair) { 176 | var value = pair.value; 177 | var key = pair.key; 178 | var newValue = update(key, value); 179 | if (identical(value, newValue)) return pair; 180 | return MapEntry(key, newValue); 181 | }); 182 | 183 | @override 184 | Iterable get values => _base.values.map((pair) => pair.value); 185 | 186 | @override 187 | String toString() => MapBase.mapToString(this); 188 | 189 | bool _isValidKey(Object? key) => 190 | (key is K) && (_isValidKeyFn == null || _isValidKeyFn(key)); 191 | 192 | /// Creates a `Map` (with the original key values). 193 | /// See [toMapOfCanonicalKeys]. 194 | Map toMap() => Map.fromEntries(_base.values); 195 | 196 | /// Creates a `Map` (with the canonicalized keys). 197 | /// See [toMap]. 198 | Map toMapOfCanonicalKeys() => Map.fromEntries( 199 | _base.entries.map((e) => MapEntry(e.key, e.value.value))); 200 | } 201 | -------------------------------------------------------------------------------- /lib/src/combined_wrappers/combined_iterable.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:collection'; 6 | 7 | import 'combined_iterator.dart'; 8 | 9 | /// A view of several iterables combined sequentially into a single iterable. 10 | /// 11 | /// All methods and accessors treat the [CombinedIterableView] as if it were a 12 | /// single concatenated iterable, but the underlying implementation is based on 13 | /// lazily accessing individual iterable instances. This means that if the 14 | /// underlying iterables change, the [CombinedIterableView] will reflect those 15 | /// changes. 16 | class CombinedIterableView extends IterableBase { 17 | /// The iterables that this combines. 18 | final Iterable> _iterables; 19 | 20 | /// Creates a combined view of [_iterables]. 21 | const CombinedIterableView(this._iterables); 22 | 23 | @override 24 | Iterator get iterator => 25 | CombinedIterator(_iterables.map((i) => i.iterator).iterator); 26 | 27 | // Special cased contains/isEmpty/length since many iterables have an 28 | // efficient implementation instead of running through the entire iterator. 29 | 30 | @override 31 | bool contains(Object? element) => _iterables.any((i) => i.contains(element)); 32 | 33 | @override 34 | bool get isEmpty => _iterables.every((i) => i.isEmpty); 35 | 36 | @override 37 | int get length => _iterables.fold(0, (length, i) => length + i.length); 38 | } 39 | -------------------------------------------------------------------------------- /lib/src/combined_wrappers/combined_iterator.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | /// The iterator for `CombinedIterableView` and `CombinedListView`. 6 | /// 7 | /// Moves through each iterable's iterator in sequence. 8 | class CombinedIterator implements Iterator { 9 | /// The iterators that this combines, or `null` if done iterating. 10 | /// 11 | /// Because this comes from a call to [Iterable.map], it's lazy and will 12 | /// avoid instantiating unnecessary iterators. 13 | Iterator>? _iterators; 14 | 15 | CombinedIterator(Iterator> iterators) : _iterators = iterators { 16 | if (!iterators.moveNext()) _iterators = null; 17 | } 18 | 19 | @override 20 | T get current { 21 | var iterators = _iterators; 22 | if (iterators != null) return iterators.current.current; 23 | return null as T; 24 | } 25 | 26 | @override 27 | bool moveNext() { 28 | var iterators = _iterators; 29 | if (iterators != null) { 30 | do { 31 | if (iterators.current.moveNext()) { 32 | return true; 33 | } 34 | } while (iterators.moveNext()); 35 | _iterators = null; 36 | } 37 | return false; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/src/combined_wrappers/combined_list.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:collection'; 6 | 7 | import 'combined_iterator.dart'; 8 | 9 | /// A view of several lists combined into a single list. 10 | /// 11 | /// All methods and accessors treat the [CombinedListView] list as if it were a 12 | /// single concatenated list, but the underlying implementation is based on 13 | /// lazily accessing individual list instances. This means that if the 14 | /// underlying lists change, the [CombinedListView] will reflect those changes. 15 | /// 16 | /// The index operator (`[]`) and [length] property of a [CombinedListView] are 17 | /// both `O(lists)` rather than `O(1)`. A [CombinedListView] is unmodifiable. 18 | class CombinedListView extends ListBase 19 | implements UnmodifiableListView { 20 | static Never _throw() { 21 | throw UnsupportedError('Cannot modify an unmodifiable List'); 22 | } 23 | 24 | /// The lists that this combines. 25 | final List> _lists; 26 | 27 | /// Creates a combined view of [_lists]. 28 | CombinedListView(this._lists); 29 | 30 | @override 31 | Iterator get iterator => 32 | CombinedIterator(_lists.map((i) => i.iterator).iterator); 33 | 34 | @override 35 | set length(int length) { 36 | _throw(); 37 | } 38 | 39 | @override 40 | int get length => _lists.fold(0, (length, list) => length + list.length); 41 | 42 | @override 43 | T operator [](int index) { 44 | var initialIndex = index; 45 | for (var i = 0; i < _lists.length; i++) { 46 | var list = _lists[i]; 47 | if (index < list.length) { 48 | return list[index]; 49 | } 50 | index -= list.length; 51 | } 52 | throw RangeError.index(initialIndex, this, 'index', null, length); 53 | } 54 | 55 | @override 56 | void operator []=(int index, T value) { 57 | _throw(); 58 | } 59 | 60 | @override 61 | void clear() { 62 | _throw(); 63 | } 64 | 65 | @override 66 | bool remove(Object? element) { 67 | _throw(); 68 | } 69 | 70 | @override 71 | void removeWhere(bool Function(T) test) { 72 | _throw(); 73 | } 74 | 75 | @override 76 | void retainWhere(bool Function(T) test) { 77 | _throw(); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /lib/src/combined_wrappers/combined_map.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:collection'; 6 | 7 | import 'combined_iterable.dart'; 8 | 9 | /// Returns a new map that represents maps flattened into a single map. 10 | /// 11 | /// All methods and accessors treat the new map as-if it were a single 12 | /// concatenated map, but the underlying implementation is based on lazily 13 | /// accessing individual map instances. In the occasion where a key occurs in 14 | /// multiple maps the first value is returned. 15 | /// 16 | /// The resulting map has an index operator (`[]`) that is `O(maps)`, rather 17 | /// than `O(1)`, and the map is unmodifiable, but underlying changes to these 18 | /// maps are still accessible from the resulting map. 19 | /// 20 | /// The `length` getter is `O(M)` where M is the total number of entries in 21 | /// all maps, since it has to remove duplicate entries. 22 | class CombinedMapView extends UnmodifiableMapBase { 23 | final Iterable> _maps; 24 | 25 | /// Create a new combined view of multiple maps. 26 | /// 27 | /// The iterable is accessed lazily so it should be collection type like 28 | /// [List] or [Set] rather than a lazy iterable produced by `map()` et al. 29 | CombinedMapView(this._maps); 30 | 31 | @override 32 | V? operator [](Object? key) { 33 | for (var map in _maps) { 34 | // Avoid two hash lookups on a positive hit. 35 | var value = map[key]; 36 | if (value != null || map.containsKey(value)) { 37 | return value; 38 | } 39 | } 40 | return null; 41 | } 42 | 43 | /// The keys of `this`. 44 | /// 45 | /// The returned iterable has efficient `contains` operations, assuming the 46 | /// iterables returned by the wrapped maps have efficient `contains` 47 | /// operations for their `keys` iterables. 48 | /// 49 | /// The `length` must do deduplication and thus is not optimized. 50 | /// 51 | /// The order of iteration is defined by the individual `Map` implementations, 52 | /// but must be consistent between changes to the maps. 53 | /// 54 | /// Unlike most [Map] implementations, modifying an individual map while 55 | /// iterating the keys will _sometimes_ throw. This behavior may change in 56 | /// the future. 57 | @override 58 | Iterable get keys => _DeduplicatingIterableView( 59 | CombinedIterableView(_maps.map((m) => m.keys))); 60 | } 61 | 62 | /// A view of an iterable that skips any duplicate entries. 63 | class _DeduplicatingIterableView extends IterableBase { 64 | final Iterable _iterable; 65 | 66 | const _DeduplicatingIterableView(this._iterable); 67 | 68 | @override 69 | Iterator get iterator => _DeduplicatingIterator(_iterable.iterator); 70 | 71 | // Special cased contains/isEmpty since many iterables have an efficient 72 | // implementation instead of running through the entire iterator. 73 | // 74 | // Note: We do not do this for `length` because we have to remove the 75 | // duplicates. 76 | 77 | @override 78 | bool contains(Object? element) => _iterable.contains(element); 79 | 80 | @override 81 | bool get isEmpty => _iterable.isEmpty; 82 | } 83 | 84 | /// An iterator that wraps another iterator and skips duplicate values. 85 | class _DeduplicatingIterator implements Iterator { 86 | final Iterator _iterator; 87 | 88 | final _emitted = HashSet(); 89 | 90 | _DeduplicatingIterator(this._iterator); 91 | 92 | @override 93 | T get current => _iterator.current; 94 | 95 | @override 96 | bool moveNext() { 97 | while (_iterator.moveNext()) { 98 | if (_emitted.add(current)) { 99 | return true; 100 | } 101 | } 102 | return false; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /lib/src/comparators.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | // Character constants. 6 | const int _zero = 0x30; 7 | const int _upperCaseA = 0x41; 8 | const int _upperCaseZ = 0x5a; 9 | const int _lowerCaseA = 0x61; 10 | const int _lowerCaseZ = 0x7a; 11 | const int _asciiCaseBit = 0x20; 12 | 13 | /// Checks if strings [a] and [b] differ only on the case of ASCII letters. 14 | /// 15 | /// Strings are equal if they have the same length, and the characters at 16 | /// each index are the same, or they are ASCII letters where one is upper-case 17 | /// and the other is the lower-case version of the same letter. 18 | /// 19 | /// The comparison does not ignore the case of non-ASCII letters, so 20 | /// an upper-case ae-ligature (Æ) is different from 21 | /// a lower case ae-ligature (æ). 22 | /// 23 | /// Ignoring non-ASCII letters is not generally a good idea, but it makes sense 24 | /// for situations where the strings are known to be ASCII. Examples could 25 | /// be Dart identifiers, base-64 or hex encoded strings, GUIDs or similar 26 | /// strings with a known structure. 27 | bool equalsIgnoreAsciiCase(String a, String b) { 28 | if (a.length != b.length) return false; 29 | for (var i = 0; i < a.length; i++) { 30 | var aChar = a.codeUnitAt(i); 31 | var bChar = b.codeUnitAt(i); 32 | if (aChar == bChar) continue; 33 | // Quick-check for whether this may be different cases of the same letter. 34 | if (aChar ^ bChar != _asciiCaseBit) return false; 35 | // If it's possible, then check if either character is actually an ASCII 36 | // letter. 37 | var aCharLowerCase = aChar | _asciiCaseBit; 38 | if (_lowerCaseA <= aCharLowerCase && aCharLowerCase <= _lowerCaseZ) { 39 | continue; 40 | } 41 | return false; 42 | } 43 | return true; 44 | } 45 | 46 | /// Hash code for a string which is compatible with [equalsIgnoreAsciiCase]. 47 | /// 48 | /// The hash code is unaffected by changing the case of ASCII letters, but 49 | /// the case of non-ASCII letters do affect the result. 50 | int hashIgnoreAsciiCase(String string) { 51 | // Jenkins hash code ( http://en.wikipedia.org/wiki/Jenkins_hash_function). 52 | // adapted to smi values. 53 | // Same hash used by dart2js for strings, modified to ignore ASCII letter 54 | // case. 55 | var hash = 0; 56 | for (var i = 0; i < string.length; i++) { 57 | var char = string.codeUnitAt(i); 58 | // Convert lower-case ASCII letters to upper case.upper 59 | // This ensures that strings that differ only in case will have the 60 | // same hash code. 61 | if (_lowerCaseA <= char && char <= _lowerCaseZ) char -= _asciiCaseBit; 62 | hash = 0x1fffffff & (hash + char); 63 | hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); 64 | hash >>= 6; 65 | } 66 | hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); 67 | hash >>= 11; 68 | return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); 69 | } 70 | 71 | /// Compares [a] and [b] lexically, converting ASCII letters to upper case. 72 | /// 73 | /// Comparison treats all lower-case ASCII letters as upper-case letters, 74 | /// but does no case conversion for non-ASCII letters. 75 | /// 76 | /// If two strings differ only on the case of ASCII letters, the one with the 77 | /// capital letter at the first difference will compare as less than the other 78 | /// string. This tie-breaking ensures that the comparison is a total ordering 79 | /// on strings and is compatible with equality. 80 | /// 81 | /// Ignoring non-ASCII letters is not generally a good idea, but it makes sense 82 | /// for situations where the strings are known to be ASCII. Examples could 83 | /// be Dart identifiers, base-64 or hex encoded strings, GUIDs or similar 84 | /// strings with a known structure. 85 | int compareAsciiUpperCase(String a, String b) { 86 | var defaultResult = 0; // Returned if no difference found. 87 | for (var i = 0; i < a.length; i++) { 88 | if (i >= b.length) return 1; 89 | var aChar = a.codeUnitAt(i); 90 | var bChar = b.codeUnitAt(i); 91 | if (aChar == bChar) continue; 92 | // Upper-case if letters. 93 | var aUpperCase = aChar; 94 | var bUpperCase = bChar; 95 | if (_lowerCaseA <= aChar && aChar <= _lowerCaseZ) { 96 | aUpperCase -= _asciiCaseBit; 97 | } 98 | if (_lowerCaseA <= bChar && bChar <= _lowerCaseZ) { 99 | bUpperCase -= _asciiCaseBit; 100 | } 101 | if (aUpperCase != bUpperCase) return (aUpperCase - bUpperCase).sign; 102 | if (defaultResult == 0) defaultResult = aChar - bChar; 103 | } 104 | if (b.length > a.length) return -1; 105 | return defaultResult.sign; 106 | } 107 | 108 | /// Compares [a] and [b] lexically, converting ASCII letters to lower case. 109 | /// 110 | /// Comparison treats all upper-case ASCII letters as lower-case letters, 111 | /// but does no case conversion for non-ASCII letters. 112 | /// 113 | /// If two strings differ only on the case of ASCII letters, the one with the 114 | /// capital letter at the first difference will compare as less than the other 115 | /// string. This tie-breaking ensures that the comparison is a total ordering 116 | /// on strings. 117 | /// 118 | /// Ignoring non-ASCII letters is not generally a good idea, but it makes sense 119 | /// for situations where the strings are known to be ASCII. Examples could 120 | /// be Dart identifiers, base-64 or hex encoded strings, GUIDs or similar 121 | /// strings with a known structure. 122 | int compareAsciiLowerCase(String a, String b) { 123 | var defaultResult = 0; 124 | for (var i = 0; i < a.length; i++) { 125 | if (i >= b.length) return 1; 126 | var aChar = a.codeUnitAt(i); 127 | var bChar = b.codeUnitAt(i); 128 | if (aChar == bChar) continue; 129 | var aLowerCase = aChar; 130 | var bLowerCase = bChar; 131 | // Upper case if ASCII letters. 132 | if (_upperCaseA <= bChar && bChar <= _upperCaseZ) { 133 | bLowerCase += _asciiCaseBit; 134 | } 135 | if (_upperCaseA <= aChar && aChar <= _upperCaseZ) { 136 | aLowerCase += _asciiCaseBit; 137 | } 138 | if (aLowerCase != bLowerCase) return (aLowerCase - bLowerCase).sign; 139 | if (defaultResult == 0) defaultResult = aChar - bChar; 140 | } 141 | if (b.length > a.length) return -1; 142 | return defaultResult.sign; 143 | } 144 | 145 | /// Compares strings [a] and [b] according to [natural sort ordering][]. 146 | /// 147 | /// A natural sort ordering is a lexical ordering where embedded 148 | /// numerals (digit sequences) are treated as a single unit and ordered by 149 | /// numerical value. 150 | /// This means that `"a10b"` will be ordered after `"a7b"` in natural 151 | /// ordering, where lexical ordering would put the `1` before the `7`, ignoring 152 | /// that the `1` is part of a larger number. 153 | /// 154 | /// Example: 155 | /// The following strings are in the order they would be sorted by using this 156 | /// comparison function: 157 | /// 158 | /// "a", "a0", "a0b", "a1", "a01", "a9", "a10", "a100", "a100b", "aa" 159 | /// 160 | /// [natural sort ordering]: https://en.wikipedia.org/wiki/Natural_sort_order 161 | int compareNatural(String a, String b) { 162 | for (var i = 0; i < a.length; i++) { 163 | if (i >= b.length) return 1; 164 | var aChar = a.codeUnitAt(i); 165 | var bChar = b.codeUnitAt(i); 166 | if (aChar != bChar) { 167 | return _compareNaturally(a, b, i, aChar, bChar); 168 | } 169 | } 170 | if (b.length > a.length) return -1; 171 | return 0; 172 | } 173 | 174 | /// Compares strings [a] and [b] according to lower-case 175 | /// [natural sort ordering][]. 176 | /// 177 | /// ASCII letters are converted to lower case before being compared, like 178 | /// for [compareAsciiLowerCase], then the result is compared like for 179 | /// [compareNatural]. 180 | /// 181 | /// If two strings differ only on the case of ASCII letters, the one with the 182 | /// capital letter at the first difference will compare as less than the other 183 | /// string. This tie-breaking ensures that the comparison is a total ordering 184 | /// on strings. 185 | /// 186 | /// [natural sort ordering]: https://en.wikipedia.org/wiki/Natural_sort_order 187 | int compareAsciiLowerCaseNatural(String a, String b) { 188 | var defaultResult = 0; // Returned if no difference found. 189 | for (var i = 0; i < a.length; i++) { 190 | if (i >= b.length) return 1; 191 | var aChar = a.codeUnitAt(i); 192 | var bChar = b.codeUnitAt(i); 193 | if (aChar == bChar) continue; 194 | var aLowerCase = aChar; 195 | var bLowerCase = bChar; 196 | if (_upperCaseA <= aChar && aChar <= _upperCaseZ) { 197 | aLowerCase += _asciiCaseBit; 198 | } 199 | if (_upperCaseA <= bChar && bChar <= _upperCaseZ) { 200 | bLowerCase += _asciiCaseBit; 201 | } 202 | if (aLowerCase != bLowerCase) { 203 | return _compareNaturally(a, b, i, aLowerCase, bLowerCase); 204 | } 205 | if (defaultResult == 0) defaultResult = aChar - bChar; 206 | } 207 | if (b.length > a.length) return -1; 208 | return defaultResult.sign; 209 | } 210 | 211 | /// Compares strings [a] and [b] according to upper-case 212 | /// [natural sort ordering][]. 213 | /// 214 | /// ASCII letters are converted to upper case before being compared, like 215 | /// for [compareAsciiUpperCase], then the result is compared like for 216 | /// [compareNatural]. 217 | /// 218 | /// If two strings differ only on the case of ASCII letters, the one with the 219 | /// capital letter at the first difference will compare as less than the other 220 | /// string. This tie-breaking ensures that the comparison is a total ordering 221 | /// on strings 222 | /// 223 | /// [natural sort ordering]: https://en.wikipedia.org/wiki/Natural_sort_order 224 | int compareAsciiUpperCaseNatural(String a, String b) { 225 | var defaultResult = 0; 226 | for (var i = 0; i < a.length; i++) { 227 | if (i >= b.length) return 1; 228 | var aChar = a.codeUnitAt(i); 229 | var bChar = b.codeUnitAt(i); 230 | if (aChar == bChar) continue; 231 | var aUpperCase = aChar; 232 | var bUpperCase = bChar; 233 | if (_lowerCaseA <= aChar && aChar <= _lowerCaseZ) { 234 | aUpperCase -= _asciiCaseBit; 235 | } 236 | if (_lowerCaseA <= bChar && bChar <= _lowerCaseZ) { 237 | bUpperCase -= _asciiCaseBit; 238 | } 239 | if (aUpperCase != bUpperCase) { 240 | return _compareNaturally(a, b, i, aUpperCase, bUpperCase); 241 | } 242 | if (defaultResult == 0) defaultResult = aChar - bChar; 243 | } 244 | if (b.length > a.length) return -1; 245 | return defaultResult.sign; 246 | } 247 | 248 | /// Check for numbers overlapping the current mismatched characters. 249 | /// 250 | /// If both [aChar] and [bChar] are digits, use numerical comparison. 251 | /// Check if the previous characters is a non-zero number, and if not, 252 | /// skip - but count - leading zeros before comparing numbers. 253 | /// 254 | /// If one is a digit and the other isn't, check if the previous character 255 | /// is a digit, and if so, the the one with the digit is the greater number. 256 | /// 257 | /// Otherwise just returns the difference between [aChar] and [bChar]. 258 | int _compareNaturally(String a, String b, int index, int aChar, int bChar) { 259 | assert(aChar != bChar); 260 | var aIsDigit = _isDigit(aChar); 261 | var bIsDigit = _isDigit(bChar); 262 | if (aIsDigit) { 263 | if (bIsDigit) { 264 | return _compareNumerically(a, b, aChar, bChar, index); 265 | } else if (index > 0 && _isDigit(a.codeUnitAt(index - 1))) { 266 | // aChar is the continuation of a longer number. 267 | return 1; 268 | } 269 | } else if (bIsDigit && index > 0 && _isDigit(b.codeUnitAt(index - 1))) { 270 | // bChar is the continuation of a longer number. 271 | return -1; 272 | } 273 | // Characters are both non-digits, or not continuation of earlier number. 274 | return (aChar - bChar).sign; 275 | } 276 | 277 | /// Compare numbers overlapping [aChar] and [bChar] numerically. 278 | /// 279 | /// If the numbers have the same numerical value, but one has more leading 280 | /// zeros, the longer number is considered greater than the shorter one. 281 | /// 282 | /// This ensures a total ordering on strings compatible with equality. 283 | int _compareNumerically(String a, String b, int aChar, int bChar, int index) { 284 | // Both are digits. Find the first significant different digit, then find 285 | // the length of the numbers. 286 | if (_isNonZeroNumberSuffix(a, index)) { 287 | // Part of a longer number, differs at this index, just count the length. 288 | var result = _compareDigitCount(a, b, index, index); 289 | if (result != 0) return result; 290 | // If same length, the current character is the most significant differing 291 | // digit. 292 | return (aChar - bChar).sign; 293 | } 294 | // Not part of larger (non-zero) number, so skip leading zeros before 295 | // comparing numbers. 296 | var aIndex = index; 297 | var bIndex = index; 298 | if (aChar == _zero) { 299 | do { 300 | aIndex++; 301 | if (aIndex == a.length) return -1; // number in a is zero, b is not. 302 | aChar = a.codeUnitAt(aIndex); 303 | } while (aChar == _zero); 304 | if (!_isDigit(aChar)) return -1; 305 | } else if (bChar == _zero) { 306 | do { 307 | bIndex++; 308 | if (bIndex == b.length) return 1; // number in b is zero, a is not. 309 | bChar = b.codeUnitAt(bIndex); 310 | } while (bChar == _zero); 311 | if (!_isDigit(bChar)) return 1; 312 | } 313 | if (aChar != bChar) { 314 | var result = _compareDigitCount(a, b, aIndex, bIndex); 315 | if (result != 0) return result; 316 | return (aChar - bChar).sign; 317 | } 318 | // Same leading digit, one had more leading zeros. 319 | // Compare digits until reaching a difference. 320 | while (true) { 321 | var aIsDigit = false; 322 | var bIsDigit = false; 323 | aChar = 0; 324 | bChar = 0; 325 | if (++aIndex < a.length) { 326 | aChar = a.codeUnitAt(aIndex); 327 | aIsDigit = _isDigit(aChar); 328 | } 329 | if (++bIndex < b.length) { 330 | bChar = b.codeUnitAt(bIndex); 331 | bIsDigit = _isDigit(bChar); 332 | } 333 | if (aIsDigit) { 334 | if (bIsDigit) { 335 | if (aChar == bChar) continue; 336 | // First different digit found. 337 | break; 338 | } 339 | // bChar is non-digit, so a has longer number. 340 | return 1; 341 | } else if (bIsDigit) { 342 | return -1; // b has longer number. 343 | } else { 344 | // Neither is digit, so numbers had same numerical value. 345 | // Fall back on number of leading zeros 346 | // (reflected by difference in indices). 347 | return (aIndex - bIndex).sign; 348 | } 349 | } 350 | // At first differing digits. 351 | var result = _compareDigitCount(a, b, aIndex, bIndex); 352 | if (result != 0) return result; 353 | return (aChar - bChar).sign; 354 | } 355 | 356 | /// Checks which of [a] and [b] has the longest sequence of digits. 357 | /// 358 | /// Starts counting from `i + 1` and `j + 1` (assumes that `a[i]` and `b[j]` are 359 | /// both already known to be digits). 360 | int _compareDigitCount(String a, String b, int i, int j) { 361 | while (++i < a.length) { 362 | var aIsDigit = _isDigit(a.codeUnitAt(i)); 363 | if (++j == b.length) return aIsDigit ? 1 : 0; 364 | var bIsDigit = _isDigit(b.codeUnitAt(j)); 365 | if (aIsDigit) { 366 | if (bIsDigit) continue; 367 | return 1; 368 | } else if (bIsDigit) { 369 | return -1; 370 | } else { 371 | return 0; 372 | } 373 | } 374 | if (++j < b.length && _isDigit(b.codeUnitAt(j))) { 375 | return -1; 376 | } 377 | return 0; 378 | } 379 | 380 | bool _isDigit(int charCode) => (charCode ^ _zero) <= 9; 381 | 382 | /// Check if the digit at [index] is continuing a non-zero number. 383 | /// 384 | /// If there is no non-zero digits before, then leading zeros at [index] 385 | /// are also ignored when comparing numerically. If there is a non-zero digit 386 | /// before, then zeros at [index] are significant. 387 | bool _isNonZeroNumberSuffix(String string, int index) { 388 | while (--index >= 0) { 389 | var char = string.codeUnitAt(index); 390 | if (char != _zero) return _isDigit(char); 391 | } 392 | return false; 393 | } 394 | -------------------------------------------------------------------------------- /lib/src/empty_unmodifiable_set.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:collection'; 6 | 7 | import 'unmodifiable_wrappers.dart'; 8 | import 'wrappers.dart'; 9 | 10 | /// An unmodifiable, empty set which can be constant. 11 | class EmptyUnmodifiableSet extends IterableBase 12 | with UnmodifiableSetMixin 13 | implements UnmodifiableSetView { 14 | const EmptyUnmodifiableSet(); 15 | 16 | @override 17 | Iterator get iterator => Iterable.empty().iterator; 18 | @override 19 | int get length => 0; 20 | @override 21 | EmptyUnmodifiableSet cast() => EmptyUnmodifiableSet(); 22 | @override 23 | bool contains(Object? element) => false; 24 | @override 25 | bool containsAll(Iterable other) => other.isEmpty; 26 | @override 27 | Iterable followedBy(Iterable other) => DelegatingIterable(other); 28 | @override 29 | E? lookup(Object? element) => null; 30 | @Deprecated('Use cast instead') 31 | @override 32 | EmptyUnmodifiableSet retype() => EmptyUnmodifiableSet(); 33 | @override 34 | E singleWhere(bool Function(E) test, {E Function()? orElse}) => 35 | orElse != null ? orElse() : throw StateError('No element'); 36 | @override 37 | Iterable whereType() => Iterable.empty(); 38 | @override 39 | Set toSet() => {}; 40 | @override 41 | Set union(Set other) => Set.of(other); 42 | @override 43 | Set intersection(Set other) => {}; 44 | @override 45 | Set difference(Set other) => {}; 46 | } 47 | -------------------------------------------------------------------------------- /lib/src/equality_map.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:collection'; 6 | 7 | import 'equality.dart'; 8 | import 'wrappers.dart'; 9 | 10 | /// A [Map] whose key equality is determined by an [Equality] object. 11 | class EqualityMap extends DelegatingMap { 12 | /// Creates a map with equality based on [equality]. 13 | EqualityMap(Equality equality) 14 | : super(LinkedHashMap( 15 | equals: equality.equals, 16 | hashCode: equality.hash, 17 | isValidKey: equality.isValidKey)); 18 | 19 | /// Creates a map with equality based on [equality] that contains all 20 | /// key-value pairs of [other]. 21 | /// 22 | /// If [other] has multiple keys that are equivalent according to [equality], 23 | /// the last one reached during iteration takes precedence. 24 | EqualityMap.from(Equality equality, Map other) 25 | : super(LinkedHashMap( 26 | equals: equality.equals, 27 | hashCode: equality.hash, 28 | isValidKey: equality.isValidKey)) { 29 | addAll(other); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/src/equality_set.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:collection'; 6 | 7 | import 'equality.dart'; 8 | import 'wrappers.dart'; 9 | 10 | /// A [Set] whose key equality is determined by an [Equality] object. 11 | class EqualitySet extends DelegatingSet { 12 | /// Creates a set with equality based on [equality]. 13 | EqualitySet(Equality equality) 14 | : super(LinkedHashSet( 15 | equals: equality.equals, 16 | hashCode: equality.hash, 17 | isValidKey: equality.isValidKey)); 18 | 19 | /// Creates a set with equality based on [equality] that contains all 20 | /// elements in [other]. 21 | /// 22 | /// If [other] has multiple values that are equivalent according to 23 | /// [equality], the first one reached during iteration takes precedence. 24 | EqualitySet.from(Equality equality, Iterable other) 25 | : super(LinkedHashSet( 26 | equals: equality.equals, 27 | hashCode: equality.hash, 28 | isValidKey: equality.isValidKey)) { 29 | addAll(other); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/src/functions.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:collection'; 6 | import 'dart:math' as math; 7 | 8 | import 'utils.dart'; 9 | 10 | /// Creates a new map from [map] with new keys and values. 11 | /// 12 | /// The return values of [key] are used as the keys and the return values of 13 | /// [value] are used as the values for the new map. 14 | @Deprecated('Use Map.map or a for loop in a Map literal.') 15 | Map mapMap(Map map, 16 | {K2 Function(K1, V1)? key, V2 Function(K1, V1)? value}) { 17 | var keyFn = key ?? (mapKey, _) => mapKey as K2; 18 | var valueFn = value ?? (_, mapValue) => mapValue as V2; 19 | 20 | var result = {}; 21 | map.forEach((mapKey, mapValue) { 22 | result[keyFn(mapKey, mapValue)] = valueFn(mapKey, mapValue); 23 | }); 24 | return result; 25 | } 26 | 27 | /// Returns a new map with all key/value pairs in both [map1] and [map2]. 28 | /// 29 | /// If there are keys that occur in both maps, the [value] function is used to 30 | /// select the value that goes into the resulting map based on the two original 31 | /// values. If [value] is omitted, the value from [map2] is used. 32 | Map mergeMaps(Map map1, Map map2, 33 | {V Function(V, V)? value}) { 34 | var result = Map.of(map1); 35 | if (value == null) return result..addAll(map2); 36 | 37 | map2.forEach((key, mapValue) { 38 | result[key] = 39 | result.containsKey(key) ? value(result[key] as V, mapValue) : mapValue; 40 | }); 41 | return result; 42 | } 43 | 44 | /// Associates the elements in [values] by the value returned by [key]. 45 | /// 46 | /// Returns a map from keys computed by [key] to the last value for which [key] 47 | /// returns that key. 48 | Map lastBy(Iterable values, T Function(S) key) => 49 | {for (var element in values) key(element): element}; 50 | 51 | /// Groups the elements in [values] by the value returned by [key]. 52 | /// 53 | /// Returns a map from keys computed by [key] to a list of all values for which 54 | /// [key] returns that key. The values appear in the list in the same relative 55 | /// order as in [values]. 56 | Map> groupBy(Iterable values, T Function(S) key) { 57 | var map = >{}; 58 | for (var element in values) { 59 | (map[key(element)] ??= []).add(element); 60 | } 61 | return map; 62 | } 63 | 64 | /// Returns the element of [values] for which [orderBy] returns the minimum 65 | /// value. 66 | /// 67 | /// The values returned by [orderBy] are compared using the [compare] function. 68 | /// If [compare] is omitted, values must implement [Comparable]`` and they 69 | /// are compared using their [Comparable.compareTo]. 70 | /// 71 | /// Returns `null` if [values] is empty. 72 | S? minBy(Iterable values, T Function(S) orderBy, 73 | {int Function(T, T)? compare}) { 74 | compare ??= defaultCompare; 75 | 76 | S? minValue; 77 | T? minOrderBy; 78 | for (var element in values) { 79 | var elementOrderBy = orderBy(element); 80 | if (minOrderBy == null || compare(elementOrderBy, minOrderBy) < 0) { 81 | minValue = element; 82 | minOrderBy = elementOrderBy; 83 | } 84 | } 85 | return minValue; 86 | } 87 | 88 | /// Returns the element of [values] for which [orderBy] returns the maximum 89 | /// value. 90 | /// 91 | /// The values returned by [orderBy] are compared using the [compare] function. 92 | /// If [compare] is omitted, values must implement [Comparable]`` and they 93 | /// are compared using their [Comparable.compareTo]. 94 | /// 95 | /// Returns `null` if [values] is empty. 96 | S? maxBy(Iterable values, T Function(S) orderBy, 97 | {int Function(T, T)? compare}) { 98 | compare ??= defaultCompare; 99 | 100 | S? maxValue; 101 | T? maxOrderBy; 102 | for (var element in values) { 103 | var elementOrderBy = orderBy(element); 104 | if (maxOrderBy == null || compare(elementOrderBy, maxOrderBy) > 0) { 105 | maxValue = element; 106 | maxOrderBy = elementOrderBy; 107 | } 108 | } 109 | return maxValue; 110 | } 111 | 112 | /// Returns the [transitive closure][] of [graph]. 113 | /// 114 | /// [transitive closure]: https://en.wikipedia.org/wiki/Transitive_closure 115 | /// 116 | /// Interprets [graph] as a directed graph with a vertex for each key and edges 117 | /// from each key to the values that the key maps to. 118 | /// 119 | /// Assumes that every vertex in the graph has a key to represent it, even if 120 | /// that vertex has no outgoing edges. This isn't checked, but if it's not 121 | /// satisfied, the function may crash or provide unexpected output. For example, 122 | /// `{"a": ["b"]}` is not valid, but `{"a": ["b"], "b": []}` is. 123 | @Deprecated('This method will be removed. Consider using package:graphs.') 124 | Map> transitiveClosure(Map> graph) { 125 | // This uses [Warshall's algorithm][], modified not to add a vertex from each 126 | // node to itself. 127 | // 128 | // [Warshall's algorithm]: https://en.wikipedia.org/wiki/Floyd%E2%80%93Warshall_algorithm#Applications_and_generalizations. 129 | var result = >{}; 130 | graph.forEach((vertex, edges) { 131 | result[vertex] = Set.from(edges); 132 | }); 133 | 134 | // Lists are faster to iterate than maps, so we create a list since we're 135 | // iterating repeatedly. 136 | var keys = graph.keys.toList(); 137 | for (var vertex1 in keys) { 138 | for (var vertex2 in keys) { 139 | for (var vertex3 in keys) { 140 | if (result[vertex2]!.contains(vertex1) && 141 | result[vertex1]!.contains(vertex3)) { 142 | result[vertex2]!.add(vertex3); 143 | } 144 | } 145 | } 146 | } 147 | 148 | return result; 149 | } 150 | 151 | /// Returns the [strongly connected components][] of [graph], in topological 152 | /// order. 153 | /// 154 | /// [strongly connected components]: https://en.wikipedia.org/wiki/Strongly_connected_component 155 | /// 156 | /// Interprets [graph] as a directed graph with a vertex for each key and edges 157 | /// from each key to the values that the key maps to. 158 | /// 159 | /// Assumes that every vertex in the graph has a key to represent it, even if 160 | /// that vertex has no outgoing edges. This isn't checked, but if it's not 161 | /// satisfied, the function may crash or provide unexpected output. For example, 162 | /// `{"a": ["b"]}` is not valid, but `{"a": ["b"], "b": []}` is. 163 | List> stronglyConnectedComponents(Map> graph) { 164 | // This uses [Tarjan's algorithm][]. 165 | // 166 | // [Tarjan's algorithm]: https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm 167 | var index = 0; 168 | var stack = []; 169 | var result = >[]; 170 | 171 | // The order of these doesn't matter, so we use un-linked implementations to 172 | // avoid unnecessary overhead. 173 | var indices = HashMap(); 174 | var lowLinks = HashMap(); 175 | var onStack = HashSet(); 176 | 177 | void strongConnect(T vertex) { 178 | indices[vertex] = index; 179 | lowLinks[vertex] = index; 180 | index++; 181 | 182 | stack.add(vertex); 183 | onStack.add(vertex); 184 | 185 | for (var successor in graph[vertex]!) { 186 | if (!indices.containsKey(successor)) { 187 | strongConnect(successor); 188 | lowLinks[vertex] = math.min(lowLinks[vertex]!, lowLinks[successor]!); 189 | } else if (onStack.contains(successor)) { 190 | lowLinks[vertex] = math.min(lowLinks[vertex]!, lowLinks[successor]!); 191 | } 192 | } 193 | 194 | if (lowLinks[vertex] == indices[vertex]) { 195 | var component = {}; 196 | T? neighbor; 197 | do { 198 | neighbor = stack.removeLast(); 199 | onStack.remove(neighbor); 200 | component.add(neighbor as T); 201 | } while (neighbor != vertex); 202 | result.add(component); 203 | } 204 | } 205 | 206 | for (var vertex in graph.keys) { 207 | if (!indices.containsKey(vertex)) strongConnect(vertex); 208 | } 209 | 210 | // Tarjan's algorithm produces a reverse-topological sort, so we reverse it to 211 | // get a normal topological sort. 212 | return result.reversed.toList(); 213 | } 214 | -------------------------------------------------------------------------------- /lib/src/iterable_zip.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:collection'; 6 | 7 | /// Iterable that iterates over lists of values from other iterables. 8 | /// 9 | /// When [iterator] is read, an [Iterator] is created for each [Iterable] in 10 | /// the [Iterable] passed to the constructor. 11 | /// 12 | /// As long as all these iterators have a next value, those next values are 13 | /// combined into a single list, which becomes the next value of this 14 | /// [Iterable]'s [Iterator]. As soon as any of the iterators run out, 15 | /// the zipped iterator also stops. 16 | class IterableZip extends IterableBase> { 17 | final Iterable> _iterables; 18 | 19 | IterableZip(Iterable> iterables) : _iterables = iterables; 20 | 21 | /// Returns an iterator that combines values of the iterables' iterators 22 | /// as long as they all have values. 23 | @override 24 | Iterator> get iterator { 25 | var iterators = _iterables.map((x) => x.iterator).toList(growable: false); 26 | return _IteratorZip(iterators); 27 | } 28 | } 29 | 30 | class _IteratorZip implements Iterator> { 31 | final List> _iterators; 32 | List? _current; 33 | 34 | _IteratorZip(List> iterators) : _iterators = iterators; 35 | 36 | @override 37 | bool moveNext() { 38 | if (_iterators.isEmpty) return false; 39 | for (var i = 0; i < _iterators.length; i++) { 40 | if (!_iterators[i].moveNext()) { 41 | _current = null; 42 | return false; 43 | } 44 | } 45 | _current = List.generate(_iterators.length, (i) => _iterators[i].current, 46 | growable: false); 47 | return true; 48 | } 49 | 50 | @override 51 | List get current => _current ?? (throw StateError('No element')); 52 | } 53 | -------------------------------------------------------------------------------- /lib/src/queue_list.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:collection'; 6 | 7 | /// A class that efficiently implements both [Queue] and [List]. 8 | // TODO(nweiz): Currently this code is copied almost verbatim from 9 | // dart:collection. The only changes are to implement List and to remove methods 10 | // that are redundant with ListMixin. Remove or simplify it when issue 21330 is 11 | // fixed. 12 | class QueueList extends Object with ListMixin implements Queue { 13 | /// Adapts [source] to be a `QueueList`. 14 | /// 15 | /// Any time the class would produce an element that is not a [T], the element 16 | /// access will throw. 17 | /// 18 | /// Any time a [T] value is attempted stored into the adapted class, the store 19 | /// will throw unless the value is also an instance of [S]. 20 | /// 21 | /// If all accessed elements of [source] are actually instances of [T] and if 22 | /// all elements stored in the returned are actually instance of [S], 23 | /// then the returned instance can be used as a `QueueList`. 24 | static QueueList _castFrom(QueueList source) { 25 | return _CastQueueList(source); 26 | } 27 | 28 | /// Default and minimal initial capacity of the queue-list. 29 | static const int _initialCapacity = 8; 30 | List _table; 31 | int _head; 32 | int _tail; 33 | 34 | /// Creates an empty queue. 35 | /// 36 | /// If [initialCapacity] is given, prepare the queue for at least that many 37 | /// elements. 38 | QueueList([int? initialCapacity]) 39 | : this._init(_computeInitialCapacity(initialCapacity)); 40 | 41 | /// Creates an empty queue with the specific initial capacity. 42 | QueueList._init(int initialCapacity) 43 | : assert(_isPowerOf2(initialCapacity)), 44 | _table = List.filled(initialCapacity, null), 45 | _head = 0, 46 | _tail = 0; 47 | 48 | /// An internal constructor for use by [_CastQueueList]. 49 | QueueList._(this._head, this._tail, this._table); 50 | 51 | /// Create a queue initially containing the elements of [source]. 52 | factory QueueList.from(Iterable source) { 53 | if (source is List) { 54 | var length = source.length; 55 | var queue = QueueList(length + 1); 56 | assert(queue._table.length > length); 57 | var sourceList = source; 58 | queue._table.setRange(0, length, sourceList, 0); 59 | queue._tail = length; 60 | return queue; 61 | } else { 62 | return QueueList()..addAll(source); 63 | } 64 | } 65 | 66 | /// Computes the actual initial capacity based on the constructor parameter. 67 | static int _computeInitialCapacity(int? initialCapacity) { 68 | if (initialCapacity == null || initialCapacity < _initialCapacity) { 69 | return _initialCapacity; 70 | } 71 | initialCapacity += 1; 72 | if (_isPowerOf2(initialCapacity)) { 73 | return initialCapacity; 74 | } 75 | return _nextPowerOf2(initialCapacity); 76 | } 77 | 78 | // Collection interface. 79 | 80 | @override 81 | void add(E element) { 82 | _add(element); 83 | } 84 | 85 | @override 86 | void addAll(Iterable iterable) { 87 | if (iterable is List) { 88 | var list = iterable; 89 | var addCount = list.length; 90 | var length = this.length; 91 | if (length + addCount >= _table.length) { 92 | _preGrow(length + addCount); 93 | // After preGrow, all elements are at the start of the list. 94 | _table.setRange(length, length + addCount, list, 0); 95 | _tail += addCount; 96 | } else { 97 | // Adding addCount elements won't reach _head. 98 | var endSpace = _table.length - _tail; 99 | if (addCount < endSpace) { 100 | _table.setRange(_tail, _tail + addCount, list, 0); 101 | _tail += addCount; 102 | } else { 103 | var preSpace = addCount - endSpace; 104 | _table.setRange(_tail, _tail + endSpace, list, 0); 105 | _table.setRange(0, preSpace, list, endSpace); 106 | _tail = preSpace; 107 | } 108 | } 109 | } else { 110 | for (var element in iterable) { 111 | _add(element); 112 | } 113 | } 114 | } 115 | 116 | QueueList cast() => QueueList._castFrom(this); 117 | 118 | @Deprecated('Use cast instead') 119 | QueueList retype() => cast(); 120 | 121 | @override 122 | String toString() => IterableBase.iterableToFullString(this, '{', '}'); 123 | 124 | // Queue interface. 125 | 126 | @override 127 | void addLast(E element) { 128 | _add(element); 129 | } 130 | 131 | @override 132 | void addFirst(E element) { 133 | _head = (_head - 1) & (_table.length - 1); 134 | _table[_head] = element; 135 | if (_head == _tail) _grow(); 136 | } 137 | 138 | @override 139 | E removeFirst() { 140 | if (_head == _tail) throw StateError('No element'); 141 | var result = _table[_head] as E; 142 | _table[_head] = null; 143 | _head = (_head + 1) & (_table.length - 1); 144 | return result; 145 | } 146 | 147 | @override 148 | E removeLast() { 149 | if (_head == _tail) throw StateError('No element'); 150 | _tail = (_tail - 1) & (_table.length - 1); 151 | var result = _table[_tail] as E; 152 | _table[_tail] = null; 153 | return result; 154 | } 155 | 156 | // List interface. 157 | 158 | @override 159 | int get length => (_tail - _head) & (_table.length - 1); 160 | 161 | @override 162 | set length(int value) { 163 | if (value < 0) throw RangeError('Length $value may not be negative.'); 164 | if (value > length && null is! E) { 165 | throw UnsupportedError( 166 | 'The length can only be increased when the element type is ' 167 | 'nullable, but the current element type is `$E`.'); 168 | } 169 | 170 | var delta = value - length; 171 | if (delta >= 0) { 172 | if (_table.length <= value) { 173 | _preGrow(value); 174 | } 175 | _tail = (_tail + delta) & (_table.length - 1); 176 | return; 177 | } 178 | 179 | var newTail = _tail + delta; // [delta] is negative. 180 | if (newTail >= 0) { 181 | _table.fillRange(newTail, _tail, null); 182 | } else { 183 | newTail += _table.length; 184 | _table.fillRange(0, _tail, null); 185 | _table.fillRange(newTail, _table.length, null); 186 | } 187 | _tail = newTail; 188 | } 189 | 190 | @override 191 | E operator [](int index) { 192 | if (index < 0 || index >= length) { 193 | throw RangeError('Index $index must be in the range [0..$length).'); 194 | } 195 | 196 | return _table[(_head + index) & (_table.length - 1)] as E; 197 | } 198 | 199 | @override 200 | void operator []=(int index, E value) { 201 | if (index < 0 || index >= length) { 202 | throw RangeError('Index $index must be in the range [0..$length).'); 203 | } 204 | 205 | _table[(_head + index) & (_table.length - 1)] = value; 206 | } 207 | 208 | // Internal helper functions. 209 | 210 | /// Whether [number] is a power of two. 211 | /// 212 | /// Only works for positive numbers. 213 | static bool _isPowerOf2(int number) => (number & (number - 1)) == 0; 214 | 215 | /// Rounds [number] up to the nearest power of 2. 216 | /// 217 | /// If [number] is a power of 2 already, it is returned. 218 | /// 219 | /// Only works for positive numbers. 220 | static int _nextPowerOf2(int number) { 221 | assert(number > 0); 222 | number = (number << 1) - 1; 223 | for (;;) { 224 | var nextNumber = number & (number - 1); 225 | if (nextNumber == 0) return number; 226 | number = nextNumber; 227 | } 228 | } 229 | 230 | /// Adds element at end of queue. Used by both [add] and [addAll]. 231 | void _add(E element) { 232 | _table[_tail] = element; 233 | _tail = (_tail + 1) & (_table.length - 1); 234 | if (_head == _tail) _grow(); 235 | } 236 | 237 | /// Grow the table when full. 238 | void _grow() { 239 | var newTable = List.filled(_table.length * 2, null); 240 | var split = _table.length - _head; 241 | newTable.setRange(0, split, _table, _head); 242 | newTable.setRange(split, split + _head, _table, 0); 243 | _head = 0; 244 | _tail = _table.length; 245 | _table = newTable; 246 | } 247 | 248 | int _writeToList(List target) { 249 | assert(target.length >= length); 250 | if (_head <= _tail) { 251 | var length = _tail - _head; 252 | target.setRange(0, length, _table, _head); 253 | return length; 254 | } else { 255 | var firstPartSize = _table.length - _head; 256 | target.setRange(0, firstPartSize, _table, _head); 257 | target.setRange(firstPartSize, firstPartSize + _tail, _table, 0); 258 | return _tail + firstPartSize; 259 | } 260 | } 261 | 262 | /// Grows the table even if it is not full. 263 | void _preGrow(int newElementCount) { 264 | assert(newElementCount >= length); 265 | 266 | // Add 1.5x extra room to ensure that there's room for more elements after 267 | // expansion. 268 | newElementCount += newElementCount >> 1; 269 | var newCapacity = _nextPowerOf2(newElementCount); 270 | var newTable = List.filled(newCapacity, null); 271 | _tail = _writeToList(newTable); 272 | _table = newTable; 273 | _head = 0; 274 | } 275 | } 276 | 277 | class _CastQueueList extends QueueList { 278 | final QueueList _delegate; 279 | 280 | // Assigns invalid values for head/tail because it uses the delegate to hold 281 | // the real values, but they are non-null fields. 282 | _CastQueueList(this._delegate) : super._(-1, -1, _delegate._table.cast()); 283 | 284 | @override 285 | int get _head => _delegate._head; 286 | 287 | @override 288 | set _head(int value) => _delegate._head = value; 289 | 290 | @override 291 | int get _tail => _delegate._tail; 292 | 293 | @override 294 | set _tail(int value) => _delegate._tail = value; 295 | } 296 | -------------------------------------------------------------------------------- /lib/src/union_set.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:collection'; 6 | 7 | import 'unmodifiable_wrappers.dart'; 8 | 9 | /// A single set that provides a view of the union over a set of sets. 10 | /// 11 | /// Since this is just a view, it reflects all changes in the underlying sets. 12 | /// 13 | /// If an element is in multiple sets and the outer set is ordered, the version 14 | /// in the earliest inner set is preferred. Component sets are assumed to use 15 | /// `==` and `hashCode` for equality. 16 | class UnionSet extends SetBase with UnmodifiableSetMixin { 17 | /// The set of sets that this provides a view of. 18 | final Set> _sets; 19 | 20 | /// Whether the sets in [_sets] are guaranteed to be disjoint. 21 | final bool _disjoint; 22 | 23 | /// Creates a new set that's a view of the union of all sets in [sets]. 24 | /// 25 | /// If any sets in [sets] change, this [UnionSet] reflects that change. If a 26 | /// new set is added to [sets], this [UnionSet] reflects that as well. 27 | /// 28 | /// If [disjoint] is `true`, then all component sets must be disjoint. That 29 | /// is, that they contain no elements in common. This makes many operations 30 | /// including [length] more efficient. If the component sets turn out not to 31 | /// be disjoint, some operations may behave inconsistently. 32 | UnionSet(Set> sets, {bool disjoint = false}) 33 | : _sets = sets, 34 | _disjoint = disjoint; 35 | 36 | /// Creates a new set that's a view of the union of all sets in [sets]. 37 | /// 38 | /// If any sets in [sets] change, this [UnionSet] reflects that change. 39 | /// However, unlike [UnionSet.new], this creates a copy of its parameter, so 40 | /// changes in [sets] aren't reflected in this [UnionSet]. 41 | /// 42 | /// If [disjoint] is `true`, then all component sets must be disjoint. That 43 | /// is, that they contain no elements in common. This makes many operations 44 | /// including [length] more efficient. If the component sets turn out not to 45 | /// be disjoint, some operations may behave inconsistently. 46 | UnionSet.from(Iterable> sets, {bool disjoint = false}) 47 | : this(sets.toSet(), disjoint: disjoint); 48 | 49 | @override 50 | int get length => _disjoint 51 | ? _sets.fold(0, (length, set) => length + set.length) 52 | : _iterable.length; 53 | 54 | @override 55 | Iterator get iterator => _iterable.iterator; 56 | 57 | /// An iterable over the contents of all [_sets]. 58 | /// 59 | /// If this is not a [_disjoint] union an extra set is used to deduplicate 60 | /// values. 61 | Iterable get _iterable { 62 | var allElements = _sets.expand((set) => set); 63 | return _disjoint ? allElements : allElements.where({}.add); 64 | } 65 | 66 | @override 67 | bool contains(Object? element) => _sets.any((set) => set.contains(element)); 68 | 69 | @override 70 | E? lookup(Object? element) { 71 | for (var set in _sets) { 72 | var result = set.lookup(element); 73 | if (result != null || set.contains(null)) return result; 74 | } 75 | return null; 76 | } 77 | 78 | @override 79 | Set toSet() => {for (var set in _sets) ...set}; 80 | } 81 | -------------------------------------------------------------------------------- /lib/src/union_set_controller.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'union_set.dart'; 6 | 7 | /// A controller that exposes a view of the union of a collection of sets. 8 | /// 9 | /// This is a convenience class for creating a [UnionSet] whose contents change 10 | /// over the lifetime of a class. For example: 11 | /// 12 | /// ```dart 13 | /// class Engine { 14 | /// Set get activeTests => _activeTestsGroup.set; 15 | /// final _activeTestsGroup = UnionSetController(); 16 | /// 17 | /// void addSuite(Suite suite) { 18 | /// _activeTestsGroup.add(suite.tests); 19 | /// _runSuite(suite); 20 | /// _activeTestsGroup.remove(suite.tests); 21 | /// } 22 | /// } 23 | /// ``` 24 | class UnionSetController { 25 | /// The [UnionSet] that provides a view of the union of sets in `this`. 26 | final UnionSet set; 27 | 28 | /// The sets whose union is exposed through [set]. 29 | final Set> _sets; 30 | 31 | /// Creates a set of sets that provides a view of the union of those sets. 32 | /// 33 | /// If [disjoint] is `true`, this assumes that all component sets are 34 | /// disjoint—that is, that they contain no elements in common. This makes 35 | /// many operations including `length` more efficient. 36 | UnionSetController({bool disjoint = false}) : this._(>{}, disjoint); 37 | 38 | /// Creates a controller with the provided [_sets]. 39 | UnionSetController._(this._sets, bool disjoint) 40 | : set = UnionSet(_sets, disjoint: disjoint); 41 | 42 | /// Adds the contents of [component] to [set]. 43 | /// 44 | /// If the contents of [component] change over time, [set] will change 45 | /// accordingly. 46 | void add(Set component) { 47 | _sets.add(component); 48 | } 49 | 50 | /// Removes the contents of [component] to [set]. 51 | /// 52 | /// If another set in `this` has overlapping elements with [component], those 53 | /// elements will remain in [set]. 54 | bool remove(Set component) => _sets.remove(component); 55 | } 56 | -------------------------------------------------------------------------------- /lib/src/unmodifiable_wrappers.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'empty_unmodifiable_set.dart'; 6 | import 'wrappers.dart'; 7 | 8 | export 'dart:collection' show UnmodifiableListView, UnmodifiableMapView; 9 | 10 | /// A fixed-length list. 11 | /// 12 | /// A `NonGrowableListView` contains a [List] object and ensures that 13 | /// its length does not change. 14 | /// Methods that would change the length of the list, 15 | /// such as [add] and [remove], throw an [UnsupportedError]. 16 | /// All other methods work directly on the underlying list. 17 | /// 18 | /// This class _does_ allow changes to the contents of the wrapped list. 19 | /// You can, for example, [sort] the list. 20 | /// Permitted operations defer to the wrapped list. 21 | class NonGrowableListView extends DelegatingList 22 | with NonGrowableListMixin { 23 | NonGrowableListView(super.listBase); 24 | } 25 | 26 | /// Mixin class that implements a throwing version of all list operations that 27 | /// change the List's length. 28 | abstract mixin class NonGrowableListMixin implements List { 29 | static Never _throw() { 30 | throw UnsupportedError('Cannot change the length of a fixed-length list'); 31 | } 32 | 33 | /// Throws an [UnsupportedError]; 34 | /// operations that change the length of the list are disallowed. 35 | @override 36 | set length(int newLength) => _throw(); 37 | 38 | /// Throws an [UnsupportedError]; 39 | /// operations that change the length of the list are disallowed. 40 | @override 41 | bool add(E value) => _throw(); 42 | 43 | /// Throws an [UnsupportedError]; 44 | /// operations that change the length of the list are disallowed. 45 | @override 46 | void addAll(Iterable iterable) => _throw(); 47 | 48 | /// Throws an [UnsupportedError]; 49 | /// operations that change the length of the list are disallowed. 50 | @override 51 | void insert(int index, E element) => _throw(); 52 | 53 | /// Throws an [UnsupportedError]; 54 | /// operations that change the length of the list are disallowed. 55 | @override 56 | void insertAll(int index, Iterable iterable) => _throw(); 57 | 58 | /// Throws an [UnsupportedError]; 59 | /// operations that change the length of the list are disallowed. 60 | @override 61 | bool remove(Object? value) => _throw(); 62 | 63 | /// Throws an [UnsupportedError]; 64 | /// operations that change the length of the list are disallowed. 65 | @override 66 | E removeAt(int index) => _throw(); 67 | 68 | /// Throws an [UnsupportedError]; 69 | /// operations that change the length of the list are disallowed. 70 | @override 71 | E removeLast() => _throw(); 72 | 73 | /// Throws an [UnsupportedError]; 74 | /// operations that change the length of the list are disallowed. 75 | @override 76 | void removeWhere(bool Function(E) test) => _throw(); 77 | 78 | /// Throws an [UnsupportedError]; 79 | /// operations that change the length of the list are disallowed. 80 | @override 81 | void retainWhere(bool Function(E) test) => _throw(); 82 | 83 | /// Throws an [UnsupportedError]; 84 | /// operations that change the length of the list are disallowed. 85 | @override 86 | void removeRange(int start, int end) => _throw(); 87 | 88 | /// Throws an [UnsupportedError]; 89 | /// operations that change the length of the list are disallowed. 90 | @override 91 | void replaceRange(int start, int end, Iterable iterable) => _throw(); 92 | 93 | /// Throws an [UnsupportedError]; 94 | /// operations that change the length of the list are disallowed. 95 | @override 96 | void clear() => _throw(); 97 | } 98 | 99 | /// An unmodifiable set. 100 | /// 101 | /// An [UnmodifiableSetView] contains a [Set], 102 | /// and prevents that set from being changed through the view. 103 | /// Methods that could change the set, 104 | /// such as [add] and [remove], throw an [UnsupportedError]. 105 | /// Permitted operations defer to the wrapped set. 106 | class UnmodifiableSetView extends DelegatingSet 107 | with UnmodifiableSetMixin { 108 | UnmodifiableSetView(super.setBase); 109 | 110 | /// An unmodifiable empty set. 111 | /// 112 | /// This is the same as `UnmodifiableSetView(Set())`, except that it 113 | /// can be used in const contexts. 114 | const factory UnmodifiableSetView.empty() = EmptyUnmodifiableSet; 115 | } 116 | 117 | /// Mixin class that implements a throwing version of all set operations that 118 | /// change the Set. 119 | abstract mixin class UnmodifiableSetMixin implements Set { 120 | static Never _throw() { 121 | throw UnsupportedError('Cannot modify an unmodifiable Set'); 122 | } 123 | 124 | /// Throws an [UnsupportedError]; 125 | /// operations that change the set are disallowed. 126 | @override 127 | bool add(E value) => _throw(); 128 | 129 | /// Throws an [UnsupportedError]; 130 | /// operations that change the set are disallowed. 131 | @override 132 | void addAll(Iterable elements) => _throw(); 133 | 134 | /// Throws an [UnsupportedError]; 135 | /// operations that change the set are disallowed. 136 | @override 137 | bool remove(Object? value) => _throw(); 138 | 139 | /// Throws an [UnsupportedError]; 140 | /// operations that change the set are disallowed. 141 | @override 142 | void removeAll(Iterable elements) => _throw(); 143 | 144 | /// Throws an [UnsupportedError]; 145 | /// operations that change the set are disallowed. 146 | @override 147 | void retainAll(Iterable elements) => _throw(); 148 | 149 | /// Throws an [UnsupportedError]; 150 | /// operations that change the set are disallowed. 151 | @override 152 | void removeWhere(bool Function(E) test) => _throw(); 153 | 154 | /// Throws an [UnsupportedError]; 155 | /// operations that change the set are disallowed. 156 | @override 157 | void retainWhere(bool Function(E) test) => _throw(); 158 | 159 | /// Throws an [UnsupportedError]; 160 | /// operations that change the set are disallowed. 161 | @override 162 | void clear() => _throw(); 163 | } 164 | 165 | /// Mixin class that implements a throwing version of all map operations that 166 | /// change the Map. 167 | abstract mixin class UnmodifiableMapMixin implements Map { 168 | static Never _throw() { 169 | throw UnsupportedError('Cannot modify an unmodifiable Map'); 170 | } 171 | 172 | /// Throws an [UnsupportedError]; 173 | /// operations that change the map are disallowed. 174 | @override 175 | void operator []=(K key, V value) => _throw(); 176 | 177 | /// Throws an [UnsupportedError]; 178 | /// operations that change the map are disallowed. 179 | @override 180 | V putIfAbsent(K key, V Function() ifAbsent) => _throw(); 181 | 182 | /// Throws an [UnsupportedError]; 183 | /// operations that change the map are disallowed. 184 | @override 185 | void addAll(Map other) => _throw(); 186 | 187 | /// Throws an [UnsupportedError]; 188 | /// operations that change the map are disallowed. 189 | @override 190 | V remove(Object? key) => _throw(); 191 | 192 | /// Throws an [UnsupportedError]; 193 | /// operations that change the map are disallowed. 194 | @override 195 | void clear() => _throw(); 196 | 197 | /// Throws an [UnsupportedError]; 198 | /// operations that change the map are disallowed. 199 | set first(_) => _throw(); 200 | 201 | /// Throws an [UnsupportedError]; 202 | /// operations that change the map are disallowed. 203 | set last(_) => _throw(); 204 | } 205 | -------------------------------------------------------------------------------- /lib/src/utils.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | /// A [Comparator] that asserts that its first argument is comparable. 6 | /// 7 | /// The function behaves just like [List.sort]'s 8 | /// default comparison function. It is entirely dynamic in its testing. 9 | /// 10 | /// Should be used when optimistically comparing object that are assumed 11 | /// to be comparable. 12 | /// If the elements are known to be comparable, use [compareComparable]. 13 | int defaultCompare(Object? value1, Object? value2) => 14 | (value1 as Comparable).compareTo(value2); 15 | 16 | /// A reusable identity function at any type. 17 | T identity(T value) => value; 18 | 19 | /// A reusable typed comparable comparator. 20 | int compareComparable>(T a, T b) => a.compareTo(b); 21 | -------------------------------------------------------------------------------- /lib/wrappers.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | /// Import `collection.dart` instead. 6 | @Deprecated('Will be removed in collection 2.0.0.') 7 | library; 8 | 9 | export 'src/canonicalized_map.dart'; 10 | export 'src/unmodifiable_wrappers.dart'; 11 | export 'src/wrappers.dart'; 12 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: collection 2 | version: 1.19.0 3 | description: >- 4 | Collections and utilities functions and classes related to collections. 5 | repository: https://github.com/dart-lang/collection 6 | 7 | topics: 8 | - data-structures 9 | - collections 10 | 11 | environment: 12 | sdk: ^3.4.0 13 | 14 | dev_dependencies: 15 | dart_flutter_team_lints: ^3.0.0 16 | test: ^1.16.0 17 | -------------------------------------------------------------------------------- /test/algorithms_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | /// Tests algorithm utilities. 6 | library; 7 | 8 | import 'dart:math'; 9 | 10 | import 'package:collection/collection.dart'; 11 | import 'package:collection/src/algorithms.dart'; 12 | import 'package:test/test.dart'; 13 | 14 | void main() { 15 | void testShuffle(List list) { 16 | var copy = list.toList(); 17 | shuffle(list); 18 | expect(const UnorderedIterableEquality().equals(list, copy), isTrue); 19 | } 20 | 21 | test('Shuffle 0', () { 22 | testShuffle([]); 23 | }); 24 | test('Shuffle 1', () { 25 | testShuffle([1]); 26 | }); 27 | test('Shuffle 3', () { 28 | testShuffle([1, 2, 3]); 29 | }); 30 | test('Shuffle 10', () { 31 | testShuffle([1, 2, 3, 4, 5, 1, 3, 5, 7, 9]); 32 | }); 33 | test('Shuffle shuffles', () { 34 | var l = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; 35 | var c = l.toList(); 36 | var count = 0; 37 | for (;;) { 38 | shuffle(l); 39 | if (!const ListEquality().equals(c, l)) return; 40 | // Odds of not changing the order should be one in ~ 16! ~= 2e+13. 41 | // Repeat this 10 times, and the odds of accidentally shuffling to the 42 | // same result every time is disappearingly tiny. 43 | count++; 44 | // If this happens even once, it's ok to report it. 45 | print('Failed shuffle $count times'); 46 | if (count == 10) fail("Shuffle didn't change order."); 47 | } 48 | }); 49 | test('Shuffle sublist', () { 50 | var l = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; 51 | var c = l.toList(); 52 | shuffle(l, 4, 12); 53 | expect(const IterableEquality().equals(l.getRange(0, 4), c.getRange(0, 4)), 54 | isTrue); 55 | expect( 56 | const IterableEquality().equals(l.getRange(12, 16), c.getRange(12, 16)), 57 | isTrue); 58 | expect( 59 | const UnorderedIterableEquality() 60 | .equals(l.getRange(4, 12), c.getRange(4, 12)), 61 | isTrue); 62 | }); 63 | 64 | test('binsearch0', () { 65 | expect(binarySearch([], 2), equals(-1)); 66 | }); 67 | 68 | test('binsearch1', () { 69 | expect(binarySearch([5], 2), equals(-1)); 70 | expect(binarySearch([5], 5), equals(0)); 71 | expect(binarySearch([5], 7), equals(-1)); 72 | }); 73 | 74 | test('binsearch3', () { 75 | expect(binarySearch([0, 5, 10], -1), equals(-1)); 76 | expect(binarySearch([0, 5, 10], 0), equals(0)); 77 | expect(binarySearch([0, 5, 10], 2), equals(-1)); 78 | expect(binarySearch([0, 5, 10], 5), equals(1)); 79 | expect(binarySearch([0, 5, 10], 7), equals(-1)); 80 | expect(binarySearch([0, 5, 10], 10), equals(2)); 81 | expect(binarySearch([0, 5, 10], 12), equals(-1)); 82 | }); 83 | 84 | test('binsearchCompare0', () { 85 | expect(binarySearch([], C(2), compare: compareC), equals(-1)); 86 | }); 87 | 88 | test('binsearchCompare1', () { 89 | var l1 = [C(5)]; 90 | expect(binarySearch(l1, C(2), compare: compareC), equals(-1)); 91 | expect(binarySearch(l1, C(5), compare: compareC), equals(0)); 92 | expect(binarySearch(l1, C(7), compare: compareC), equals(-1)); 93 | }); 94 | 95 | test('binsearchCompare3', () { 96 | var l3 = [C(0), C(5), C(10)]; 97 | expect(binarySearch(l3, C(-1), compare: compareC), equals(-1)); 98 | expect(binarySearch(l3, C(0), compare: compareC), equals(0)); 99 | expect(binarySearch(l3, C(2), compare: compareC), equals(-1)); 100 | expect(binarySearch(l3, C(5), compare: compareC), equals(1)); 101 | expect(binarySearch(l3, C(7), compare: compareC), equals(-1)); 102 | expect(binarySearch(l3, C(10), compare: compareC), equals(2)); 103 | expect(binarySearch(l3, C(12), compare: compareC), equals(-1)); 104 | }); 105 | 106 | test('lowerbound0', () { 107 | expect(lowerBound([], 2), equals(0)); 108 | }); 109 | 110 | test('lowerbound1', () { 111 | expect(lowerBound([5], 2), equals(0)); 112 | expect(lowerBound([5], 5), equals(0)); 113 | expect(lowerBound([5], 7), equals(1)); 114 | }); 115 | 116 | test('lowerbound3', () { 117 | expect(lowerBound([0, 5, 10], -1), equals(0)); 118 | expect(lowerBound([0, 5, 10], 0), equals(0)); 119 | expect(lowerBound([0, 5, 10], 2), equals(1)); 120 | expect(lowerBound([0, 5, 10], 5), equals(1)); 121 | expect(lowerBound([0, 5, 10], 7), equals(2)); 122 | expect(lowerBound([0, 5, 10], 10), equals(2)); 123 | expect(lowerBound([0, 5, 10], 12), equals(3)); 124 | }); 125 | 126 | test('lowerboundRepeat', () { 127 | expect(lowerBound([5, 5, 5], 5), equals(0)); 128 | expect(lowerBound([0, 5, 5, 5, 10], 5), equals(1)); 129 | }); 130 | 131 | test('lowerboundCompare0', () { 132 | expect(lowerBound([], C(2), compare: compareC), equals(0)); 133 | }); 134 | 135 | test('lowerboundCompare1', () { 136 | var l1 = [C(5)]; 137 | expect(lowerBound(l1, C(2), compare: compareC), equals(0)); 138 | expect(lowerBound(l1, C(5), compare: compareC), equals(0)); 139 | expect(lowerBound(l1, C(7), compare: compareC), equals(1)); 140 | }); 141 | 142 | test('lowerboundCompare3', () { 143 | var l3 = [C(0), C(5), C(10)]; 144 | expect(lowerBound(l3, C(-1), compare: compareC), equals(0)); 145 | expect(lowerBound(l3, C(0), compare: compareC), equals(0)); 146 | expect(lowerBound(l3, C(2), compare: compareC), equals(1)); 147 | expect(lowerBound(l3, C(5), compare: compareC), equals(1)); 148 | expect(lowerBound(l3, C(7), compare: compareC), equals(2)); 149 | expect(lowerBound(l3, C(10), compare: compareC), equals(2)); 150 | expect(lowerBound(l3, C(12), compare: compareC), equals(3)); 151 | }); 152 | 153 | test('lowerboundCompareRepeat', () { 154 | var l1 = [C(5), C(5), C(5)]; 155 | var l2 = [C(0), C(5), C(5), C(5), C(10)]; 156 | expect(lowerBound(l1, C(5), compare: compareC), equals(0)); 157 | expect(lowerBound(l2, C(5), compare: compareC), equals(1)); 158 | }); 159 | 160 | void testSort(String name, 161 | void Function(List elements, [int? start, int? end]) sort) { 162 | test('${name}Random', () { 163 | var random = Random(); 164 | for (var i = 0; i < 250; i += 10) { 165 | var list = [ 166 | for (var j = 0; j < i; j++) 167 | random.nextInt(25) // Expect some equal elements. 168 | ]; 169 | sort(list); 170 | for (var j = 1; j < i; j++) { 171 | expect(list[j - 1], lessThanOrEqualTo(list[j])); 172 | } 173 | } 174 | }); 175 | 176 | test('${name}SubRanges', () { 177 | var l = [6, 5, 4, 3, 2, 1]; 178 | sort(l, 2, 4); 179 | expect(l, equals([6, 5, 3, 4, 2, 1])); 180 | sort(l, 1, 1); 181 | expect(l, equals([6, 5, 3, 4, 2, 1])); 182 | sort(l, 4, 6); 183 | expect(l, equals([6, 5, 3, 4, 1, 2])); 184 | sort(l, 0, 2); 185 | expect(l, equals([5, 6, 3, 4, 1, 2])); 186 | sort(l, 0, 6); 187 | expect(l, equals([1, 2, 3, 4, 5, 6])); 188 | }); 189 | 190 | test('$name insertionSortSpecialCases', () { 191 | var l = [6, 6, 6, 6, 6, 6]; 192 | sort(l); 193 | expect(l, equals([6, 6, 6, 6, 6, 6])); 194 | 195 | l = [6, 6, 3, 3, 0, 0]; 196 | sort(l); 197 | expect(l, equals([0, 0, 3, 3, 6, 6])); 198 | }); 199 | } 200 | 201 | int intId(int x) => x; 202 | int intCompare(int a, int b) => a - b; 203 | testSort('insertionSort', (list, [start, end]) { 204 | insertionSortBy(list, intId, intCompare, start ?? 0, end ?? list.length); 205 | }); 206 | testSort('mergeSort compare', (list, [start, end]) { 207 | mergeSort(list, 208 | start: start ?? 0, end: end ?? list.length, compare: intCompare); 209 | }); 210 | testSort('mergeSort comparable', (list, [start, end]) { 211 | mergeSort(list, start: start ?? 0, end: end ?? list.length); 212 | }); 213 | testSort('mergeSortBy', (list, [start, end]) { 214 | mergeSortBy(list, intId, intCompare, start ?? 0, end ?? list.length); 215 | }); 216 | testSort('quickSort', (list, [start, end]) { 217 | quickSort(list, intCompare, start ?? 0, end ?? list.length); 218 | }); 219 | testSort('quickSortBy', (list, [start, end]) { 220 | quickSortBy(list, intId, intCompare, start ?? 0, end ?? list.length); 221 | }); 222 | test('MergeSortSpecialCases', () { 223 | for (var size in [511, 512, 513]) { 224 | // All equal. 225 | var list = List.generate(size, (i) => OC(0, i)); 226 | mergeSort(list); 227 | for (var i = 0; i < size; i++) { 228 | expect(list[i].order, equals(i)); 229 | } 230 | // All but one equal, first. 231 | list[0] = OC(1, 0); 232 | for (var i = 1; i < size; i++) { 233 | list[i] = OC(0, i); 234 | } 235 | mergeSort(list); 236 | for (var i = 0; i < size - 1; i++) { 237 | expect(list[i].order, equals(i + 1)); 238 | } 239 | expect(list[size - 1].order, equals(0)); 240 | 241 | // All but one equal, last. 242 | for (var i = 0; i < size - 1; i++) { 243 | list[i] = OC(0, i); 244 | } 245 | list[size - 1] = OC(-1, size - 1); 246 | mergeSort(list); 247 | expect(list[0].order, equals(size - 1)); 248 | for (var i = 1; i < size; i++) { 249 | expect(list[i].order, equals(i - 1)); 250 | } 251 | 252 | // Reversed. 253 | for (var i = 0; i < size; i++) { 254 | list[i] = OC(size - 1 - i, i); 255 | } 256 | mergeSort(list); 257 | for (var i = 0; i < size; i++) { 258 | expect(list[i].id, equals(i)); 259 | expect(list[i].order, equals(size - 1 - i)); 260 | } 261 | } 262 | }); 263 | 264 | void testSortBy( 265 | String name, 266 | void Function(List elements, K Function(T element) keyOf, 267 | int Function(K a, K b) compare, 268 | [int start, int end]) 269 | sort) { 270 | for (var n in [0, 1, 2, 10, 75, 250]) { 271 | var name2 = name; 272 | test('$name2: Same #$n', () { 273 | var list = List.generate(n, (i) => OC(i, 0)); 274 | // Should succeed. Bad implementations of, e.g., quicksort can diverge. 275 | sort(list, _ocOrder, _compareInt); 276 | }); 277 | test('$name: Pre-sorted #$n', () { 278 | var list = List.generate(n, (i) => OC(-i, i)); 279 | var expected = list.toList(); 280 | sort(list, _ocOrder, _compareInt); 281 | // Elements have not moved. 282 | expect(list, expected); 283 | }); 284 | test('$name: Reverse-sorted #$n', () { 285 | var list = List.generate(n, (i) => OC(i, -i)); 286 | sort(list, _ocOrder, _compareInt); 287 | expectSorted(list, _ocOrder, _compareInt); 288 | }); 289 | test('$name: Random #$n', () { 290 | var random = Random(); 291 | var list = List.generate(n, (i) => OC(i, random.nextInt(n))); 292 | sort(list, _ocOrder, _compareInt); 293 | expectSorted(list, _ocOrder, _compareInt); 294 | }); 295 | test('$name: Sublist #$n', () { 296 | var random = Random(); 297 | var list = List.generate(n, (i) => OC(i, random.nextInt(n))); 298 | var original = list.toList(); 299 | var start = n ~/ 4; 300 | var end = start * 3; 301 | sort(list, _ocOrder, _compareInt, start, end); 302 | expectSorted(list, _ocOrder, _compareInt, start, end); 303 | expect(list.sublist(0, start), original.sublist(0, start)); 304 | expect(list.sublist(end), original.sublist(end)); 305 | }); 306 | } 307 | } 308 | 309 | testSortBy('insertionSort', insertionSortBy); 310 | testSortBy('mergeSort', mergeSortBy); 311 | testSortBy('quickSortBy', quickSortBy); 312 | 313 | test('MergeSortPreservesOrder', () { 314 | var random = Random(); 315 | // Small case where only insertion call is called, 316 | // larger case where the internal moving insertion sort is used 317 | // larger cases with multiple splittings, numbers just around a power of 2. 318 | for (var size in [8, 50, 511, 512, 513]) { 319 | // Class OC compares using id. 320 | // With size elements with id's in the range 0..size/4, a number of 321 | // collisions are guaranteed. These should be sorted so that the 'order' 322 | // part of the objects are still in order. 323 | var list = [ 324 | for (var i = 0; i < size; i++) OC(random.nextInt(size >> 2), i) 325 | ]; 326 | mergeSort(list); 327 | var prev = list[0]; 328 | for (var i = 1; i < size; i++) { 329 | var next = list[i]; 330 | expect(prev.id, lessThanOrEqualTo(next.id)); 331 | if (next.id == prev.id) { 332 | expect(prev.order, lessThanOrEqualTo(next.order)); 333 | } 334 | prev = next; 335 | } 336 | // Reverse compare on part of list. 337 | List copy = list.toList(); 338 | var min = size >> 2; 339 | var max = size - min; 340 | mergeSort(list, 341 | start: min, end: max, compare: (a, b) => b.compareTo(a)); 342 | prev = list[min]; 343 | for (var i = min + 1; i < max; i++) { 344 | var next = list[i]; 345 | expect(prev.id, greaterThanOrEqualTo(next.id)); 346 | if (next.id == prev.id) { 347 | expect(prev.order, lessThanOrEqualTo(next.order)); 348 | } 349 | prev = next; 350 | } 351 | // Equals on OC objects is identity, so this means the parts before min, 352 | // and the parts after max, didn't change at all. 353 | expect(list.sublist(0, min), equals(copy.sublist(0, min))); 354 | expect(list.sublist(max), equals(copy.sublist(max))); 355 | } 356 | }); 357 | 358 | test('Reverse', () { 359 | var l = [6, 5, 4, 3, 2, 1]; 360 | reverse(l, 2, 4); 361 | expect(l, equals([6, 5, 3, 4, 2, 1])); 362 | reverse(l, 1, 1); 363 | expect(l, equals([6, 5, 3, 4, 2, 1])); 364 | reverse(l, 4, 6); 365 | expect(l, equals([6, 5, 3, 4, 1, 2])); 366 | reverse(l, 0, 2); 367 | expect(l, equals([5, 6, 3, 4, 1, 2])); 368 | reverse(l, 0, 6); 369 | expect(l, equals([2, 1, 4, 3, 6, 5])); 370 | }); 371 | 372 | test('mergeSort works when runtime generic is a subtype of the static type', 373 | () { 374 | // Regression test for https://github.com/dart-lang/collection/issues/317 375 | final length = 1000; // Larger than _mergeSortLimit 376 | // Out of order list, with first half guaranteed to empty first during 377 | // merge. 378 | final list = [ 379 | for (var i = 0; i < length / 2; i++) -i, 380 | for (var i = 0; i < length / 2; i++) i + length, 381 | ]; 382 | expect(() => mergeSort(list), returnsNormally); 383 | }); 384 | } 385 | 386 | class C { 387 | final int id; 388 | C(this.id); 389 | } 390 | 391 | int compareC(C one, C other) => one.id - other.id; 392 | 393 | /// Class naturally ordered by its first constructor argument. 394 | class OC implements Comparable { 395 | final int id; 396 | final int order; 397 | OC(this.id, this.order); 398 | 399 | @override 400 | int compareTo(OC other) => id - other.id; 401 | 402 | @override 403 | String toString() => 'OC[$id,$order]'; 404 | } 405 | 406 | int _ocOrder(OC oc) => oc.order; 407 | 408 | int _compareInt(int a, int b) => a - b; 409 | 410 | /// Check that a list is sorted according to [compare] of [keyOf] of elements. 411 | void expectSorted( 412 | List list, K Function(T element) keyOf, int Function(K a, K b) compare, 413 | [int start = 0, int? end]) { 414 | end ??= list.length; 415 | if (start == end) return; 416 | var prev = keyOf(list[start]); 417 | for (var i = start + 1; i < end; i++) { 418 | var next = keyOf(list[i]); 419 | expect(compare(prev, next), isNonPositive); 420 | prev = next; 421 | } 422 | } 423 | -------------------------------------------------------------------------------- /test/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: ../analysis_options.yaml 2 | 3 | # Turn off the avoid_dynamic_calls lint for the test/ directory. 4 | analyzer: 5 | errors: 6 | avoid_dynamic_calls: ignore 7 | inference_failure_on_collection_literal: ignore 8 | inference_failure_on_instance_creation: ignore 9 | -------------------------------------------------------------------------------- /test/boollist_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | // Tests for BoolList. 6 | 7 | import 'package:collection/collection.dart'; 8 | import 'package:test/test.dart'; 9 | 10 | void main() { 11 | bool generator(int index) { 12 | if (index < 512) { 13 | return index.isEven; 14 | } 15 | return false; 16 | } 17 | 18 | test('BoolList()', () { 19 | expect(BoolList(1024, fill: false), List.filled(1024, false)); 20 | 21 | expect(BoolList(1024, fill: true), List.filled(1024, true)); 22 | }); 23 | 24 | test('BoolList.empty()', () { 25 | expect(BoolList.empty(growable: true, capacity: 1024), []); 26 | 27 | expect(BoolList.empty(growable: false, capacity: 1024), []); 28 | }); 29 | 30 | test('BoolList.generate()', () { 31 | expect( 32 | BoolList.generate(1024, generator), 33 | List.generate(1024, generator), 34 | ); 35 | }); 36 | 37 | test('BoolList.of()', () { 38 | var src = List.generate(1024, generator); 39 | expect(BoolList.of(src), src); 40 | }); 41 | 42 | group('[], []=', () { 43 | test('RangeError', () { 44 | var b = BoolList(1024, fill: false); 45 | 46 | expect(() { 47 | // ignore: unnecessary_statements 48 | b[-1]; 49 | }, throwsRangeError); 50 | 51 | expect(() { 52 | // ignore: unnecessary_statements 53 | b[1024]; 54 | }, throwsRangeError); 55 | }); 56 | 57 | test('[], []=', () { 58 | var b = BoolList(1024, fill: false); 59 | 60 | bool posVal; 61 | for (var pos = 0; pos < 1024; ++pos) { 62 | posVal = generator(pos); 63 | b[pos] = posVal; 64 | expect(b[pos], posVal, reason: 'at pos $pos'); 65 | } 66 | }); 67 | }); 68 | 69 | group('length', () { 70 | test('shrink length', () { 71 | var b = BoolList(1024, fill: true, growable: true); 72 | 73 | b.length = 768; 74 | expect(b, List.filled(768, true)); 75 | 76 | b.length = 128; 77 | expect(b, List.filled(128, true)); 78 | 79 | b.length = 0; 80 | expect(b, []); 81 | }); 82 | 83 | test('expand from != 0', () { 84 | var b = BoolList(256, fill: true, growable: true); 85 | 86 | b.length = 384; 87 | expect(b, List.filled(384, false)..fillRange(0, 256, true)); 88 | 89 | b.length = 2048; 90 | expect(b, List.filled(2048, false)..fillRange(0, 256, true)); 91 | }); 92 | 93 | test('expand from = 0', () { 94 | var b = BoolList(0, growable: true); 95 | expect(b.length, 0); 96 | 97 | b.length = 256; 98 | expect(b, List.filled(256, false)); 99 | }); 100 | 101 | test('throw UnsupportedError', () { 102 | expect(() { 103 | BoolList(1024).length = 512; 104 | }, throwsUnsupportedError); 105 | }); 106 | }); 107 | 108 | group('fillRange', () { 109 | test('In one word', () { 110 | expect( 111 | BoolList(1024)..fillRange(32, 64, true), 112 | List.filled(1024, false)..fillRange(32, 64, true), 113 | ); 114 | 115 | expect( 116 | // BoolList.filled constructor isn't used due internal usage of 117 | // fillRange 118 | BoolList.generate(1024, (i) => true)..fillRange(32, 64, false), 119 | List.filled(1024, true)..fillRange(32, 64, false), 120 | ); 121 | }); 122 | 123 | test('In several words', () { 124 | expect( 125 | BoolList(1024)..fillRange(32, 128, true), 126 | List.filled(1024, false)..fillRange(32, 128, true), 127 | ); 128 | 129 | expect( 130 | // BoolList.filled constructor isn't used due internal usage of 131 | // fillRange 132 | BoolList.generate(1024, (i) => true)..fillRange(32, 128, false), 133 | List.filled(1024, true)..fillRange(32, 128, false), 134 | ); 135 | }); 136 | }); 137 | 138 | group('Iterator', () { 139 | test('Iterator', () { 140 | var b = BoolList.generate(1024, generator); 141 | var iter = b.iterator; 142 | 143 | expect(iter.current, false); 144 | for (var i = 0; i < 1024; i++) { 145 | expect(iter.moveNext(), true); 146 | 147 | expect(iter.current, generator(i), reason: 'at pos $i'); 148 | } 149 | 150 | expect(iter.moveNext(), false); 151 | expect(iter.current, false); 152 | }); 153 | 154 | test('throw ConcurrentModificationError', () { 155 | var b = BoolList(1024, fill: true, growable: true); 156 | 157 | var iter = b.iterator; 158 | 159 | iter.moveNext(); 160 | b.length = 512; 161 | expect(() { 162 | iter.moveNext(); 163 | }, throwsConcurrentModificationError); 164 | }); 165 | }); 166 | } 167 | -------------------------------------------------------------------------------- /test/canonicalized_map_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'package:collection/collection.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | void main() { 9 | group('with an empty canonicalized map', () { 10 | late CanonicalizedMap map; 11 | 12 | setUp(() { 13 | map = CanonicalizedMap(int.parse, isValidKey: RegExp(r'^\d+$').hasMatch); 14 | }); 15 | 16 | test('canonicalizes keys on set and get', () { 17 | map['1'] = 'value'; 18 | expect(map['01'], equals('value')); 19 | }); 20 | 21 | test('get returns null for uncanonicalizable key', () { 22 | expect(map['foo'], isNull); 23 | }); 24 | 25 | test('set affects nothing for uncanonicalizable key', () { 26 | map['foo'] = 'value'; 27 | expect(map['foo'], isNull); 28 | expect(map.containsKey('foo'), isFalse); 29 | expect(map.length, equals(0)); 30 | }); 31 | 32 | test('canonicalizes keys for addAll', () { 33 | map.addAll({'1': 'value 1', '2': 'value 2', '3': 'value 3'}); 34 | expect(map['01'], equals('value 1')); 35 | expect(map['02'], equals('value 2')); 36 | expect(map['03'], equals('value 3')); 37 | }); 38 | 39 | test('uses the final value for addAll collisions', () { 40 | map.addAll({'1': 'value 1', '01': 'value 2', '001': 'value 3'}); 41 | expect(map.length, equals(1)); 42 | expect(map['0001'], equals('value 3')); 43 | }); 44 | 45 | test('clear clears the map', () { 46 | map.addAll({'1': 'value 1', '2': 'value 2', '3': 'value 3'}); 47 | expect(map, isNot(isEmpty)); 48 | map.clear(); 49 | expect(map, isEmpty); 50 | }); 51 | 52 | test('canonicalizes keys for containsKey', () { 53 | map['1'] = 'value'; 54 | expect(map.containsKey('01'), isTrue); 55 | expect(map.containsKey('2'), isFalse); 56 | }); 57 | 58 | test('containsKey returns false for uncanonicalizable key', () { 59 | expect(map.containsKey('foo'), isFalse); 60 | }); 61 | 62 | test('canonicalizes keys for putIfAbsent', () { 63 | map['1'] = 'value'; 64 | expect(map.putIfAbsent('01', () => throw Exception("shouldn't run")), 65 | equals('value')); 66 | expect(map.putIfAbsent('2', () => 'new value'), equals('new value')); 67 | }); 68 | 69 | test('canonicalizes keys for remove', () { 70 | map['1'] = 'value'; 71 | expect(map.remove('2'), isNull); 72 | expect(map.remove('01'), equals('value')); 73 | expect(map, isEmpty); 74 | }); 75 | 76 | test('remove returns null for uncanonicalizable key', () { 77 | expect(map.remove('foo'), isNull); 78 | }); 79 | 80 | test('containsValue returns whether a value is in the map', () { 81 | map['1'] = 'value'; 82 | expect(map.containsValue('value'), isTrue); 83 | expect(map.containsValue('not value'), isFalse); 84 | }); 85 | 86 | test('isEmpty returns whether the map is empty', () { 87 | expect(map.isEmpty, isTrue); 88 | map['1'] = 'value'; 89 | expect(map.isEmpty, isFalse); 90 | map.remove('01'); 91 | expect(map.isEmpty, isTrue); 92 | }); 93 | 94 | test("isNotEmpty returns whether the map isn't empty", () { 95 | expect(map.isNotEmpty, isFalse); 96 | map['1'] = 'value'; 97 | expect(map.isNotEmpty, isTrue); 98 | map.remove('01'); 99 | expect(map.isNotEmpty, isFalse); 100 | }); 101 | 102 | test('length returns the number of pairs in the map', () { 103 | expect(map.length, equals(0)); 104 | map['1'] = 'value 1'; 105 | expect(map.length, equals(1)); 106 | map['01'] = 'value 01'; 107 | expect(map.length, equals(1)); 108 | map['02'] = 'value 02'; 109 | expect(map.length, equals(2)); 110 | }); 111 | 112 | test('uses original keys for keys', () { 113 | map['001'] = 'value 1'; 114 | map['02'] = 'value 2'; 115 | expect(map.keys, equals(['001', '02'])); 116 | }); 117 | 118 | test('uses original keys for forEach', () { 119 | map['001'] = 'value 1'; 120 | map['02'] = 'value 2'; 121 | 122 | var keys = []; 123 | map.forEach((key, value) => keys.add(key)); 124 | expect(keys, equals(['001', '02'])); 125 | }); 126 | 127 | test('values returns all values in the map', () { 128 | map.addAll( 129 | {'1': 'value 1', '01': 'value 01', '2': 'value 2', '03': 'value 03'}); 130 | 131 | expect(map.values, equals(['value 01', 'value 2', 'value 03'])); 132 | }); 133 | 134 | test('entries returns all key-value pairs in the map', () { 135 | map.addAll({ 136 | '1': 'value 1', 137 | '01': 'value 01', 138 | '2': 'value 2', 139 | }); 140 | 141 | var entries = map.entries.toList(); 142 | expect(entries[0].key, '01'); 143 | expect(entries[0].value, 'value 01'); 144 | expect(entries[1].key, '2'); 145 | expect(entries[1].value, 'value 2'); 146 | }); 147 | 148 | test('addEntries adds key-value pairs to the map', () { 149 | map.addEntries([ 150 | const MapEntry('1', 'value 1'), 151 | const MapEntry('01', 'value 01'), 152 | const MapEntry('2', 'value 2'), 153 | ]); 154 | expect(map, {'01': 'value 01', '2': 'value 2'}); 155 | }); 156 | 157 | test('cast returns a new map instance', () { 158 | expect(map.cast(), isNot(same(map))); 159 | }); 160 | }); 161 | 162 | group('CanonicalizedMap builds an informative string representation', () { 163 | dynamic map; 164 | setUp(() { 165 | map = CanonicalizedMap(int.parse, 166 | isValidKey: RegExp(r'^\d+$').hasMatch); 167 | }); 168 | 169 | test('for an empty map', () { 170 | expect(map.toString(), equals('{}')); 171 | }); 172 | 173 | test('for a map with one value', () { 174 | map.addAll({'1': 'value 1'}); 175 | expect(map.toString(), equals('{1: value 1}')); 176 | }); 177 | 178 | test('for a map with multiple values', () { 179 | map.addAll( 180 | {'1': 'value 1', '01': 'value 01', '2': 'value 2', '03': 'value 03'}); 181 | expect( 182 | map.toString(), equals('{01: value 01, 2: value 2, 03: value 03}')); 183 | }); 184 | 185 | test('for a map with a loop', () { 186 | map.addAll({'1': 'value 1', '2': map}); 187 | expect(map.toString(), equals('{1: value 1, 2: {...}}')); 188 | }); 189 | }); 190 | 191 | group('CanonicalizedMap.from', () { 192 | test('canonicalizes its keys', () { 193 | var map = CanonicalizedMap.from( 194 | {'1': 'value 1', '2': 'value 2', '3': 'value 3'}, int.parse); 195 | expect(map['01'], equals('value 1')); 196 | expect(map['02'], equals('value 2')); 197 | expect(map['03'], equals('value 3')); 198 | }); 199 | 200 | test('uses the final value for collisions', () { 201 | var map = CanonicalizedMap.from( 202 | {'1': 'value 1', '01': 'value 2', '001': 'value 3'}, int.parse); 203 | expect(map.length, equals(1)); 204 | expect(map['0001'], equals('value 3')); 205 | }); 206 | }); 207 | 208 | group('CanonicalizedMap.fromEntries', () { 209 | test('canonicalizes its keys', () { 210 | var map = CanonicalizedMap.fromEntries( 211 | {'1': 'value 1', '2': 'value 2', '3': 'value 3'}.entries, int.parse); 212 | expect(map['01'], equals('value 1')); 213 | expect(map['02'], equals('value 2')); 214 | expect(map['03'], equals('value 3')); 215 | }); 216 | 217 | test('uses the final value for collisions', () { 218 | var map = CanonicalizedMap.fromEntries( 219 | {'1': 'value 1', '01': 'value 2', '001': 'value 3'}.entries, 220 | int.parse); 221 | expect(map.length, equals(1)); 222 | expect(map['0001'], equals('value 3')); 223 | }); 224 | }); 225 | 226 | group('CanonicalizedMap.toMapOfCanonicalKeys', () { 227 | test('convert to a `Map`', () { 228 | var map = CanonicalizedMap.from( 229 | {'1': 'value 1', '2': 'value 2', '3': 'value 3'}, int.parse); 230 | 231 | var map2 = map.toMapOfCanonicalKeys(); 232 | 233 | expect(map2, isNot(isA())); 234 | 235 | expect(map2[1], equals('value 1')); 236 | expect(map2[2], equals('value 2')); 237 | expect(map2[3], equals('value 3')); 238 | 239 | expect(map2, equals({1: 'value 1', 2: 'value 2', 3: 'value 3'})); 240 | }); 241 | }); 242 | 243 | group('CanonicalizedMap.toMap', () { 244 | test('convert to a `Map`', () { 245 | var map = CanonicalizedMap.from( 246 | {'1': 'value 1', '2': 'value 2', '3': 'value 3'}, int.parse); 247 | 248 | var map2 = map.toMap(); 249 | 250 | expect(map2, isNot(isA())); 251 | 252 | expect(map2['1'], equals('value 1')); 253 | expect(map2['2'], equals('value 2')); 254 | expect(map2['3'], equals('value 3')); 255 | 256 | expect(map2, equals({'1': 'value 1', '2': 'value 2', '3': 'value 3'})); 257 | }); 258 | }); 259 | 260 | group('CanonicalizedMap.copy', () { 261 | test('copy instance', () { 262 | var map = CanonicalizedMap.from( 263 | {'1': 'value 1', '2': 'value 2', '3': 'value 3'}, int.parse); 264 | 265 | var map2 = map.copy(); 266 | 267 | expect(map2['01'], equals('value 1')); 268 | expect(map2['02'], equals('value 2')); 269 | expect(map2['03'], equals('value 3')); 270 | 271 | expect(map2['1'], equals('value 1')); 272 | expect(map2['2'], equals('value 2')); 273 | expect(map2['3'], equals('value 3')); 274 | 275 | expect(map2, equals({'1': 'value 1', '2': 'value 2', '3': 'value 3'})); 276 | 277 | var map3 = Map.fromEntries(map2.entries); 278 | 279 | expect(map3, equals({'1': 'value 1', '2': 'value 2', '3': 'value 3'})); 280 | }); 281 | }); 282 | } 283 | -------------------------------------------------------------------------------- /test/combined_wrapper/iterable_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'package:collection/collection.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | void main() { 9 | var iterable1 = Iterable.generate(3); 10 | var iterable2 = Iterable.generate(3, (i) => i + 3); 11 | var iterable3 = Iterable.generate(3, (i) => i + 6); 12 | 13 | test('should combine multiple iterables when iterating', () { 14 | var combined = CombinedIterableView([iterable1, iterable2, iterable3]); 15 | expect(combined, [0, 1, 2, 3, 4, 5, 6, 7, 8]); 16 | }); 17 | 18 | test('should combine multiple iterables with some empty ones', () { 19 | var combined = 20 | CombinedIterableView([iterable1, [], iterable2, [], iterable3, []]); 21 | expect(combined, [0, 1, 2, 3, 4, 5, 6, 7, 8]); 22 | }); 23 | 24 | test('should function as an empty iterable when no iterables are passed', () { 25 | var empty = const CombinedIterableView([]); 26 | expect(empty, isEmpty); 27 | }); 28 | 29 | test('should function as an empty iterable with all empty iterables', () { 30 | var empty = const CombinedIterableView([[], [], []]); 31 | expect(empty, isEmpty); 32 | }); 33 | 34 | test('should reflect changes from the underlying iterables', () { 35 | var list1 = []; 36 | var list2 = []; 37 | var combined = CombinedIterableView([list1, list2]); 38 | expect(combined, isEmpty); 39 | list1.addAll([1, 2]); 40 | list2.addAll([3, 4]); 41 | expect(combined, [1, 2, 3, 4]); 42 | expect(combined.last, 4); 43 | expect(combined.first, 1); 44 | }); 45 | 46 | test('should reflect changes from the iterable of iterables', () { 47 | var iterables = []; 48 | var combined = CombinedIterableView(iterables); 49 | expect(combined, isEmpty); 50 | expect(combined, hasLength(0)); 51 | 52 | iterables.add(iterable1); 53 | expect(combined, isNotEmpty); 54 | expect(combined, hasLength(3)); 55 | 56 | iterables.clear(); 57 | expect(combined, isEmpty); 58 | expect(combined, hasLength(0)); 59 | }); 60 | } 61 | -------------------------------------------------------------------------------- /test/combined_wrapper/list_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'package:collection/collection.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | import '../unmodifiable_collection_test.dart' as common; 9 | 10 | void main() { 11 | var list1 = [1, 2, 3]; 12 | var list2 = [4, 5, 6]; 13 | var list3 = [7, 8, 9]; 14 | var concat = [...list1, ...list2, ...list3]; 15 | 16 | // In every way possible this should test the same as an UnmodifiableListView. 17 | common.testUnmodifiableList( 18 | concat, CombinedListView([list1, list2, list3]), 'combineLists'); 19 | 20 | common.testUnmodifiableList(concat, 21 | CombinedListView([list1, [], list2, [], list3, []]), 'combineLists'); 22 | 23 | test('should function as an empty list when no lists are passed', () { 24 | var empty = CombinedListView([]); 25 | expect(empty, isEmpty); 26 | expect(empty.length, 0); 27 | expect(() => empty[0], throwsRangeError); 28 | }); 29 | 30 | test('should function as an empty list when only empty lists are passed', () { 31 | var empty = CombinedListView([[], [], []]); 32 | expect(empty, isEmpty); 33 | expect(empty.length, 0); 34 | expect(() => empty[0], throwsRangeError); 35 | }); 36 | 37 | test('should reflect underlying changes back to the combined list', () { 38 | var backing1 = []; 39 | var backing2 = []; 40 | var combined = CombinedListView([backing1, backing2]); 41 | expect(combined, isEmpty); 42 | backing1.addAll(list1); 43 | expect(combined, list1); 44 | backing2.addAll(list2); 45 | expect(combined, backing1.toList()..addAll(backing2)); 46 | }); 47 | 48 | test('should reflect underlying changes from the list of lists', () { 49 | var listOfLists = >[]; 50 | var combined = CombinedListView(listOfLists); 51 | expect(combined, isEmpty); 52 | listOfLists.add(list1); 53 | expect(combined, list1); 54 | listOfLists.add(list2); 55 | expect(combined, [...list1, ...list2]); 56 | listOfLists.clear(); 57 | expect(combined, isEmpty); 58 | }); 59 | 60 | test('should reflect underlying changes with a single list', () { 61 | var backing1 = []; 62 | var combined = CombinedListView([backing1]); 63 | expect(combined, isEmpty); 64 | backing1.addAll(list1); 65 | expect(combined, list1); 66 | }); 67 | } 68 | -------------------------------------------------------------------------------- /test/combined_wrapper/map_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:collection'; 6 | 7 | import 'package:collection/collection.dart'; 8 | import 'package:test/test.dart'; 9 | 10 | void main() { 11 | var map1 = const {1: 1, 2: 2, 3: 3}; 12 | var map2 = const {4: 4, 5: 5, 6: 6}; 13 | var map3 = const {7: 7, 8: 8, 9: 9}; 14 | var map4 = const {1: -1, 2: -2, 3: -3}; 15 | var concat = SplayTreeMap() 16 | // The duplicates map appears first here but last in the CombinedMapView 17 | // which has the opposite semantics of `concat`. Keys/values should be 18 | // returned from the first map that contains them. 19 | ..addAll(map4) 20 | ..addAll(map1) 21 | ..addAll(map2) 22 | ..addAll(map3); 23 | 24 | // In every way possible this should test the same as an UnmodifiableMapView. 25 | _testReadMap( 26 | concat, CombinedMapView([map1, map2, map3, map4]), 'CombinedMapView'); 27 | 28 | _testReadMap( 29 | concat, 30 | CombinedMapView([map1, {}, map2, {}, map3, {}, map4, {}]), 31 | 'CombinedMapView (some empty)'); 32 | 33 | test('should function as an empty map when no maps are passed', () { 34 | var empty = CombinedMapView([]); 35 | expect(empty, isEmpty); 36 | expect(empty.length, 0); 37 | }); 38 | 39 | test('should function as an empty map when only empty maps are passed', () { 40 | var empty = CombinedMapView([{}, {}, {}]); 41 | expect(empty, isEmpty); 42 | expect(empty.length, 0); 43 | }); 44 | 45 | test('should reflect underlying changes back to the combined map', () { 46 | var backing1 = {}; 47 | var backing2 = {}; 48 | var combined = CombinedMapView([backing1, backing2]); 49 | expect(combined, isEmpty); 50 | backing1.addAll(map1); 51 | expect(combined, map1); 52 | backing2.addAll(map2); 53 | expect(combined, Map.from(backing1)..addAll(backing2)); 54 | }); 55 | 56 | test('should reflect underlying changes with a single map', () { 57 | var backing1 = {}; 58 | var combined = CombinedMapView([backing1]); 59 | expect(combined, isEmpty); 60 | backing1.addAll(map1); 61 | expect(combined, map1); 62 | }); 63 | 64 | test('re-iterating keys produces same result', () { 65 | var combined = CombinedMapView([map1, map2, map3, map4]); 66 | var keys = combined.keys; 67 | expect(keys.toList(), keys.toList()); 68 | }); 69 | } 70 | 71 | void _testReadMap(Map original, Map wrapped, String name) { 72 | test('$name length', () { 73 | expect(wrapped.length, equals(original.length)); 74 | }); 75 | 76 | test('$name isEmpty', () { 77 | expect(wrapped.isEmpty, equals(original.isEmpty)); 78 | }); 79 | 80 | test('$name isNotEmpty', () { 81 | expect(wrapped.isNotEmpty, equals(original.isNotEmpty)); 82 | }); 83 | 84 | test('$name operator[]', () { 85 | expect(wrapped[0], equals(original[0])); 86 | expect(wrapped[999], equals(original[999])); 87 | }); 88 | 89 | test('$name containsKey', () { 90 | expect(wrapped.containsKey(0), equals(original.containsKey(0))); 91 | expect(wrapped.containsKey(999), equals(original.containsKey(999))); 92 | }); 93 | 94 | test('$name containsValue', () { 95 | expect(wrapped.containsValue(0), equals(original.containsValue(0))); 96 | expect(wrapped.containsValue(999), equals(original.containsValue(999))); 97 | }); 98 | 99 | test('$name forEach', () { 100 | var origCnt = 0; 101 | var wrapCnt = 0; 102 | wrapped.forEach((k, v) { 103 | wrapCnt += 1 << k + 3 * v; 104 | }); 105 | original.forEach((k, v) { 106 | origCnt += 1 << k + 3 * v; 107 | }); 108 | expect(wrapCnt, equals(origCnt)); 109 | }); 110 | 111 | test('$name keys', () { 112 | expect(wrapped.keys, orderedEquals(original.keys)); 113 | }); 114 | 115 | test('$name values', () { 116 | expect(wrapped.values, orderedEquals(original.values)); 117 | }); 118 | } 119 | -------------------------------------------------------------------------------- /test/comparators_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'package:collection/collection.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | void main() { 9 | var strings = [ 10 | '', 11 | '\x00', 12 | ' ', 13 | '+', 14 | '/', 15 | '0', 16 | '00', 17 | '000', 18 | '001', 19 | '01', 20 | '011', 21 | '1', 22 | '100', 23 | '11', 24 | '110', 25 | '9', 26 | ':', 27 | '=', 28 | '@', 29 | 'A', 30 | 'A0', 31 | 'A000A', 32 | 'A001A', 33 | 'A00A', 34 | 'A01A', 35 | 'A0A', 36 | 'A1A', 37 | 'AA', 38 | 'AAB', 39 | 'AB', 40 | 'Z', 41 | '[', 42 | '_', 43 | '`', 44 | 'a', 45 | 'a0', 46 | 'a000a', 47 | 'a001a', 48 | 'a00a', 49 | 'a01a', 50 | 'a0a', 51 | 'a1a', 52 | 'aa', 53 | 'aab', 54 | 'ab', 55 | 'z', 56 | '{', 57 | '~' 58 | ]; 59 | 60 | List sortedBy(int Function(String, String)? compare) => 61 | strings.toList() 62 | ..shuffle() 63 | ..sort(compare); 64 | 65 | test('String.compareTo', () { 66 | expect(sortedBy(null), strings); 67 | }); 68 | 69 | test('compareAsciiLowerCase', () { 70 | expect(sortedBy(compareAsciiLowerCase), sortedBy((a, b) { 71 | var delta = a.toLowerCase().compareTo(b.toLowerCase()); 72 | if (delta != 0) return delta; 73 | if (a == b) return 0; 74 | return a.compareTo(b); 75 | })); 76 | }); 77 | 78 | test('compareAsciiUpperCase', () { 79 | expect(sortedBy(compareAsciiUpperCase), sortedBy((a, b) { 80 | var delta = a.toUpperCase().compareTo(b.toUpperCase()); 81 | if (delta != 0) return delta; 82 | if (a == b) return 0; 83 | return a.compareTo(b); 84 | })); 85 | }); 86 | 87 | // Replace any digit sequence by ("0", value, length) as char codes. 88 | // This will sort alphabetically (by charcode) the way digits sort 89 | // numerically, and the leading 0 means it sorts like a digit 90 | // compared to non-digits. 91 | String replaceNumbers(String string) => 92 | string.replaceAllMapped(RegExp(r'\d+'), (m) { 93 | var digits = m[0]!; 94 | return String.fromCharCodes([0x30, int.parse(digits), digits.length]); 95 | }); 96 | 97 | test('compareNatural', () { 98 | expect(sortedBy(compareNatural), 99 | sortedBy((a, b) => replaceNumbers(a).compareTo(replaceNumbers(b)))); 100 | }); 101 | 102 | test('compareAsciiLowerCaseNatural', () { 103 | expect(sortedBy(compareAsciiLowerCaseNatural), sortedBy((a, b) { 104 | var delta = replaceNumbers(a.toLowerCase()) 105 | .compareTo(replaceNumbers(b.toLowerCase())); 106 | if (delta != 0) return delta; 107 | if (a == b) return 0; 108 | return a.compareTo(b); 109 | })); 110 | }); 111 | 112 | test('compareAsciiUpperCaseNatural', () { 113 | expect(sortedBy(compareAsciiUpperCaseNatural), sortedBy((a, b) { 114 | var delta = replaceNumbers(a.toUpperCase()) 115 | .compareTo(replaceNumbers(b.toUpperCase())); 116 | if (delta != 0) return delta; 117 | if (a == b) return 0; 118 | return a.compareTo(b); 119 | })); 120 | }); 121 | } 122 | -------------------------------------------------------------------------------- /test/equality_map_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'package:collection/collection.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | void main() { 9 | test('uses the given equality', () { 10 | var map = EqualityMap(const IterableEquality()); 11 | expect(map, isEmpty); 12 | 13 | map[[1, 2, 3]] = 1; 14 | expect(map, containsPair([1, 2, 3], 1)); 15 | 16 | map[[1, 2, 3]] = 2; 17 | expect(map, containsPair([1, 2, 3], 2)); 18 | 19 | map[[2, 3, 4]] = 3; 20 | expect(map, containsPair([1, 2, 3], 2)); 21 | expect(map, containsPair([2, 3, 4], 3)); 22 | }); 23 | 24 | test('EqualityMap.from() prefers the lattermost equivalent key', () { 25 | var map = EqualityMap.from(const IterableEquality(), { 26 | [1, 2, 3]: 1, 27 | [2, 3, 4]: 2, 28 | [1, 2, 3]: 3, 29 | [2, 3, 4]: 4, 30 | [1, 2, 3]: 5, 31 | [1, 2, 3]: 6, 32 | }); 33 | 34 | expect(map, containsPair([1, 2, 3], 6)); 35 | expect(map, containsPair([2, 3, 4], 4)); 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /test/equality_set_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'package:collection/collection.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | void main() { 9 | test('uses the given equality', () { 10 | var set = EqualitySet(const IterableEquality()); 11 | expect(set, isEmpty); 12 | 13 | var list1 = [1, 2, 3]; 14 | expect(set.add(list1), isTrue); 15 | expect(set, contains([1, 2, 3])); 16 | expect(set, contains(same(list1))); 17 | 18 | var list2 = [1, 2, 3]; 19 | expect(set.add(list2), isFalse); 20 | expect(set, contains([1, 2, 3])); 21 | expect(set, contains(same(list1))); 22 | expect(set, isNot(contains(same(list2)))); 23 | 24 | var list3 = [2, 3, 4]; 25 | expect(set.add(list3), isTrue); 26 | expect(set, contains(same(list1))); 27 | expect(set, contains(same(list3))); 28 | }); 29 | 30 | test('EqualitySet.from() prefers the lattermost equivalent value', () { 31 | var list1 = [1, 2, 3]; 32 | var list2 = [2, 3, 4]; 33 | var list3 = [1, 2, 3]; 34 | var list4 = [2, 3, 4]; 35 | var list5 = [1, 2, 3]; 36 | var list6 = [1, 2, 3]; 37 | 38 | var set = EqualitySet.from( 39 | const IterableEquality(), [list1, list2, list3, list4, list5, list6]); 40 | 41 | expect(set, contains(same(list1))); 42 | expect(set, contains(same(list2))); 43 | expect(set, isNot(contains(same(list3)))); 44 | expect(set, isNot(contains(same(list4)))); 45 | expect(set, isNot(contains(same(list5)))); 46 | expect(set, isNot(contains(same(list6)))); 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /test/equality_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | // Tests equality utilities. 6 | 7 | import 'dart:collection'; 8 | 9 | import 'package:collection/collection.dart'; 10 | import 'package:test/test.dart'; 11 | 12 | void main() { 13 | Element o(Comparable id) => Element(id); 14 | 15 | // Lists that are point-wise equal, but not identical. 16 | var list1 = [o(1), o(2), o(3), o(4), o(5)]; 17 | var list2 = [o(1), o(2), o(3), o(4), o(5)]; 18 | // Similar length list with equal elements in different order. 19 | var list3 = [o(1), o(3), o(5), o(4), o(2)]; 20 | 21 | test('IterableEquality - List', () { 22 | expect(const IterableEquality().equals(list1, list2), isTrue); 23 | Equality iterId = const IterableEquality(IdentityEquality()); 24 | expect(iterId.equals(list1, list2), isFalse); 25 | }); 26 | 27 | test('IterableEquality - LinkedSet', () { 28 | var l1 = LinkedHashSet.from(list1); 29 | var l2 = LinkedHashSet.from(list2); 30 | expect(const IterableEquality().equals(l1, l2), isTrue); 31 | Equality iterId = const IterableEquality(IdentityEquality()); 32 | expect(iterId.equals(l1, l2), isFalse); 33 | }); 34 | 35 | test('ListEquality', () { 36 | expect(const ListEquality().equals(list1, list2), isTrue); 37 | Equality listId = const ListEquality(IdentityEquality()); 38 | expect(listId.equals(list1, list2), isFalse); 39 | }); 40 | 41 | test('ListInequality length', () { 42 | var list4 = [o(1), o(2), o(3), o(4), o(5), o(6)]; 43 | expect(const ListEquality().equals(list1, list4), isFalse); 44 | expect( 45 | const ListEquality(IdentityEquality()).equals(list1, list4), isFalse); 46 | }); 47 | 48 | test('ListInequality value', () { 49 | var list5 = [o(1), o(2), o(3), o(4), o(6)]; 50 | expect(const ListEquality().equals(list1, list5), isFalse); 51 | expect( 52 | const ListEquality(IdentityEquality()).equals(list1, list5), isFalse); 53 | }); 54 | 55 | test('UnorderedIterableEquality', () { 56 | expect(const UnorderedIterableEquality().equals(list1, list3), isTrue); 57 | Equality uniterId = const UnorderedIterableEquality(IdentityEquality()); 58 | expect(uniterId.equals(list1, list3), isFalse); 59 | }); 60 | 61 | test('UnorderedIterableInequality length', () { 62 | var list6 = [o(1), o(3), o(5), o(4), o(2), o(1)]; 63 | expect(const UnorderedIterableEquality().equals(list1, list6), isFalse); 64 | expect( 65 | const UnorderedIterableEquality(IdentityEquality()) 66 | .equals(list1, list6), 67 | isFalse); 68 | }); 69 | 70 | test('UnorderedIterableInequality values', () { 71 | var list7 = [o(1), o(3), o(5), o(4), o(6)]; 72 | expect(const UnorderedIterableEquality().equals(list1, list7), isFalse); 73 | expect( 74 | const UnorderedIterableEquality(IdentityEquality()) 75 | .equals(list1, list7), 76 | isFalse); 77 | }); 78 | 79 | test('SetEquality', () { 80 | var set1 = HashSet.from(list1); 81 | var set2 = LinkedHashSet.from(list3); 82 | expect(const SetEquality().equals(set1, set2), isTrue); 83 | Equality setId = const SetEquality(IdentityEquality()); 84 | expect(setId.equals(set1, set2), isFalse); 85 | }); 86 | 87 | test('SetInequality length', () { 88 | var list8 = [o(1), o(3), o(5), o(4), o(2), o(6)]; 89 | var set1 = HashSet.from(list1); 90 | var set2 = LinkedHashSet.from(list8); 91 | expect(const SetEquality().equals(set1, set2), isFalse); 92 | expect(const SetEquality(IdentityEquality()).equals(set1, set2), isFalse); 93 | }); 94 | 95 | test('SetInequality value', () { 96 | var list7 = [o(1), o(3), o(5), o(4), o(6)]; 97 | var set1 = HashSet.from(list1); 98 | var set2 = LinkedHashSet.from(list7); 99 | expect(const SetEquality().equals(set1, set2), isFalse); 100 | expect(const SetEquality(IdentityEquality()).equals(set1, set2), isFalse); 101 | }); 102 | 103 | var map1a = { 104 | 'x': [o(1), o(2), o(3)], 105 | 'y': [true, false, null] 106 | }; 107 | var map1b = { 108 | 'x': [o(4), o(5), o(6)], 109 | 'y': [false, true, null] 110 | }; 111 | var map2a = { 112 | 'x': [o(3), o(2), o(1)], 113 | 'y': [false, true, null] 114 | }; 115 | var map2b = { 116 | 'x': [o(6), o(5), o(4)], 117 | 'y': [null, false, true] 118 | }; 119 | var l1 = [map1a, map1b]; 120 | var l2 = [map2a, map2b]; 121 | var s1 = {...l1}; 122 | var s2 = {map2b, map2a}; 123 | 124 | var i1 = Iterable.generate(l1.length, (i) => l1[i]); 125 | 126 | test('RecursiveEquality', () { 127 | const unordered = UnorderedIterableEquality(); 128 | expect(unordered.equals(map1a['x'], map2a['x']), isTrue); 129 | expect(unordered.equals(map1a['y'], map2a['y']), isTrue); 130 | expect(unordered.equals(map1b['x'], map2b['x']), isTrue); 131 | expect(unordered.equals(map1b['y'], map2b['y']), isTrue); 132 | const mapval = MapEquality(values: unordered); 133 | expect(mapval.equals(map1a, map2a), isTrue); 134 | expect(mapval.equals(map1b, map2b), isTrue); 135 | const listmapval = ListEquality(mapval); 136 | expect(listmapval.equals(l1, l2), isTrue); 137 | const setmapval = SetEquality(mapval); 138 | expect(setmapval.equals(s1, s2), isTrue); 139 | }); 140 | 141 | group('DeepEquality', () { 142 | group('unordered', () { 143 | var colleq = const DeepCollectionEquality.unordered(); 144 | 145 | test('with identical collection types', () { 146 | expect(colleq.equals(map1a['x'], map2a['x']), isTrue); 147 | expect(colleq.equals(map1a['y'], map2a['y']), isTrue); 148 | expect(colleq.equals(map1b['x'], map2b['x']), isTrue); 149 | expect(colleq.equals(map1b['y'], map2b['y']), isTrue); 150 | expect(colleq.equals(map1a, map2a), isTrue); 151 | expect(colleq.equals(map1b, map2b), isTrue); 152 | expect(colleq.equals(l1, l2), isTrue); 153 | expect(colleq.equals(s1, s2), isTrue); 154 | }); 155 | 156 | // TODO: https://github.com/dart-lang/collection/issues/208 157 | test('comparing collections and iterables', () { 158 | expect(colleq.equals(l1, i1), isFalse); 159 | expect(colleq.equals(i1, l1), isFalse); 160 | expect(colleq.equals(s1, i1), isFalse); 161 | expect(colleq.equals(i1, s1), isTrue); 162 | }); 163 | }); 164 | 165 | group('ordered', () { 166 | var colleq = const DeepCollectionEquality(); 167 | 168 | test('with identical collection types', () { 169 | expect(colleq.equals(l1, l1.toList()), isTrue); 170 | expect(colleq.equals(s1, s1.toSet()), isTrue); 171 | expect(colleq.equals(map1b, map1b.map(MapEntry.new)), isTrue); 172 | expect(colleq.equals(i1, i1.map((i) => i)), isTrue); 173 | expect(colleq.equals(map1a, map2a), isFalse); 174 | expect(colleq.equals(map1b, map2b), isFalse); 175 | expect(colleq.equals(l1, l2), isFalse); 176 | expect(colleq.equals(s1, s2), isFalse); 177 | }); 178 | 179 | // TODO: https://github.com/dart-lang/collection/issues/208 180 | test('comparing collections and iterables', () { 181 | expect(colleq.equals(l1, i1), isFalse); 182 | expect(colleq.equals(i1, l1), isTrue); 183 | expect(colleq.equals(s1, i1), isFalse); 184 | expect(colleq.equals(i1, s1), isTrue); 185 | }); 186 | }); 187 | }); 188 | 189 | test('CaseInsensitiveEquality', () { 190 | var equality = const CaseInsensitiveEquality(); 191 | expect(equality.equals('foo', 'foo'), isTrue); 192 | expect(equality.equals('fOo', 'FoO'), isTrue); 193 | expect(equality.equals('FoO', 'fOo'), isTrue); 194 | expect(equality.equals('foo', 'bar'), isFalse); 195 | expect(equality.equals('fÕÕ', 'fõõ'), isFalse); 196 | 197 | expect(equality.hash('foo'), equals(equality.hash('foo'))); 198 | expect(equality.hash('fOo'), equals(equality.hash('FoO'))); 199 | expect(equality.hash('FoO'), equals(equality.hash('fOo'))); 200 | expect(equality.hash('foo'), isNot(equals(equality.hash('bar')))); 201 | expect(equality.hash('fÕÕ'), isNot(equals(equality.hash('fõõ')))); 202 | }); 203 | 204 | group('EqualityBy should use a derived value for ', () { 205 | var firstEquality = EqualityBy, String>((e) => e.first); 206 | var firstInsensitiveEquality = EqualityBy, String>( 207 | (e) => e.first, const CaseInsensitiveEquality()); 208 | var firstObjectEquality = EqualityBy, Object>( 209 | (e) => e.first, const IterableEquality()); 210 | 211 | test('equality', () { 212 | expect(firstEquality.equals(['foo', 'foo'], ['foo', 'bar']), isTrue); 213 | expect(firstEquality.equals(['foo', 'foo'], ['bar', 'bar']), isFalse); 214 | }); 215 | 216 | test('equality with an inner equality', () { 217 | expect(firstInsensitiveEquality.equals(['fOo'], ['FoO']), isTrue); 218 | expect(firstInsensitiveEquality.equals(['foo'], ['ffõõ']), isFalse); 219 | }); 220 | 221 | test('hash', () { 222 | expect(firstEquality.hash(['foo', 'bar']), 'foo'.hashCode); 223 | }); 224 | 225 | test('hash with an inner equality', () { 226 | expect(firstInsensitiveEquality.hash(['fOo']), 227 | const CaseInsensitiveEquality().hash('foo')); 228 | }); 229 | 230 | test('isValidKey', () { 231 | expect(firstEquality.isValidKey(['foo']), isTrue); 232 | expect(firstEquality.isValidKey('foo'), isFalse); 233 | expect(firstEquality.isValidKey([1]), isFalse); 234 | }); 235 | 236 | test('isValidKey with an inner equality', () { 237 | expect(firstObjectEquality.isValidKey([[]]), isTrue); 238 | expect(firstObjectEquality.isValidKey([{}]), isFalse); 239 | }); 240 | }); 241 | 242 | test('Equality accepts null', () { 243 | var ie = const IterableEquality(); 244 | var le = const ListEquality(); 245 | var se = const SetEquality(); 246 | var me = const MapEquality(); 247 | expect(ie.equals(null, null), true); 248 | expect(ie.equals([], null), false); 249 | expect(ie.equals(null, []), false); 250 | expect(ie.hash(null), null.hashCode); 251 | 252 | expect(le.equals(null, null), true); 253 | expect(le.equals([], null), false); 254 | expect(le.equals(null, []), false); 255 | expect(le.hash(null), null.hashCode); 256 | 257 | expect(se.equals(null, null), true); 258 | expect(se.equals({}, null), false); 259 | expect(se.equals(null, {}), false); 260 | expect(se.hash(null), null.hashCode); 261 | 262 | expect(me.equals(null, null), true); 263 | expect(me.equals({}, null), false); 264 | expect(me.equals(null, {}), false); 265 | expect(me.hash(null), null.hashCode); 266 | }); 267 | } 268 | 269 | /// Wrapper objects for an `id` value. 270 | /// 271 | /// Compares the `id` value by equality and for comparison. 272 | /// Allows creating simple objects that are equal without being identical. 273 | class Element implements Comparable { 274 | final Comparable id; 275 | const Element(this.id); 276 | @override 277 | int get hashCode => id.hashCode; 278 | @override 279 | bool operator ==(Object other) => other is Element && id == other.id; 280 | @override 281 | int compareTo(Element other) => id.compareTo(other.id); 282 | } 283 | -------------------------------------------------------------------------------- /test/functions_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | // ignore_for_file: deprecated_member_use_from_same_package 6 | 7 | import 'package:collection/collection.dart'; 8 | import 'package:test/test.dart'; 9 | 10 | void main() { 11 | group('mapMap()', () { 12 | test('with an empty map returns an empty map', () { 13 | expect( 14 | mapMap({}, 15 | key: expectAsync2((_, __) {}, count: 0), 16 | value: expectAsync2((_, __) {}, count: 0)), 17 | isEmpty); 18 | }); 19 | 20 | test('with no callbacks, returns a copy of the map', () { 21 | var map = {'foo': 1, 'bar': 2}; 22 | var result = mapMap(map); 23 | expect(result, equals({'foo': 1, 'bar': 2})); 24 | 25 | // The resulting map should be a copy. 26 | result['foo'] = 3; 27 | expect(map, equals({'foo': 1, 'bar': 2})); 28 | }); 29 | 30 | test("maps the map's keys", () { 31 | expect( 32 | mapMap({'foo': 1, 'bar': 2}, 33 | key: (dynamic key, dynamic value) => key[value]), 34 | equals({'o': 1, 'r': 2})); 35 | }); 36 | 37 | test("maps the map's values", () { 38 | expect( 39 | mapMap({'foo': 1, 'bar': 2}, 40 | value: (dynamic key, dynamic value) => key[value]), 41 | equals({'foo': 'o', 'bar': 'r'})); 42 | }); 43 | 44 | test("maps both the map's keys and values", () { 45 | expect( 46 | mapMap({'foo': 1, 'bar': 2}, 47 | key: (dynamic key, dynamic value) => '$key$value', 48 | value: (dynamic key, dynamic value) => key[value]), 49 | equals({'foo1': 'o', 'bar2': 'r'})); 50 | }); 51 | }); 52 | 53 | group('mergeMaps()', () { 54 | test('with empty maps returns an empty map', () { 55 | expect( 56 | mergeMaps({}, {}, 57 | value: expectAsync2((dynamic _, dynamic __) {}, count: 0)), 58 | isEmpty); 59 | }); 60 | 61 | test('returns a map with all values in both input maps', () { 62 | expect(mergeMaps({'foo': 1, 'bar': 2}, {'baz': 3, 'qux': 4}), 63 | equals({'foo': 1, 'bar': 2, 'baz': 3, 'qux': 4})); 64 | }); 65 | 66 | test("the second map's values win by default", () { 67 | expect(mergeMaps({'foo': 1, 'bar': 2}, {'bar': 3, 'baz': 4}), 68 | equals({'foo': 1, 'bar': 3, 'baz': 4})); 69 | }); 70 | 71 | test('uses the callback to merge values', () { 72 | expect( 73 | mergeMaps({'foo': 1, 'bar': 2}, {'bar': 3, 'baz': 4}, 74 | value: (dynamic value1, dynamic value2) => value1 + value2), 75 | equals({'foo': 1, 'bar': 5, 'baz': 4})); 76 | }); 77 | }); 78 | 79 | group('lastBy()', () { 80 | test('returns an empty map for an empty iterable', () { 81 | expect( 82 | lastBy([], (_) => fail('Must not be called for empty input')), 83 | isEmpty, 84 | ); 85 | }); 86 | 87 | test("keeps the latest element for the function's return value", () { 88 | expect( 89 | lastBy(['foo', 'bar', 'baz', 'bop', 'qux'], 90 | (String string) => string[1]), 91 | equals({ 92 | 'o': 'bop', 93 | 'a': 'baz', 94 | 'u': 'qux', 95 | })); 96 | }); 97 | }); 98 | 99 | group('groupBy()', () { 100 | test('returns an empty map for an empty iterable', () { 101 | expect(groupBy([], expectAsync1((dynamic _) {}, count: 0)), isEmpty); 102 | }); 103 | 104 | test("groups elements by the function's return value", () { 105 | expect( 106 | groupBy(['foo', 'bar', 'baz', 'bop', 'qux'], 107 | (dynamic string) => string[1]), 108 | equals({ 109 | 'o': ['foo', 'bop'], 110 | 'a': ['bar', 'baz'], 111 | 'u': ['qux'] 112 | })); 113 | }); 114 | }); 115 | 116 | group('minBy()', () { 117 | test('returns null for an empty iterable', () { 118 | expect( 119 | minBy([], expectAsync1((dynamic _) {}, count: 0), 120 | compare: expectAsync2((dynamic _, dynamic __) => -1, count: 0)), 121 | isNull); 122 | }); 123 | 124 | test( 125 | 'returns the element for which the ordering function returns the ' 126 | 'smallest value', () { 127 | expect( 128 | minBy([ 129 | {'foo': 3}, 130 | {'foo': 5}, 131 | {'foo': 4}, 132 | {'foo': 1}, 133 | {'foo': 2} 134 | ], (dynamic map) => map['foo']), 135 | equals({'foo': 1})); 136 | }); 137 | 138 | test('uses a custom comparator if provided', () { 139 | expect( 140 | minBy, Map>([ 141 | {'foo': 3}, 142 | {'foo': 5}, 143 | {'foo': 4}, 144 | {'foo': 1}, 145 | {'foo': 2} 146 | ], (map) => map, 147 | compare: (map1, map2) => map1['foo']!.compareTo(map2['foo']!)), 148 | equals({'foo': 1})); 149 | }); 150 | }); 151 | 152 | group('maxBy()', () { 153 | test('returns null for an empty iterable', () { 154 | expect( 155 | maxBy([], expectAsync1((dynamic _) {}, count: 0), 156 | compare: expectAsync2((dynamic _, dynamic __) => 0, count: 0)), 157 | isNull); 158 | }); 159 | 160 | test( 161 | 'returns the element for which the ordering function returns the ' 162 | 'largest value', () { 163 | expect( 164 | maxBy([ 165 | {'foo': 3}, 166 | {'foo': 5}, 167 | {'foo': 4}, 168 | {'foo': 1}, 169 | {'foo': 2} 170 | ], (dynamic map) => map['foo']), 171 | equals({'foo': 5})); 172 | }); 173 | 174 | test('uses a custom comparator if provided', () { 175 | expect( 176 | maxBy, Map>([ 177 | {'foo': 3}, 178 | {'foo': 5}, 179 | {'foo': 4}, 180 | {'foo': 1}, 181 | {'foo': 2} 182 | ], (map) => map, 183 | compare: (map1, map2) => map1['foo']!.compareTo(map2['foo']!)), 184 | equals({'foo': 5})); 185 | }); 186 | }); 187 | 188 | group('transitiveClosure()', () { 189 | test('returns an empty map for an empty graph', () { 190 | expect(transitiveClosure({}), isEmpty); 191 | }); 192 | 193 | test('returns the input when there are no transitive connections', () { 194 | expect( 195 | transitiveClosure({ 196 | 'foo': ['bar'], 197 | 'bar': [], 198 | 'bang': ['qux', 'zap'], 199 | 'qux': [], 200 | 'zap': [] 201 | }), 202 | equals({ 203 | 'foo': ['bar'], 204 | 'bar': [], 205 | 'bang': ['qux', 'zap'], 206 | 'qux': [], 207 | 'zap': [] 208 | })); 209 | }); 210 | 211 | test('flattens transitive connections', () { 212 | expect( 213 | transitiveClosure({ 214 | 'qux': [], 215 | 'bar': ['baz'], 216 | 'baz': ['qux'], 217 | 'foo': ['bar'] 218 | }), 219 | equals({ 220 | 'foo': ['bar', 'baz', 'qux'], 221 | 'bar': ['baz', 'qux'], 222 | 'baz': ['qux'], 223 | 'qux': [] 224 | })); 225 | }); 226 | 227 | test('handles loops', () { 228 | expect( 229 | transitiveClosure({ 230 | 'foo': ['bar'], 231 | 'bar': ['baz'], 232 | 'baz': ['foo'] 233 | }), 234 | equals({ 235 | 'foo': ['bar', 'baz', 'foo'], 236 | 'bar': ['baz', 'foo', 'bar'], 237 | 'baz': ['foo', 'bar', 'baz'] 238 | })); 239 | }); 240 | }); 241 | 242 | group('stronglyConnectedComponents()', () { 243 | test('returns an empty list for an empty graph', () { 244 | expect(stronglyConnectedComponents({}), isEmpty); 245 | }); 246 | 247 | test('returns one set for a singleton graph', () { 248 | expect( 249 | stronglyConnectedComponents({'a': []}), 250 | equals([ 251 | {'a'} 252 | ])); 253 | }); 254 | 255 | test('returns two sets for a two-element tree', () { 256 | expect( 257 | stronglyConnectedComponents({ 258 | 'a': ['b'], 259 | 'b': [] 260 | }), 261 | equals([ 262 | {'a'}, 263 | {'b'} 264 | ])); 265 | }); 266 | 267 | test('returns one set for a two-element loop', () { 268 | expect( 269 | stronglyConnectedComponents({ 270 | 'a': ['b'], 271 | 'b': ['a'] 272 | }), 273 | equals([ 274 | {'a', 'b'} 275 | ])); 276 | }); 277 | 278 | test('returns individual vertices for a tree', () { 279 | expect( 280 | stronglyConnectedComponents({ 281 | 'foo': ['bar'], 282 | 'bar': ['baz', 'bang'], 283 | 'baz': ['qux'], 284 | 'bang': ['zap'], 285 | 'qux': [], 286 | 'zap': [] 287 | }), 288 | equals([ 289 | // This is expected to return *a* topological ordering, but this isn't 290 | // the only valid one. If the function implementation changes in the 291 | // future, this test may need to be updated. 292 | {'foo'}, 293 | {'bar'}, 294 | {'bang'}, 295 | {'zap'}, 296 | {'baz'}, 297 | {'qux'} 298 | ]), 299 | ); 300 | }); 301 | 302 | test('returns a single set for a fully cyclic graph', () { 303 | expect( 304 | stronglyConnectedComponents({ 305 | 'foo': ['bar'], 306 | 'bar': ['baz'], 307 | 'baz': ['bang'], 308 | 'bang': ['foo'] 309 | }), 310 | equals([ 311 | {'foo', 'bar', 'baz', 'bang'} 312 | ])); 313 | }); 314 | 315 | test('returns separate sets for each strongly connected component', () { 316 | // https://en.wikipedia.org/wiki/Strongly_connected_component#/media/File:Scc.png 317 | expect( 318 | stronglyConnectedComponents({ 319 | 'a': ['b'], 320 | 'b': ['c', 'e', 'f'], 321 | 'c': ['d', 'g'], 322 | 'd': ['c', 'h'], 323 | 'e': ['a', 'f'], 324 | 'f': ['g'], 325 | 'g': ['f'], 326 | 'h': ['g', 'd'] 327 | }), 328 | equals([ 329 | // This is expected to return *a* topological ordering, but this isn't 330 | // the only valid one. If the function implementation changes in the 331 | // future, this test may need to be updated. 332 | {'a', 'b', 'e'}, 333 | {'c', 'd', 'h'}, 334 | {'f', 'g'}, 335 | ]), 336 | ); 337 | }); 338 | 339 | test('always returns components in topological order', () { 340 | expect( 341 | stronglyConnectedComponents({ 342 | 'bar': ['baz', 'bang'], 343 | 'zap': [], 344 | 'baz': ['qux'], 345 | 'qux': [], 346 | 'foo': ['bar'], 347 | 'bang': ['zap'] 348 | }), 349 | equals([ 350 | // This is expected to return *a* topological ordering, but this isn't 351 | // the only valid one. If the function implementation changes in the 352 | // future, this test may need to be updated. 353 | {'foo'}, 354 | {'bar'}, 355 | {'bang'}, 356 | {'zap'}, 357 | {'baz'}, 358 | {'qux'} 359 | ]), 360 | ); 361 | }); 362 | }); 363 | } 364 | -------------------------------------------------------------------------------- /test/ignore_ascii_case_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | /// Tests case-ignoring compare and equality. 6 | library; 7 | 8 | import 'package:collection/collection.dart'; 9 | import 'package:test/test.dart'; 10 | 11 | void main() { 12 | test('equality ignore ASCII case', () { 13 | var strings = [ 14 | '0@`aopz[{', 15 | '0@`aopz[{', 16 | '0@`Aopz[{', 17 | '0@`aOpz[{', 18 | '0@`AOpz[{', 19 | '0@`aoPz[{', 20 | '0@`AoPz[{', 21 | '0@`aOPz[{', 22 | '0@`AOPz[{', 23 | '0@`aopZ[{', 24 | '0@`AopZ[{', 25 | '0@`aOpZ[{', 26 | '0@`AOpZ[{', 27 | '0@`aoPZ[{', 28 | '0@`AoPZ[{', 29 | '0@`aOPZ[{', 30 | '0@`AOPZ[{', 31 | ]; 32 | 33 | for (var s1 in strings) { 34 | for (var s2 in strings) { 35 | var reason = '$s1 =?= $s2'; 36 | expect(equalsIgnoreAsciiCase(s1, s2), true, reason: reason); 37 | expect(hashIgnoreAsciiCase(s1), hashIgnoreAsciiCase(s2), 38 | reason: reason); 39 | } 40 | } 41 | 42 | var upperCaseLetters = '@`abcdefghijklmnopqrstuvwxyz[{åÅ'; 43 | var lowerCaseLetters = '@`ABCDEFGHIJKLMNOPQRSTUVWXYZ[{åÅ'; 44 | expect(equalsIgnoreAsciiCase(upperCaseLetters, lowerCaseLetters), true); 45 | 46 | void testChars(String char1, String char2, bool areEqual) { 47 | expect(equalsIgnoreAsciiCase(char1, char2), areEqual, 48 | reason: "$char1 ${areEqual ? "=" : "!"}= $char2"); 49 | } 50 | 51 | for (var i = 0; i < upperCaseLetters.length; i++) { 52 | for (var j = 0; i < upperCaseLetters.length; i++) { 53 | testChars(upperCaseLetters[i], upperCaseLetters[j], i == j); 54 | testChars(lowerCaseLetters[i], upperCaseLetters[j], i == j); 55 | testChars(upperCaseLetters[i], lowerCaseLetters[j], i == j); 56 | testChars(lowerCaseLetters[i], lowerCaseLetters[j], i == j); 57 | } 58 | } 59 | }); 60 | } 61 | -------------------------------------------------------------------------------- /test/iterable_zip_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'package:collection/collection.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | /// Iterable like [base] except that it throws when value equals [errorValue]. 9 | Iterable iterError(Iterable base, int errorValue) { 10 | // ignore: only_throw_errors 11 | return base.map((x) => x == errorValue ? throw 'BAD' : x); 12 | } 13 | 14 | void main() { 15 | test('Basic', () { 16 | expect( 17 | IterableZip([ 18 | [1, 2, 3], 19 | [4, 5, 6], 20 | [7, 8, 9] 21 | ]), 22 | equals([ 23 | [1, 4, 7], 24 | [2, 5, 8], 25 | [3, 6, 9] 26 | ])); 27 | }); 28 | 29 | test('Uneven length 1', () { 30 | expect( 31 | IterableZip([ 32 | [1, 2, 3, 99, 100], 33 | [4, 5, 6], 34 | [7, 8, 9] 35 | ]), 36 | equals([ 37 | [1, 4, 7], 38 | [2, 5, 8], 39 | [3, 6, 9] 40 | ])); 41 | }); 42 | 43 | test('Uneven length 2', () { 44 | expect( 45 | IterableZip([ 46 | [1, 2, 3], 47 | [4, 5, 6, 99, 100], 48 | [7, 8, 9] 49 | ]), 50 | equals([ 51 | [1, 4, 7], 52 | [2, 5, 8], 53 | [3, 6, 9] 54 | ])); 55 | }); 56 | 57 | test('Uneven length 3', () { 58 | expect( 59 | IterableZip([ 60 | [1, 2, 3], 61 | [4, 5, 6], 62 | [7, 8, 9, 99, 100] 63 | ]), 64 | equals([ 65 | [1, 4, 7], 66 | [2, 5, 8], 67 | [3, 6, 9] 68 | ])); 69 | }); 70 | 71 | test('Uneven length 3', () { 72 | expect( 73 | IterableZip([ 74 | [1, 2, 3, 98], 75 | [4, 5, 6], 76 | [7, 8, 9, 99, 100] 77 | ]), 78 | equals([ 79 | [1, 4, 7], 80 | [2, 5, 8], 81 | [3, 6, 9] 82 | ])); 83 | }); 84 | 85 | test('Empty 1', () { 86 | expect( 87 | IterableZip([ 88 | [], 89 | [4, 5, 6], 90 | [7, 8, 9] 91 | ]), 92 | equals([])); 93 | }); 94 | 95 | test('Empty 2', () { 96 | expect( 97 | IterableZip([ 98 | [1, 2, 3], 99 | [], 100 | [7, 8, 9] 101 | ]), 102 | equals([])); 103 | }); 104 | 105 | test('Empty 3', () { 106 | expect( 107 | IterableZip([ 108 | [1, 2, 3], 109 | [4, 5, 6], 110 | [] 111 | ]), 112 | equals([])); 113 | }); 114 | 115 | test('Empty source', () { 116 | expect(IterableZip([]), equals([])); 117 | }); 118 | 119 | test('Single Source', () { 120 | expect( 121 | IterableZip([ 122 | [1, 2, 3] 123 | ]), 124 | equals([ 125 | [1], 126 | [2], 127 | [3] 128 | ])); 129 | }); 130 | 131 | test('Not-lists', () { 132 | // Use other iterables than list literals. 133 | var it1 = [1, 2, 3, 4, 5, 6].where((x) => x < 4); 134 | var it2 = {4, 5, 6}; 135 | var it3 = {7: 0, 8: 0, 9: 0}.keys; 136 | var allIts = Iterable.generate(3, (i) => [it1, it2, it3][i]); 137 | expect( 138 | IterableZip(allIts), 139 | equals([ 140 | [1, 4, 7], 141 | [2, 5, 8], 142 | [3, 6, 9] 143 | ])); 144 | }); 145 | 146 | test('Error 1', () { 147 | expect( 148 | () => IterableZip([ 149 | iterError([1, 2, 3], 2), 150 | [4, 5, 6], 151 | [7, 8, 9] 152 | ]).toList(), 153 | throwsA(equals('BAD'))); 154 | }); 155 | 156 | test('Error 2', () { 157 | expect( 158 | () => IterableZip([ 159 | [1, 2, 3], 160 | iterError([4, 5, 6], 5), 161 | [7, 8, 9] 162 | ]).toList(), 163 | throwsA(equals('BAD'))); 164 | }); 165 | 166 | test('Error 3', () { 167 | expect( 168 | () => IterableZip([ 169 | [1, 2, 3], 170 | [4, 5, 6], 171 | iterError([7, 8, 9], 8) 172 | ]).toList(), 173 | throwsA(equals('BAD'))); 174 | }); 175 | 176 | test('Error at end', () { 177 | expect( 178 | () => IterableZip([ 179 | [1, 2, 3], 180 | iterError([4, 5, 6], 6), 181 | [7, 8, 9] 182 | ]).toList(), 183 | throwsA(equals('BAD'))); 184 | }); 185 | 186 | test('Error before first end', () { 187 | expect( 188 | () => IterableZip([ 189 | iterError([1, 2, 3, 4], 4), 190 | [4, 5, 6], 191 | [7, 8, 9] 192 | ]).toList(), 193 | throwsA(equals('BAD'))); 194 | }); 195 | 196 | test('Error after first end', () { 197 | expect( 198 | IterableZip([ 199 | [1, 2, 3], 200 | [4, 5, 6], 201 | iterError([7, 8, 9, 10], 10) 202 | ]), 203 | equals([ 204 | [1, 4, 7], 205 | [2, 5, 8], 206 | [3, 6, 9] 207 | ])); 208 | }); 209 | } 210 | -------------------------------------------------------------------------------- /test/priority_queue_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | /// Tests priority queue implementations utilities. 6 | library; 7 | 8 | import 'package:collection/src/priority_queue.dart'; 9 | import 'package:test/test.dart'; 10 | 11 | void main() { 12 | testDefault(); 13 | testInt(HeapPriorityQueue.new); 14 | testCustom(HeapPriorityQueue.new); 15 | testDuplicates(); 16 | testNullable(); 17 | testConcurrentModification(); 18 | } 19 | 20 | void testDefault() { 21 | test('PriorityQueue() returns a HeapPriorityQueue', () { 22 | expect(PriorityQueue(), const TypeMatcher>()); 23 | }); 24 | testInt(PriorityQueue.new); 25 | testCustom(PriorityQueue.new); 26 | } 27 | 28 | void testInt(PriorityQueue Function() create) { 29 | for (var count in [1, 5, 127, 128]) { 30 | testQueue('int:$count', create, List.generate(count, (x) => x), count); 31 | } 32 | } 33 | 34 | void testCustom( 35 | PriorityQueue Function(int Function(C, C)? comparator) create) { 36 | for (var count in [1, 5, 127, 128]) { 37 | testQueue('Custom:$count/null', () => create(null), 38 | List.generate(count, C.new), C(count)); 39 | testQueue('Custom:$count/compare', () => create(compare), 40 | List.generate(count, C.new), C(count)); 41 | testQueue('Custom:$count/compareNeg', () => create(compareNeg), 42 | List.generate(count, (x) => C(count - x)), const C(0)); 43 | } 44 | } 45 | 46 | /// Test that a queue behaves correctly. 47 | /// 48 | /// The elements must be in priority order, from highest to lowest. 49 | void testQueue( 50 | String name, 51 | PriorityQueue Function() create, 52 | List elements, 53 | T notElement, 54 | ) { 55 | test(name, () => testQueueBody(create, elements, notElement)); 56 | } 57 | 58 | void testQueueBody( 59 | PriorityQueue Function() create, List elements, T notElement) { 60 | var q = create(); 61 | expect(q.isEmpty, isTrue); 62 | expect(q, hasLength(0)); 63 | expect(() { 64 | q.first; 65 | }, throwsStateError); 66 | expect(() { 67 | q.removeFirst(); 68 | }, throwsStateError); 69 | 70 | // Tests removeFirst, first, contains, toList and toSet. 71 | void testElements() { 72 | expect(q.isNotEmpty, isTrue); 73 | expect(q, hasLength(elements.length)); 74 | 75 | expect(q.toList(), equals(elements)); 76 | expect(q.toSet().toList(), equals(elements)); 77 | expect(q.toUnorderedList(), unorderedEquals(elements)); 78 | expect(q.unorderedElements, unorderedEquals(elements)); 79 | 80 | var allElements = q.removeAll(); 81 | q.addAll(allElements); 82 | 83 | for (var i = 0; i < elements.length; i++) { 84 | expect(q.contains(elements[i]), isTrue); 85 | } 86 | expect(q.contains(notElement), isFalse); 87 | 88 | var all = []; 89 | while (q.isNotEmpty) { 90 | var expected = q.first; 91 | var actual = q.removeFirst(); 92 | expect(actual, same(expected)); 93 | all.add(actual); 94 | } 95 | 96 | expect(all.length, elements.length); 97 | for (var i = 0; i < all.length; i++) { 98 | expect(all[i], same(elements[i])); 99 | } 100 | 101 | expect(q.isEmpty, isTrue); 102 | } 103 | 104 | q.addAll(elements); 105 | testElements(); 106 | 107 | q.addAll(elements.reversed); 108 | testElements(); 109 | 110 | // Add elements in a non-linear order (gray order). 111 | for (var i = 0, j = 0; i < elements.length; i++) { 112 | int gray; 113 | do { 114 | gray = j ^ (j >> 1); 115 | j++; 116 | } while (gray >= elements.length); 117 | q.add(elements[gray]); 118 | } 119 | testElements(); 120 | 121 | // Add elements by picking the middle element first, and then recursing 122 | // on each side. 123 | void addRec(int min, int max) { 124 | var mid = min + ((max - min) >> 1); 125 | q.add(elements[mid]); 126 | if (mid + 1 < max) addRec(mid + 1, max); 127 | if (mid > min) addRec(min, mid); 128 | } 129 | 130 | addRec(0, elements.length); 131 | testElements(); 132 | 133 | // Test removeAll. 134 | q.addAll(elements); 135 | expect(q, hasLength(elements.length)); 136 | var all = q.removeAll(); 137 | expect(q.isEmpty, isTrue); 138 | expect(all, hasLength(elements.length)); 139 | for (var i = 0; i < elements.length; i++) { 140 | expect(all, contains(elements[i])); 141 | } 142 | 143 | // Test the same element more than once in queue. 144 | q.addAll(elements); 145 | q.addAll(elements.reversed); 146 | expect(q, hasLength(elements.length * 2)); 147 | for (var i = 0; i < elements.length; i++) { 148 | var element = elements[i]; 149 | expect(q.contains(element), isTrue); 150 | expect(q.removeFirst(), element); 151 | expect(q.removeFirst(), element); 152 | } 153 | 154 | // Test queue with all same element. 155 | var a = elements[0]; 156 | for (var i = 0; i < elements.length; i++) { 157 | q.add(a); 158 | } 159 | expect(q, hasLength(elements.length)); 160 | expect(q.contains(a), isTrue); 161 | expect(q.contains(notElement), isFalse); 162 | q.removeAll().forEach((x) => expect(x, same(a))); 163 | 164 | // Test remove. 165 | q.addAll(elements); 166 | for (var element in elements.reversed) { 167 | expect(q.remove(element), isTrue); 168 | } 169 | expect(q.isEmpty, isTrue); 170 | } 171 | 172 | void testDuplicates() { 173 | // Check how the heap handles duplicate, or equal-but-not-identical, values. 174 | test('duplicates', () { 175 | var q = HeapPriorityQueue(compare); 176 | var c1 = const C(0); 177 | // ignore: prefer_const_constructors 178 | var c2 = C(0); 179 | 180 | // Can contain the same element more than once. 181 | expect(c1, equals(c2)); 182 | expect(c1, isNot(same(c2))); 183 | q.add(c1); 184 | q.add(c1); 185 | expect(q.length, 2); 186 | expect(q.contains(c1), true); 187 | expect(q.contains(c2), true); 188 | expect(q.remove(c2), true); 189 | expect(q.length, 1); 190 | expect(q.removeFirst(), same(c1)); 191 | 192 | // Can contain equal elements. 193 | q.add(c1); 194 | q.add(c2); 195 | expect(q.length, 2); 196 | expect(q.contains(c1), true); 197 | expect(q.contains(c2), true); 198 | expect(q.remove(c1), true); 199 | expect(q.length, 1); 200 | expect(q.first, anyOf(same(c1), same(c2))); 201 | }); 202 | } 203 | 204 | void testNullable() { 205 | // Check that the queue works with a nullable type, and a comparator 206 | // which accepts `null`. 207 | // Compares `null` before instances of `C`. 208 | int nullCompareFirst(C? a, C? b) => a == null 209 | ? b == null 210 | ? 0 211 | : -1 212 | : b == null 213 | ? 1 214 | : compare(a, b); 215 | 216 | int nullCompareLast(C? a, C? b) => a == null 217 | ? b == null 218 | ? 0 219 | : 1 220 | : b == null 221 | ? -1 222 | : compare(a, b); 223 | 224 | var c1 = const C(1); 225 | var c2 = const C(2); 226 | var c3 = const C(3); 227 | 228 | test('nulls first', () { 229 | var q = HeapPriorityQueue(nullCompareFirst); 230 | q.add(c2); 231 | q.add(c1); 232 | q.add(null); 233 | expect(q.length, 3); 234 | expect(q.contains(null), true); 235 | expect(q.contains(c1), true); 236 | expect(q.contains(c3), false); 237 | 238 | expect(q.removeFirst(), null); 239 | expect(q.length, 2); 240 | expect(q.contains(null), false); 241 | q.add(null); 242 | expect(q.length, 3); 243 | expect(q.contains(null), true); 244 | q.add(null); 245 | expect(q.length, 4); 246 | expect(q.contains(null), true); 247 | expect(q.remove(null), true); 248 | expect(q.length, 3); 249 | expect(q.toList(), [null, c1, c2]); 250 | }); 251 | 252 | test('nulls last', () { 253 | var q = HeapPriorityQueue(nullCompareLast); 254 | q.add(c2); 255 | q.add(c1); 256 | q.add(null); 257 | expect(q.length, 3); 258 | expect(q.contains(null), true); 259 | expect(q.contains(c1), true); 260 | expect(q.contains(c3), false); 261 | expect(q.first, c1); 262 | 263 | q.add(null); 264 | expect(q.length, 4); 265 | expect(q.contains(null), true); 266 | q.add(null); 267 | expect(q.length, 5); 268 | expect(q.contains(null), true); 269 | expect(q.remove(null), true); 270 | expect(q.length, 4); 271 | expect(q.toList(), [c1, c2, null, null]); 272 | }); 273 | } 274 | 275 | void testConcurrentModification() { 276 | group('concurrent modification for', () { 277 | test('add', () { 278 | var q = HeapPriorityQueue((a, b) => a - b) 279 | ..addAll([6, 4, 2, 3, 5, 8]); 280 | var e = q.unorderedElements; 281 | q.add(12); // Modifiation before creating iterator is not a problem. 282 | var it = e.iterator; 283 | q.add(7); // Modification after creatig iterator is a problem. 284 | expect(it.moveNext, throwsConcurrentModificationError); 285 | 286 | it = e.iterator; // New iterator is not affected. 287 | expect(it.moveNext(), true); 288 | expect(it.moveNext(), true); 289 | q.add(9); // Modification during iteration is a problem. 290 | expect(it.moveNext, throwsConcurrentModificationError); 291 | }); 292 | 293 | test('addAll', () { 294 | var q = HeapPriorityQueue((a, b) => a - b) 295 | ..addAll([6, 4, 2, 3, 5, 8]); 296 | var e = q.unorderedElements; 297 | q.addAll([12]); // Modifiation before creating iterator is not a problem. 298 | var it = e.iterator; 299 | q.addAll([7]); // Modification after creatig iterator is a problem. 300 | expect(it.moveNext, throwsConcurrentModificationError); 301 | it = e.iterator; // New iterator is not affected. 302 | expect(it.moveNext(), true); 303 | q.addAll([]); // Adding nothing is not a modification. 304 | expect(it.moveNext(), true); 305 | q.addAll([9]); // Modification during iteration is a problem. 306 | expect(it.moveNext, throwsConcurrentModificationError); 307 | }); 308 | 309 | test('removeFirst', () { 310 | var q = HeapPriorityQueue((a, b) => a - b) 311 | ..addAll([6, 4, 2, 3, 5, 8]); 312 | var e = q.unorderedElements; 313 | expect(q.removeFirst(), 314 | 2); // Modifiation before creating iterator is not a problem. 315 | var it = e.iterator; 316 | expect(q.removeFirst(), 317 | 3); // Modification after creatig iterator is a problem. 318 | expect(it.moveNext, throwsConcurrentModificationError); 319 | 320 | it = e.iterator; // New iterator is not affected. 321 | expect(it.moveNext(), true); 322 | expect(it.moveNext(), true); 323 | expect(q.removeFirst(), 4); // Modification during iteration is a problem. 324 | expect(it.moveNext, throwsConcurrentModificationError); 325 | }); 326 | 327 | test('remove', () { 328 | var q = HeapPriorityQueue((a, b) => a - b) 329 | ..addAll([6, 4, 2, 3, 5, 8]); 330 | var e = q.unorderedElements; 331 | expect(q.remove(3), true); 332 | var it = e.iterator; 333 | expect(q.remove(2), true); 334 | expect(it.moveNext, throwsConcurrentModificationError); 335 | it = e.iterator; 336 | expect(q.remove(99), false); 337 | expect(it.moveNext(), true); 338 | expect(it.moveNext(), true); 339 | expect(q.remove(5), true); 340 | expect(it.moveNext, throwsConcurrentModificationError); 341 | }); 342 | 343 | test('removeAll', () { 344 | var q = HeapPriorityQueue((a, b) => a - b) 345 | ..addAll([6, 4, 2, 3, 5, 8]); 346 | var e = q.unorderedElements; 347 | var it = e.iterator; 348 | expect(it.moveNext(), true); 349 | expect(it.moveNext(), true); 350 | expect(q.removeAll(), hasLength(6)); 351 | expect(it.moveNext, throwsConcurrentModificationError); 352 | }); 353 | 354 | test('clear', () { 355 | var q = HeapPriorityQueue((a, b) => a - b) 356 | ..addAll([6, 4, 2, 3, 5, 8]); 357 | var e = q.unorderedElements; 358 | var it = e.iterator; 359 | expect(it.moveNext(), true); 360 | expect(it.moveNext(), true); 361 | q.clear(); 362 | expect(it.moveNext, throwsConcurrentModificationError); 363 | }); 364 | }); 365 | } 366 | 367 | // Custom class. 368 | // Class is comparable, comparators match normal and inverse order. 369 | int compare(C c1, C c2) => c1.value - c2.value; 370 | int compareNeg(C c1, C c2) => c2.value - c1.value; 371 | 372 | class C implements Comparable { 373 | final int value; 374 | const C(this.value); 375 | @override 376 | int get hashCode => value; 377 | @override 378 | bool operator ==(Object other) => other is C && value == other.value; 379 | @override 380 | int compareTo(C other) => value - other.value; 381 | @override 382 | String toString() => 'C($value)'; 383 | } 384 | -------------------------------------------------------------------------------- /test/queue_list_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'package:collection/collection.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | void main() { 9 | group('QueueList()', () { 10 | test('creates an empty QueueList', () { 11 | expect(QueueList(), isEmpty); 12 | }); 13 | 14 | test('takes an initial capacity', () { 15 | expect(QueueList(100), isEmpty); 16 | }); 17 | }); 18 | 19 | test('QueueList.from() copies the contents of an iterable', () { 20 | expect(QueueList.from([1, 2, 3].skip(1)), equals([2, 3])); 21 | }); 22 | 23 | group('add()', () { 24 | test('adds an element to the end of the queue', () { 25 | var queue = QueueList.from([1, 2, 3]); 26 | queue.add(4); 27 | expect(queue, equals([1, 2, 3, 4])); 28 | }); 29 | 30 | test('expands a full queue', () { 31 | var queue = atCapacity(); 32 | queue.add(8); 33 | expect(queue, equals([1, 2, 3, 4, 5, 6, 7, 8])); 34 | }); 35 | }); 36 | 37 | group('addAll()', () { 38 | test('adds elements to the end of the queue', () { 39 | var queue = QueueList.from([1, 2, 3]); 40 | queue.addAll([4, 5, 6]); 41 | expect(queue, equals([1, 2, 3, 4, 5, 6])); 42 | }); 43 | 44 | test('expands a full queue', () { 45 | var queue = atCapacity(); 46 | queue.addAll([8, 9]); 47 | expect(queue, equals([1, 2, 3, 4, 5, 6, 7, 8, 9])); 48 | }); 49 | }); 50 | 51 | group('addFirst()', () { 52 | test('adds an element to the beginning of the queue', () { 53 | var queue = QueueList.from([1, 2, 3]); 54 | queue.addFirst(0); 55 | expect(queue, equals([0, 1, 2, 3])); 56 | }); 57 | 58 | test('expands a full queue', () { 59 | var queue = atCapacity(); 60 | queue.addFirst(0); 61 | expect(queue, equals([0, 1, 2, 3, 4, 5, 6, 7])); 62 | }); 63 | }); 64 | 65 | group('removeFirst()', () { 66 | test('removes an element from the beginning of the queue', () { 67 | var queue = QueueList.from([1, 2, 3]); 68 | expect(queue.removeFirst(), equals(1)); 69 | expect(queue, equals([2, 3])); 70 | }); 71 | 72 | test( 73 | 'removes an element from the beginning of a queue with an internal ' 74 | 'gap', () { 75 | var queue = withInternalGap(); 76 | expect(queue.removeFirst(), equals(1)); 77 | expect(queue, equals([2, 3, 4, 5, 6, 7])); 78 | }); 79 | 80 | test('removes an element from the beginning of a queue at capacity', () { 81 | var queue = atCapacity(); 82 | expect(queue.removeFirst(), equals(1)); 83 | expect(queue, equals([2, 3, 4, 5, 6, 7])); 84 | }); 85 | 86 | test('throws a StateError for an empty queue', () { 87 | expect(QueueList().removeFirst, throwsStateError); 88 | }); 89 | }); 90 | 91 | group('removeLast()', () { 92 | test('removes an element from the end of the queue', () { 93 | var queue = QueueList.from([1, 2, 3]); 94 | expect(queue.removeLast(), equals(3)); 95 | expect(queue, equals([1, 2])); 96 | }); 97 | 98 | test('removes an element from the end of a queue with an internal gap', () { 99 | var queue = withInternalGap(); 100 | expect(queue.removeLast(), equals(7)); 101 | expect(queue, equals([1, 2, 3, 4, 5, 6])); 102 | }); 103 | 104 | test('removes an element from the end of a queue at capacity', () { 105 | var queue = atCapacity(); 106 | expect(queue.removeLast(), equals(7)); 107 | expect(queue, equals([1, 2, 3, 4, 5, 6])); 108 | }); 109 | 110 | test('throws a StateError for an empty queue', () { 111 | expect(QueueList().removeLast, throwsStateError); 112 | }); 113 | }); 114 | 115 | group('length', () { 116 | test('returns the length of a queue', () { 117 | expect(QueueList.from([1, 2, 3]).length, equals(3)); 118 | }); 119 | 120 | test('returns the length of a queue with an internal gap', () { 121 | expect(withInternalGap().length, equals(7)); 122 | }); 123 | 124 | test('returns the length of a queue at capacity', () { 125 | expect(atCapacity().length, equals(7)); 126 | }); 127 | }); 128 | 129 | group('length=', () { 130 | test('shrinks a larger queue', () { 131 | var queue = QueueList.from([1, 2, 3]); 132 | queue.length = 1; 133 | expect(queue, equals([1])); 134 | }); 135 | 136 | test('grows a smaller queue', () { 137 | var queue = QueueList.from([1, 2, 3]); 138 | queue.length = 5; 139 | expect(queue, equals([1, 2, 3, null, null])); 140 | }); 141 | 142 | test('throws a RangeError if length is less than 0', () { 143 | expect(() => QueueList().length = -1, throwsRangeError); 144 | }); 145 | 146 | test('throws an UnsupportedError if element type is non-nullable', () { 147 | expect(() => QueueList().length = 1, throwsUnsupportedError); 148 | }); 149 | }); 150 | 151 | group('[]', () { 152 | test('returns individual entries in the queue', () { 153 | var queue = QueueList.from([1, 2, 3]); 154 | expect(queue[0], equals(1)); 155 | expect(queue[1], equals(2)); 156 | expect(queue[2], equals(3)); 157 | }); 158 | 159 | test('returns individual entries in a queue with an internal gap', () { 160 | var queue = withInternalGap(); 161 | expect(queue[0], equals(1)); 162 | expect(queue[1], equals(2)); 163 | expect(queue[2], equals(3)); 164 | expect(queue[3], equals(4)); 165 | expect(queue[4], equals(5)); 166 | expect(queue[5], equals(6)); 167 | expect(queue[6], equals(7)); 168 | }); 169 | 170 | test('throws a RangeError if the index is less than 0', () { 171 | var queue = QueueList.from([1, 2, 3]); 172 | expect(() => queue[-1], throwsRangeError); 173 | }); 174 | 175 | test( 176 | 'throws a RangeError if the index is greater than or equal to the ' 177 | 'length', () { 178 | var queue = QueueList.from([1, 2, 3]); 179 | expect(() => queue[3], throwsRangeError); 180 | }); 181 | }); 182 | 183 | group('[]=', () { 184 | test('sets individual entries in the queue', () { 185 | var queue = QueueList.from([1, 2, 3]); 186 | queue[0] = 'a'; 187 | queue[1] = 'b'; 188 | queue[2] = 'c'; 189 | expect(queue, equals(['a', 'b', 'c'])); 190 | }); 191 | 192 | test('sets individual entries in a queue with an internal gap', () { 193 | var queue = withInternalGap(); 194 | queue[0] = 'a'; 195 | queue[1] = 'b'; 196 | queue[2] = 'c'; 197 | queue[3] = 'd'; 198 | queue[4] = 'e'; 199 | queue[5] = 'f'; 200 | queue[6] = 'g'; 201 | expect(queue, equals(['a', 'b', 'c', 'd', 'e', 'f', 'g'])); 202 | }); 203 | 204 | test('throws a RangeError if the index is less than 0', () { 205 | var queue = QueueList.from([1, 2, 3]); 206 | expect(() { 207 | queue[-1] = 0; 208 | }, throwsRangeError); 209 | }); 210 | 211 | test( 212 | 'throws a RangeError if the index is greater than or equal to the ' 213 | 'length', () { 214 | var queue = QueueList.from([1, 2, 3]); 215 | expect(() { 216 | queue[3] = 4; 217 | }, throwsRangeError); 218 | }); 219 | }); 220 | 221 | group('throws a modification error for', () { 222 | dynamic queue; 223 | setUp(() { 224 | queue = QueueList.from([1, 2, 3]); 225 | }); 226 | 227 | test('add', () { 228 | expect(() => queue.forEach((_) => queue.add(4)), 229 | throwsConcurrentModificationError); 230 | }); 231 | 232 | test('addAll', () { 233 | expect(() => queue.forEach((_) => queue.addAll([4, 5, 6])), 234 | throwsConcurrentModificationError); 235 | }); 236 | 237 | test('addFirst', () { 238 | expect(() => queue.forEach((_) => queue.addFirst(0)), 239 | throwsConcurrentModificationError); 240 | }); 241 | 242 | test('removeFirst', () { 243 | expect(() => queue.forEach((_) => queue.removeFirst()), 244 | throwsConcurrentModificationError); 245 | }); 246 | 247 | test('removeLast', () { 248 | expect(() => queue.forEach((_) => queue.removeLast()), 249 | throwsConcurrentModificationError); 250 | }); 251 | 252 | test('length=', () { 253 | expect(() => queue.forEach((_) => queue.length = 1), 254 | throwsConcurrentModificationError); 255 | }); 256 | }); 257 | 258 | test('cast does not throw on mutation when the type is valid', () { 259 | var patternQueue = QueueList()..addAll(['a', 'b']); 260 | var stringQueue = patternQueue.cast(); 261 | stringQueue.addAll(['c', 'd']); 262 | expect(stringQueue, const TypeMatcher>(), 263 | reason: 'Expected QueueList, got ${stringQueue.runtimeType}'); 264 | 265 | expect(stringQueue, ['a', 'b', 'c', 'd']); 266 | 267 | expect(patternQueue, stringQueue, reason: 'Should forward to original'); 268 | }); 269 | 270 | test('cast throws on mutation when the type is not valid', () { 271 | QueueList stringQueue = QueueList(); 272 | var numQueue = stringQueue.cast(); 273 | expect(numQueue, const TypeMatcher>(), 274 | reason: 'Expected QueueList, got ${numQueue.runtimeType}'); 275 | expect(() => numQueue.add(1), throwsA(isA())); 276 | }); 277 | 278 | test('cast returns a new QueueList', () { 279 | var queue = QueueList(); 280 | expect(queue.cast(), isNot(same(queue))); 281 | }); 282 | } 283 | 284 | /// Returns a queue whose internal ring buffer is full enough that adding a new 285 | /// element will expand it. 286 | QueueList atCapacity() { 287 | // Use addAll because `QueueList.from(list)` won't use the default initial 288 | // capacity of 8. 289 | return QueueList()..addAll([1, 2, 3, 4, 5, 6, 7]); 290 | } 291 | 292 | /// Returns a queue whose internal tail has a lower index than its head. 293 | QueueList withInternalGap() { 294 | var queue = QueueList.from([null, null, null, null, 1, 2, 3, 4]); 295 | for (var i = 0; i < 4; i++) { 296 | queue.removeFirst(); 297 | } 298 | for (var i = 5; i < 8; i++) { 299 | queue.addLast(i); 300 | } 301 | return queue; 302 | } 303 | 304 | /// Returns a matcher that expects that a closure throws a 305 | /// [ConcurrentModificationError]. 306 | final throwsConcurrentModificationError = 307 | throwsA(const TypeMatcher()); 308 | -------------------------------------------------------------------------------- /test/union_set_controller_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'package:collection/collection.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | void main() { 9 | late UnionSetController controller; 10 | late Set innerSet; 11 | setUp(() { 12 | innerSet = {1, 2, 3}; 13 | controller = UnionSetController()..add(innerSet); 14 | }); 15 | 16 | test('exposes a union set', () { 17 | expect(controller.set, unorderedEquals([1, 2, 3])); 18 | 19 | controller.add({3, 4, 5}); 20 | expect(controller.set, unorderedEquals([1, 2, 3, 4, 5])); 21 | 22 | controller.remove(innerSet); 23 | expect(controller.set, unorderedEquals([3, 4, 5])); 24 | }); 25 | 26 | test('exposes a disjoint union set', () { 27 | expect(controller.set, unorderedEquals([1, 2, 3])); 28 | 29 | controller.add({4, 5, 6}); 30 | expect(controller.set, unorderedEquals([1, 2, 3, 4, 5, 6])); 31 | 32 | controller.remove(innerSet); 33 | expect(controller.set, unorderedEquals([4, 5, 6])); 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /test/union_set_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'package:collection/collection.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | void main() { 9 | group('with an empty outer set', () { 10 | dynamic set; 11 | setUp(() { 12 | set = UnionSet({}); 13 | }); 14 | 15 | test('length returns 0', () { 16 | expect(set.length, equals(0)); 17 | }); 18 | 19 | test('contains() returns false', () { 20 | expect(set.contains(0), isFalse); 21 | expect(set.contains(null), isFalse); 22 | expect(set.contains('foo'), isFalse); 23 | }); 24 | 25 | test('lookup() returns null', () { 26 | expect(set.lookup(0), isNull); 27 | expect(set.lookup(null), isNull); 28 | expect(set.lookup('foo'), isNull); 29 | }); 30 | 31 | test('toSet() returns an empty set', () { 32 | expect(set.toSet(), isEmpty); 33 | expect(set.toSet(), isNot(same(set))); 34 | }); 35 | 36 | test("map() doesn't run on any elements", () { 37 | expect(set.map(expectAsync1((dynamic _) {}, count: 0)), isEmpty); 38 | }); 39 | }); 40 | 41 | group('with multiple disjoint sets', () { 42 | late Set set; 43 | setUp(() { 44 | set = UnionSet.from([ 45 | {1, 2}, 46 | {3, 4}, 47 | {5}, 48 | {}, 49 | ], disjoint: true); 50 | }); 51 | 52 | test('length returns the total length', () { 53 | expect(set.length, equals(5)); 54 | }); 55 | 56 | test('contains() returns whether any set contains the element', () { 57 | expect(set.contains(1), isTrue); 58 | expect(set.contains(4), isTrue); 59 | expect(set.contains(5), isTrue); 60 | expect(set.contains(6), isFalse); 61 | }); 62 | 63 | test('lookup() returns elements that are in any set', () { 64 | expect(set.lookup(1), equals(1)); 65 | expect(set.lookup(4), equals(4)); 66 | expect(set.lookup(5), equals(5)); 67 | expect(set.lookup(6), isNull); 68 | }); 69 | 70 | test('toSet() returns the union of all the sets', () { 71 | expect(set.toSet(), unorderedEquals([1, 2, 3, 4, 5])); 72 | expect(set.toSet(), isNot(same(set))); 73 | }); 74 | 75 | test('map() maps the elements', () { 76 | expect(set.map((i) => i * 2), unorderedEquals([2, 4, 6, 8, 10])); 77 | }); 78 | }); 79 | 80 | group('with multiple overlapping sets', () { 81 | late Set set; 82 | setUp(() { 83 | set = UnionSet.from([ 84 | {1, 2, 3}, 85 | {3, 4}, 86 | {5, 1}, 87 | {}, 88 | ]); 89 | }); 90 | 91 | test('length returns the total length', () { 92 | expect(set.length, equals(5)); 93 | }); 94 | 95 | test('contains() returns whether any set contains the element', () { 96 | expect(set.contains(1), isTrue); 97 | expect(set.contains(4), isTrue); 98 | expect(set.contains(5), isTrue); 99 | expect(set.contains(6), isFalse); 100 | }); 101 | 102 | test('lookup() returns elements that are in any set', () { 103 | expect(set.lookup(1), equals(1)); 104 | expect(set.lookup(4), equals(4)); 105 | expect(set.lookup(5), equals(5)); 106 | expect(set.lookup(6), isNull); 107 | }); 108 | 109 | test('lookup() returns the first element in an ordered context', () { 110 | var duration1 = const Duration(seconds: 0); 111 | // ignore: prefer_const_constructors 112 | var duration2 = Duration(seconds: 0); 113 | expect(duration1, equals(duration2)); 114 | expect(duration1, isNot(same(duration2))); 115 | 116 | var set = UnionSet.from([ 117 | {duration1}, 118 | {duration2} 119 | ]); 120 | 121 | expect(set.lookup(const Duration(seconds: 0)), same(duration1)); 122 | }); 123 | 124 | test('toSet() returns the union of all the sets', () { 125 | expect(set.toSet(), unorderedEquals([1, 2, 3, 4, 5])); 126 | expect(set.toSet(), isNot(same(set))); 127 | }); 128 | 129 | test('map() maps the elements', () { 130 | expect(set.map((i) => i * 2), unorderedEquals([2, 4, 6, 8, 10])); 131 | }); 132 | }); 133 | 134 | group('after an inner set was modified', () { 135 | late Set set; 136 | setUp(() { 137 | var innerSet = {3, 7}; 138 | set = UnionSet.from([ 139 | {1, 2}, 140 | {5}, 141 | innerSet 142 | ]); 143 | 144 | innerSet.add(4); 145 | innerSet.remove(7); 146 | }); 147 | 148 | test('length returns the total length', () { 149 | expect(set.length, equals(5)); 150 | }); 151 | 152 | test('contains() returns true for a new element', () { 153 | expect(set.contains(4), isTrue); 154 | }); 155 | 156 | test('contains() returns false for a removed element', () { 157 | expect(set.contains(7), isFalse); 158 | }); 159 | 160 | test('lookup() returns a new element', () { 161 | expect(set.lookup(4), equals(4)); 162 | }); 163 | 164 | test("lookup() doesn't returns a removed element", () { 165 | expect(set.lookup(7), isNull); 166 | }); 167 | 168 | test('toSet() returns the union of all the sets', () { 169 | expect(set.toSet(), unorderedEquals([1, 2, 3, 4, 5])); 170 | expect(set.toSet(), isNot(same(set))); 171 | }); 172 | 173 | test('map() maps the elements', () { 174 | expect(set.map((i) => i * 2), unorderedEquals([2, 4, 6, 8, 10])); 175 | }); 176 | }); 177 | 178 | group('after the outer set was modified', () { 179 | late Set set; 180 | setUp(() { 181 | var innerSet = {6}; 182 | var outerSet = { 183 | {1, 2}, 184 | {5}, 185 | innerSet 186 | }; 187 | 188 | set = UnionSet(outerSet); 189 | outerSet.remove(innerSet); 190 | outerSet.add({3, 4}); 191 | }); 192 | 193 | test('length returns the total length', () { 194 | expect(set.length, equals(5)); 195 | }); 196 | 197 | test('contains() returns true for a new element', () { 198 | expect(set.contains(4), isTrue); 199 | }); 200 | 201 | test('contains() returns false for a removed element', () { 202 | expect(set.contains(6), isFalse); 203 | }); 204 | 205 | test('lookup() returns a new element', () { 206 | expect(set.lookup(4), equals(4)); 207 | }); 208 | 209 | test("lookup() doesn't returns a removed element", () { 210 | expect(set.lookup(6), isNull); 211 | }); 212 | 213 | test('toSet() returns the union of all the sets', () { 214 | expect(set.toSet(), unorderedEquals([1, 2, 3, 4, 5])); 215 | expect(set.toSet(), isNot(same(set))); 216 | }); 217 | 218 | test('map() maps the elements', () { 219 | expect(set.map((i) => i * 2), unorderedEquals([2, 4, 6, 8, 10])); 220 | }); 221 | }); 222 | } 223 | --------------------------------------------------------------------------------