├── .github ├── dependabot.yaml └── workflows │ └── dart.yml ├── .gitignore ├── AUTHORS ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── contributing.md ├── example └── main.dart ├── lib ├── basics.dart ├── comparable_basics.dart ├── date_time_basics.dart ├── int_basics.dart ├── iterable_basics.dart ├── list_basics.dart ├── map_basics.dart ├── set_basics.dart ├── src │ ├── slice_indices.dart │ └── sort_key_compare.dart └── string_basics.dart ├── pubspec.yaml └── test ├── comparable_basics_test.dart ├── date_time_basics_test.dart ├── int_basics_test.dart ├── iterable_basics_test.dart ├── list_basics_test.dart ├── map_basics_test.dart ├── set_basics_test.dart └── string_basics_test.dart /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | # Dependabot configuration file. 2 | version: 2 3 | enable-beta-ecosystems: true 4 | 5 | updates: 6 | - package-ecosystem: "github-actions" 7 | directory: "/" 8 | schedule: 9 | interval: "monthly" 10 | -------------------------------------------------------------------------------- /.github/workflows/dart.yml: -------------------------------------------------------------------------------- 1 | name: Dart CI 2 | 3 | on: 4 | schedule: 5 | # “At 00:00 (UTC) on Sunday.” 6 | - cron: '0 0 * * 0' 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | os: [ubuntu-latest] 19 | sdk: [2.17.0, stable, dev] 20 | 21 | name: build on ${{ matrix.os }} for ${{ matrix.sdk }} 22 | 23 | steps: 24 | - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c 25 | - uses: dart-lang/setup-dart@a57a6c04cf7d4840e88432aad6281d1e125f0d46 26 | 27 | - name: Install dependencies 28 | run: dart pub get 29 | 30 | - name: Verify formatting 31 | run: dart format --output=none --set-exit-if-changed . 32 | 33 | - name: Analyze project source 34 | run: dart analyze --fatal-infos 35 | 36 | - name: Run tests 37 | run: dart test 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Reference: 2 | 3 | # pub stuff. 4 | .dart_tool/ 5 | .packages 6 | build/ 7 | .pub/ 8 | pubspec.lock 9 | doc/api/ 10 | 11 | # IntelliJ stuff. 12 | *.iml 13 | *.ipr 14 | *.iws 15 | .idea 16 | 17 | # macOS stuff. 18 | .DS_Store 19 | 20 | .vscode/launch.json 21 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # Below is a list of people and organizations that have contributed 2 | # to the Dart Basics project. Names should be added to the list like so: 3 | # 4 | # Name/Organization 5 | 6 | Google Inc. 7 | MH Johnson 8 | Zachary Garcia 9 | eikob 10 | Tianguang 11 | James Lin 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [0.10.0] 2 | 3 | * Added `truncate` method to `String`. 4 | * Added `capitalize` method to `String`. 5 | 6 | ## [0.9.0] 7 | 8 | * Added `average` method to `Iterable`. 9 | 10 | ## [0.8.1] 11 | 12 | * Added stricter type checks. 13 | 14 | ## [0.8.0] 15 | 16 | * Added `ComparableBasics`. 17 | * Added `getRange` to `Iterable`. 18 | * Added `isNullOrBlank` and `isNotNullOrBlank` to `String?` 19 | * Improved performance of `maxBy`, `minBy` and `sortBy` by adding a cache. 20 | 21 | ## [0.7.0] 22 | 23 | Added `copyWith`, `addCalendarDays`, and `calendarDaysTill` to `DateTimeBasics`. 24 | 25 | ## [0.6.0] 26 | 27 | Remove object basics (`isNull` and `isNotNull`). 28 | 29 | ## [0.5.1] 30 | 31 | Update docs. 32 | 33 | ## [0.5.0] 34 | 35 | Updated with null safety. 36 | 37 | ## [0.4.0] 38 | 39 | Added `MapBasics.get`. 40 | 41 | ## [0.3.0] 42 | 43 | Added `DateTimeBasics`. 44 | 45 | ## [0.2.0] - 2020-04-24 46 | 47 | Add example and changelog. 48 | 49 | ## [0.1.0] - 2020-04-24 50 | 51 | Initial release. 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019, Google. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Dart CI](https://github.com/google/dart-basics/actions/workflows/dart.yml/badge.svg)](https://github.com/google/dart-basics/actions/workflows/dart.yml) 2 | [![pub package](https://img.shields.io/pub/v/basics.svg)](https://pub.dev/packages/basics) 3 | [![package publisher](https://img.shields.io/pub/publisher/basics.svg)](https://pub.dev/packages/basics/publisher) 4 | 5 | This repository contains a collection of useful extension methods on the 6 | built-in objects in Dart, such as String, Iterable, and Object. 7 | 8 | ## Usage 9 | Import the basics library. 10 | 11 | ```dart 12 | import 'package:basics/basics.dart'; 13 | ``` 14 | 15 | Then use the methods directly on objects in your dart code. 16 | 17 | ```dart 18 | import 'package:basics/basics.dart'; 19 | 20 | main() async { 21 | const numbers = [2, 4, 8]; 22 | 23 | if (numbers.all((n) => n.isEven)) { 24 | print('All numbers are even.'); 25 | } 26 | 27 | print('sum of numbers is: ${numbers.sum()}'); 28 | 29 | for (var _ in 5.range) { 30 | print('waiting 500 milliseconds...'); 31 | await Future.delayed(500.milliseconds); 32 | } 33 | } 34 | ``` 35 | 36 | ## Notes 37 | This is not an official Google project. 38 | 39 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:pedantic/analysis_options.1.8.0.yaml 2 | 3 | analyzer: 4 | language: 5 | strict-casts: true 6 | strict-inference: true 7 | strict-raw-types: true 8 | 9 | linter: 10 | rules: 11 | - avoid_types_on_closure_parameters 12 | - avoid_void_async 13 | - await_only_futures 14 | - camel_case_types 15 | - cancel_subscriptions 16 | - close_sinks 17 | - constant_identifier_names 18 | - control_flow_in_finally 19 | - directives_ordering 20 | - empty_statements 21 | - hash_and_equals 22 | - implementation_imports 23 | - non_constant_identifier_names 24 | - package_api_docs 25 | - package_names 26 | - package_prefixed_library_names 27 | - test_types_in_equals 28 | - throw_in_finally 29 | - unnecessary_brace_in_string_interps 30 | - unnecessary_getters_setters 31 | - unnecessary_new 32 | - unnecessary_statements 33 | -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution; 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | 25 | ## Community Guidelines 26 | 27 | This project follows [Google's Open Source Community 28 | Guidelines](https://opensource.google/conduct/). 29 | -------------------------------------------------------------------------------- /example/main.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020, Google Inc. Please see the AUTHORS file for details. 2 | // All rights reserved. Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | import 'package:basics/basics.dart'; 6 | 7 | void main() async { 8 | const numbers = [2, 4, 8]; 9 | 10 | if (numbers.all((n) => n.isEven)) { 11 | print('All numbers are even.'); 12 | } 13 | 14 | print('sum of numbers is: ${numbers.sum()}'); 15 | 16 | for (var _ in 5.range) { 17 | print('waiting 500 milliseconds...'); 18 | await Future.delayed(500.milliseconds); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/basics.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019, Google Inc. Please see the AUTHORS file for details. 2 | // All rights reserved. Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | export 'date_time_basics.dart'; 6 | export 'int_basics.dart'; 7 | export 'iterable_basics.dart'; 8 | export 'list_basics.dart'; 9 | export 'map_basics.dart'; 10 | export 'set_basics.dart'; 11 | export 'string_basics.dart'; 12 | -------------------------------------------------------------------------------- /lib/comparable_basics.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, Google Inc. Please see the AUTHORS file for details. 2 | // All rights reserved. Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | import 'dart:math' as math show min, max; 6 | 7 | /// Utility extension methods for the [Comparable] class. 8 | extension ComparableBasics on Comparable { 9 | /// Returns true if [this] should be ordered strictly before [other]. 10 | bool operator <(T other) => compareTo(other) < 0; 11 | 12 | /// Returns true if [this] should be ordered strictly after [other]. 13 | bool operator >(T other) => compareTo(other) > 0; 14 | 15 | /// Returns true if [this] should be ordered before or with [other]. 16 | bool operator <=(T other) => compareTo(other) <= 0; 17 | 18 | /// Returns true if [this] should be ordered after or with [other]. 19 | bool operator >=(T other) => compareTo(other) >= 0; 20 | } 21 | 22 | /// Returns the greater of two [Comparable] objects. 23 | /// 24 | /// For [num] values, behaves identically to [math.max]. 25 | /// 26 | /// If the arguments compare equal, then it is unspecified which of the two 27 | /// arguments is returned. 28 | T max>(T a, T b) { 29 | if (a is num) { 30 | return math.max(a, b as num) as T; 31 | } 32 | return (a >= b) ? a : b; 33 | } 34 | 35 | /// Returns the lesser of two [Comparable] objects. 36 | /// 37 | /// For [num] values, behaves identically to [math.min]. 38 | /// 39 | /// If the arguments compare equal, then it is unspecified which of the two 40 | /// arguments is returned. 41 | T min>(T a, T b) { 42 | if (a is num) { 43 | return math.min(a, b as num) as T; 44 | } 45 | return (a <= b) ? a : b; 46 | } 47 | -------------------------------------------------------------------------------- /lib/date_time_basics.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020, Google Inc. Please see the AUTHORS file for details. 2 | // All rights reserved. Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | /// Utility extension methods for the core [DateTime] class. 6 | extension DateTimeBasics on DateTime { 7 | /// Returns true if [this] occurs strictly before [other], accounting for time 8 | /// zones. 9 | /// 10 | /// Alias for [DateTime.isBefore]. 11 | /// 12 | /// Note that attempting to use this operator with [DateTime.==] will likely 13 | /// give undesirable results. This operator compares moments (i.e. with time 14 | /// zone taken into account), while [DateTime.==] compares field values (i.e. 15 | /// two [DateTime]s representing the same moment in different time zones will 16 | /// be treated as not equal). To check moment equality with time zone taken 17 | /// into account, use [DateTime.isAtSameMomentAs] rather than [DateTime.==]. 18 | bool operator <(DateTime other) => isBefore(other); 19 | 20 | /// Returns true if [this] occurs strictly after [other], accounting for time 21 | /// zones. 22 | /// 23 | /// Alias for [DateTime.isAfter]. 24 | /// 25 | /// Note that attempting to use this operator with [DateTime.==] will likely 26 | /// give undesirable results. This operator compares moments (i.e. with time 27 | /// zone taken into account), while [DateTime.==] compares field values (i.e. 28 | /// two [DateTime]s representing the same moment in different time zones will 29 | /// be treated as not equal). To check moment equality with time zone taken 30 | /// into account, use [DateTime.isAtSameMomentAs] rather than [DateTime.==]. 31 | bool operator >(DateTime other) => isAfter(other); 32 | 33 | /// Returns true if [this] occurs at or before [other], accounting for time 34 | /// zones. 35 | /// 36 | /// Alias for [isAtOrBefore]. 37 | /// 38 | /// Note that attempting to use this operator with [DateTime.==] will likely 39 | /// give undesirable results. This operator compares moments (i.e. with time 40 | /// zone taken into account), while [DateTime.==] compares field values (i.e. 41 | /// two [DateTime]s representing the same moment in different time zones will 42 | /// be treated as not equal). To check moment equality with time zone taken 43 | /// into account, use [DateTime.isAtSameMomentAs] rather than [DateTime.==]. 44 | bool operator <=(DateTime other) => isAtOrBefore(other); 45 | 46 | /// Returns true if [this] occurs at or after [other], accounting for time 47 | /// zones. 48 | /// 49 | /// Alias for [isAtOrAfter]. 50 | /// 51 | /// Note that attempting to use this operator with [DateTime.==] will likely 52 | /// give undesirable results. This operator compares moments (i.e. with time 53 | /// zone taken into account), while [DateTime.==] compares field values (i.e. 54 | /// two [DateTime]s representing the same moment in different time zones will 55 | /// be treated as not equal). To check moment equality with time zone taken 56 | /// into account, use [DateTime.isAtSameMomentAs] rather than [DateTime.==]. 57 | bool operator >=(DateTime other) => isAtOrAfter(other); 58 | 59 | /// Returns a new [DateTime] instance with [duration] added to [this]. 60 | /// 61 | /// Alias for [DateTime.add]. 62 | DateTime operator +(Duration duration) => add(duration); 63 | 64 | /// Returns the [Duration] between [this] and [other]. 65 | /// 66 | /// The returned [Duration] will be negative if [other] occurs after [this]. 67 | /// 68 | /// Alias for [DateTime.difference]. 69 | Duration operator -(DateTime other) => difference(other); 70 | 71 | /// Returns true if [this] occurs at or before [other], accounting for time 72 | /// zones. 73 | /// 74 | /// Delegates to [DateTime]'s built-in comparison methods and therefore obeys 75 | /// the same contract. 76 | bool isAtOrBefore(DateTime other) => 77 | isAtSameMomentAs(other) || isBefore(other); 78 | 79 | /// Returns true if [this] occurs at or after [other], accounting for time 80 | /// zones. 81 | /// 82 | /// Delegates to [DateTime]'s built-in comparison methods and therefore obeys 83 | /// the same contract. 84 | bool isAtOrAfter(DateTime other) => isAtSameMomentAs(other) || isAfter(other); 85 | 86 | /// Copies a [DateTime], overriding specified values. 87 | /// 88 | /// A UTC [DateTime] will remain in UTC; a local [DateTime] will remain local. 89 | DateTime copyWith({ 90 | int? year, 91 | int? month, 92 | int? day, 93 | int? hour, 94 | int? minute, 95 | int? second, 96 | int? millisecond, 97 | int? microsecond, 98 | }) { 99 | return (isUtc ? DateTime.utc : DateTime.new)( 100 | year ?? this.year, 101 | month ?? this.month, 102 | day ?? this.day, 103 | hour ?? this.hour, 104 | minute ?? this.minute, 105 | second ?? this.second, 106 | millisecond ?? this.millisecond, 107 | microsecond ?? this.microsecond, 108 | ); 109 | } 110 | 111 | /// Adds a specified number of days to this [DateTime]. 112 | /// 113 | /// Unlike `DateTime.add(Duration(days: numberOfDays))`, this adds calendar 114 | /// days and not 24-hour increments. When possible, it therefore leaves the 115 | /// time of day unchanged if a DST change would occur during the time 116 | /// interval. (The returned time can still be different from the original if 117 | /// it would be invalid for the returned date.) 118 | DateTime addCalendarDays(int numberOfDays) => 119 | copyWith(day: day + numberOfDays); 120 | 121 | /// Returns the number of calendar days till the specified date. 122 | /// 123 | /// Returns a negative value if the specified date is in the past. Ignores 124 | /// the time of day. 125 | /// 126 | /// Example: 127 | /// ``` 128 | /// DateTime(2020, 12, 31).calendarDaysTill(2021, 1, 1); // 1 129 | /// DateTime(2020, 12, 31, 23, 59).calendarDaysTill(2021, 1, 1); // 1 130 | /// ``` 131 | /// 132 | /// This function intentionally does not take a [DateTime] argument to: 133 | /// * More clearly indicate that it does not take time of day into account. 134 | /// * Avoid potential problems if one [DateTime] is in UTC and the other is 135 | /// not. 136 | int calendarDaysTill(int year, int month, int day) { 137 | // Discard the time of day, and perform all calculations in UTC so that 138 | // Daylight Saving Time adjustments are not a factor. 139 | // 140 | // Note that this intentionally isn't the same as `toUtc()`; we instead 141 | // want to treat this `DateTime` object *as* a UTC `DateTime`. 142 | final startDay = DateTime.utc(this.year, this.month, this.day); 143 | final endDay = DateTime.utc(year, month, day); 144 | return (endDay - startDay).inDays; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /lib/int_basics.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019, Google Inc. Please see the AUTHORS file for details. 2 | // All rights reserved. Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | /// Utility extension methods for the native [int] class. 6 | extension IntBasics on int { 7 | /// Returns an iterable from `0` up to but not including [this]. 8 | /// 9 | /// Example: 10 | /// ```dart 11 | /// 5.range; // (0, 1, 2, 3, 4) 12 | /// ``` 13 | Iterable get range => Iterable.generate(this); 14 | 15 | /// Returns an iterable from [this] inclusive to [end] exclusive. 16 | /// 17 | /// Example: 18 | /// ```dart 19 | /// 3.to(6); // (3, 4, 5) 20 | /// 2.to(-2); // (2, 1, 0, -1) 21 | /// ``` 22 | /// 23 | /// If [by] is provided, it will be used as step size for iteration. [by] is 24 | /// always positive, even if the direction of iteration is decreasing. 25 | /// 26 | /// Example: 27 | /// ```dart 28 | /// 8.to(3, by: 2); // (8, 6, 4) 29 | /// ``` 30 | Iterable to(int end, {int by = 1}) { 31 | if (by < 1) { 32 | throw ArgumentError( 33 | 'Invalid step size: $by. Step size must be greater than 0'); 34 | } 35 | final count = ((end - this).abs() / by).ceil(); 36 | // Explicit type declaration required for function argument. 37 | final int Function(int) generator = this >= end 38 | ? (index) => this - (by * index) 39 | : (index) => this + (by * index); 40 | return Iterable.generate(count, generator); 41 | } 42 | 43 | /// Returns [Duration] of [this] in days. 44 | Duration get days => Duration(days: this); 45 | 46 | /// Returns [Duration] of [this] in hours. 47 | Duration get hours => Duration(hours: this); 48 | 49 | /// Returns [Duration] of [this] in minutes. 50 | Duration get minutes => Duration(minutes: this); 51 | 52 | /// Returns [Duration] of [this] in seconds. 53 | Duration get seconds => Duration(seconds: this); 54 | 55 | /// Returns [Duration] of [this] in milliseconds. 56 | Duration get milliseconds => Duration(milliseconds: this); 57 | 58 | /// Returns [Duration] of [this] in microseconds. 59 | Duration get microseconds => Duration(microseconds: this); 60 | } 61 | -------------------------------------------------------------------------------- /lib/iterable_basics.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019, Google Inc. Please see the AUTHORS file for details. 2 | // All rights reserved. Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | import 'dart:math' as math; 6 | 7 | import 'src/sort_key_compare.dart'; 8 | 9 | /// Utility extension methods for the native [Iterable] class. 10 | extension IterableBasics on Iterable { 11 | /// Alias for [Iterable]`.every`. 12 | bool all(bool Function(E) test) => this.every(test); 13 | 14 | /// Returns `true` if no element of [this] satisfies [test]. 15 | /// 16 | /// Example: 17 | /// ```dart 18 | /// [1, 2, 3].none((e) => e > 4); // true 19 | /// [1, 2, 3].none((e) => e > 2); // false 20 | /// ``` 21 | bool none(bool Function(E) test) => !this.any(test); 22 | 23 | /// Returns `true` if there is exactly one element of [this] which satisfies 24 | /// [test]. 25 | /// 26 | /// Example: 27 | /// ```dart 28 | /// [1, 2, 3].one((e) => e == 2); // 1 element satisfies. Returns true. 29 | /// [1, 2, 3].one((e) => e > 4); // No element satisfies. Returns false. 30 | /// [1, 2, 3].one((e) => e > 1); // >1 element satisfies. Returns false. 31 | /// ``` 32 | bool one(bool Function(E) test) { 33 | bool foundOne = false; 34 | for (var e in this) { 35 | if (test(e)) { 36 | if (foundOne) return false; 37 | foundOne = true; 38 | } 39 | } 40 | return foundOne; 41 | } 42 | 43 | /// Returns `true` if [this] contains at least one element also contained in 44 | /// [other]. 45 | /// 46 | /// Example: 47 | /// ```dart 48 | /// [1, 2, 3].containsAny([5, 2]); // true 49 | /// [1, 2, 3].containsAny([4, 5, 6]); // false 50 | /// ``` 51 | bool containsAny(Iterable other) => this.any(other.contains); 52 | 53 | /// Returns true if every element in [other] also exists in [this]. 54 | /// 55 | /// Example: 56 | /// ```dart 57 | /// [1, 2, 3].containsAll([1, 2]); // true 58 | /// [1, 2].containsAll([1, 2, 3]); // false 59 | /// ``` 60 | /// 61 | /// If [collapseDuplicates] is true, only the presence of a value will be 62 | /// considered, not the number of times it occurs. If [collapseDuplicates] is 63 | /// false, the number of occurrences of a given value in [this] must be 64 | /// greater than or equal to the number of occurrences of that value in 65 | /// [other] for the result to be true. 66 | /// 67 | /// Example: 68 | /// ``` 69 | /// [1, 2, 3].containsAll([1, 1, 1, 2]); // true 70 | /// [1, 2, 3].containsAll([1, 1, 1, 2], collapseDuplicates: false); // false 71 | /// [1, 1, 2, 3].containsAll([1, 1, 2], collapseDuplicates: false); // true 72 | /// ``` 73 | bool containsAll(Iterable other, {bool collapseDuplicates = true}) { 74 | if (other.isEmpty) return true; 75 | if (collapseDuplicates) { 76 | return Set.from(this).containsAll(Set.from(other)); 77 | } 78 | 79 | final thisElementCounts = _elementCountsIn(this); 80 | final otherElementCounts = _elementCountsIn(other); 81 | 82 | for (final element in otherElementCounts.keys) { 83 | final countInThis = thisElementCounts[element] ?? 0; 84 | final countInOther = otherElementCounts[element] ?? 0; 85 | if (countInThis < countInOther) { 86 | return false; 87 | } 88 | } 89 | return true; 90 | } 91 | 92 | /// Returns the greatest element of [this] as ordered by [compare], or [null] 93 | /// if [this] is empty. 94 | /// 95 | /// Example: 96 | /// ```dart 97 | /// ['a', 'aaa', 'aa'] 98 | /// .max((a, b) => a.length.compareTo(b.length)).value; // 'aaa' 99 | /// ``` 100 | E? max(Comparator compare) => 101 | this.isEmpty ? null : this.reduce(_generateCustomMaxFunction(compare)); 102 | 103 | /// Returns the smallest element of [this] as ordered by [compare], or [null] 104 | /// if [this] is empty. 105 | /// 106 | /// Example: 107 | /// ```dart 108 | /// ['a', 'aaa', 'aa'] 109 | /// .min((a, b) => a.length.compareTo(b.length)).value; // 'a' 110 | /// ``` 111 | E? min(Comparator compare) => 112 | this.isEmpty ? null : this.reduce(_generateCustomMinFunction(compare)); 113 | 114 | /// Returns the element of [this] with the greatest value for [sortKey], or 115 | /// [null] if [this] is empty. 116 | /// 117 | /// This method is guaranteed to calculate [sortKey] only once for each 118 | /// element. 119 | /// 120 | /// Example: 121 | /// ```dart 122 | /// ['a', 'aaa', 'aa'].maxBy((e) => e.length).value; // 'aaa' 123 | /// ``` 124 | E? maxBy(Comparable Function(E) sortKey) { 125 | final sortKeyCache = >{}; 126 | return this.max((a, b) => sortKeyCompare(a, b, sortKey, sortKeyCache)); 127 | } 128 | 129 | /// Returns the element of [this] with the least value for [sortKey], or 130 | /// [null] if [this] is empty. 131 | /// 132 | /// This method is guaranteed to calculate [sortKey] only once for each 133 | /// element. 134 | /// 135 | /// Example: 136 | /// ```dart 137 | /// ['a', 'aaa', 'aa'].minBy((e) => e.length).value; // 'a' 138 | /// ``` 139 | E? minBy(Comparable Function(E) sortKey) { 140 | final sortKeyCache = >{}; 141 | return this.min((a, b) => sortKeyCompare(a, b, sortKey, sortKeyCache)); 142 | } 143 | 144 | /// Returns the sum of all the values in this iterable, as defined by 145 | /// [addend]. 146 | /// 147 | /// Returns 0 if [this] is empty. 148 | /// 149 | /// Example: 150 | /// ```dart 151 | /// ['a', 'aa', 'aaa'].sum((s) => s.length); // 6 152 | /// ``` 153 | num sum(num Function(E) addend) => this.isEmpty 154 | ? 0 155 | : this.fold(0, (prev, element) => prev + addend(element)); 156 | 157 | /// Returns the average of all the values in this iterable, as defined by 158 | /// [value]. 159 | /// 160 | /// Returns null if [this] is empty. 161 | /// 162 | /// Example: 163 | /// ```dart 164 | /// ['a', 'aa', 'aaa'].average((s) => s.length); // 2 165 | /// [].average(); // null 166 | /// ``` 167 | num? average(num Function(E) value) { 168 | if (this.isEmpty) return null; 169 | 170 | return this.sum(value) / this.length; 171 | } 172 | 173 | /// Returns a random element of [this], or [null] if [this] is empty. 174 | /// 175 | /// If [seed] is provided, will be used as the random seed for determining 176 | /// which element to select. (See [math.Random].) 177 | E? getRandom({int? seed}) => this.isEmpty 178 | ? null 179 | : this.elementAt(math.Random(seed).nextInt(this.length)); 180 | 181 | /// Returns an [Iterable] containing the first [end] elements of [this], 182 | /// excluding the first [start] elements. 183 | /// 184 | /// This method is a generalization of [List.getRange] to [Iterable]s, 185 | /// and obeys the same contract. 186 | /// 187 | /// Example: 188 | /// ```dart 189 | /// {3, 8, 12, 4, 1}.range(2, 4); // [12, 4] 190 | /// ``` 191 | Iterable getRange(int start, int end) { 192 | RangeError.checkValidRange(start, end, this.length); 193 | return this.skip(start).take(end - start); 194 | } 195 | } 196 | 197 | /// Utility extension methods for [Iterable]s containing [num]s. 198 | extension NumIterableBasics on Iterable { 199 | /// Returns the greatest number in [this], or [null] if [this] is empty. 200 | /// 201 | /// Example: 202 | /// ```dart 203 | /// [104, 3, 18].max().value; // 104 204 | /// ``` 205 | /// 206 | /// If [compare] is provided, it will be used to order the elements. 207 | /// 208 | /// Example: 209 | /// ```dart 210 | /// [-47, 10, 2].max((a, b) => 211 | /// a.toString().length.compareTo(b.toString().length)).value; // -47 212 | /// ``` 213 | E? max([Comparator? compare]) => this.isEmpty 214 | ? null 215 | : this.reduce( 216 | compare == null ? math.max : _generateCustomMaxFunction(compare)); 217 | 218 | /// Returns the least number in [this], or [null] if [this] is empty. 219 | /// 220 | /// Example: 221 | /// ```dart 222 | /// [104, 3, 18].min().value; // 3 223 | /// ``` 224 | /// 225 | /// If [compare] is provided, it will be used to order the elements. 226 | /// 227 | /// Example: 228 | /// ```dart 229 | /// [-100, -200, 5].min((a, b) => 230 | /// a.toString().length.compareTo(b.toString().length)).value; // 5 231 | /// ``` 232 | E? min([Comparator? compare]) => this.isEmpty 233 | ? null 234 | : this.reduce( 235 | compare == null ? math.min : _generateCustomMinFunction(compare)); 236 | 237 | /// Returns the sum of all the values in this iterable. 238 | /// 239 | /// If [addend] is provided, it will be used to compute the value to be 240 | /// summed. 241 | /// 242 | /// Returns 0 if [this] is empty. 243 | /// 244 | /// Example: 245 | /// ```dart 246 | /// [1, 2, 3].sum(); // 6. 247 | /// [2, 3, 4].sum((i) => i * 0.5); // 4.5. 248 | /// [].sum() // 0. 249 | /// ``` 250 | num sum([num Function(E)? addend]) { 251 | if (this.isEmpty) return 0; 252 | return addend == null 253 | ? this.reduce((a, b) => (a + b) as E) 254 | : this.fold(0, (prev, element) => prev + addend(element)); 255 | } 256 | 257 | /// Returns the average of all the values in this iterable. 258 | /// 259 | /// If [value] is provided, it will be used to compute the value to be 260 | /// averaged. 261 | /// 262 | /// Returns null if [this] is empty. 263 | /// 264 | /// Example: 265 | /// ```dart 266 | /// [2, 2, 4, 8].average(); // 4. 267 | /// [2, 2, 4, 8].average((i) => i + 1); // 5. 268 | /// [].average() // null. 269 | /// ``` 270 | num? average([num Function(E)? value]) { 271 | if (this.isEmpty) return null; 272 | 273 | return this.sum(value) / this.length; 274 | } 275 | } 276 | 277 | T Function(T, T) _generateCustomMaxFunction(Comparator compare) { 278 | T max(T a, T b) { 279 | if (compare(a, b) >= 0) return a; 280 | return b; 281 | } 282 | 283 | return max; 284 | } 285 | 286 | T Function(T, T) _generateCustomMinFunction(Comparator compare) { 287 | T min(T a, T b) { 288 | if (compare(a, b) <= 0) return a; 289 | return b; 290 | } 291 | 292 | return min; 293 | } 294 | 295 | Map _elementCountsIn(Iterable iterable) { 296 | final counts = {}; 297 | for (final element in iterable) { 298 | final currentCount = counts[element] ?? 0; 299 | counts[element] = currentCount + 1; 300 | } 301 | return counts; 302 | } 303 | -------------------------------------------------------------------------------- /lib/list_basics.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019, Google Inc. Please see the AUTHORS file for details. 2 | // All rights reserved. Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | import 'dart:math' as math; 6 | 7 | import 'src/slice_indices.dart'; 8 | import 'src/sort_key_compare.dart'; 9 | 10 | /// Utility extension methods for the native [List] class. 11 | extension ListBasics on List { 12 | /// Returns a new list containing the elements of [this] from [start] 13 | /// inclusive to [end] exclusive, skipping by [step]. 14 | /// 15 | /// Example: 16 | /// ```dart 17 | /// [1, 2, 3, 4].slice(start: 1, end: 3); // [2, 3] 18 | /// [1, 2, 3, 4].slice(start: 1, end: 4, step: 2); // [2, 4] 19 | /// ``` 20 | /// 21 | /// [start] defaults to the first element if [step] is positive and to the 22 | /// last element if [step] is negative. [end] does the opposite. 23 | /// 24 | /// Example: 25 | /// ```dart 26 | /// [1, 2, 3, 4].slice(end: 2); // [1, 2] 27 | /// [1, 2, 3, 4].slice(start: 1); // [2, 3, 4] 28 | /// [1, 2, 3, 4].slice(end: 1, step: -1); // [4, 3] 29 | /// [1, 2, 3, 4].slice(start: 2, step: -1); // [3, 2, 1] 30 | /// ``` 31 | /// 32 | /// If [start] or [end] is negative, it will be counted backwards from the 33 | /// last element of [this]. If [step] is negative, the elements will be 34 | /// returned in reverse order. 35 | /// 36 | /// Example: 37 | /// ```dart 38 | /// [1, 2, 3, 4].slice(start: -2); // [3, 4] 39 | /// [1, 2, 3, 4].slice(end: -1); // [1, 2, 3] 40 | /// [1, 2, 3, 4].slice(step: -1); // [4, 3, 2, 1] 41 | /// ``` 42 | /// 43 | /// Any out-of-range values for [start] or [end] will be truncated to the 44 | /// maximum in-range value in that direction. 45 | /// 46 | /// Example: 47 | /// ```dart 48 | /// [1, 2, 3, 4].slice(start: -100); // [1, 2, 3, 4] 49 | /// [1, 2, 3, 4].slice(end: 100); // [1, 2, 3, 4] 50 | /// ``` 51 | /// 52 | /// Will return an empty list if [start] and [end] are equal, [start] is 53 | /// greater than [end] while [step] is positive, or [end] is greater than 54 | /// [start] while [step] is negative. 55 | /// 56 | /// Example: 57 | /// ```dart 58 | /// [1, 2, 3, 4].slice(start: 1, end: -3); // [] 59 | /// [1, 2, 3, 4].slice(start: 3, end: 1); // [] 60 | /// [1, 2, 3, 4].slice(start: 1, end: 3, step: -1); // [] 61 | /// ``` 62 | List slice({int? start, int? end, int step = 1}) { 63 | final indices = sliceIndices(start, end, step, this.length); 64 | if (indices == null) { 65 | return []; 66 | } 67 | 68 | final _start = indices.start; 69 | final _end = indices.end; 70 | final slice = []; 71 | 72 | if (step > 0) { 73 | for (var i = _start; i < _end; i += step) { 74 | slice.add(this[i]); 75 | } 76 | } else { 77 | for (var i = _start; i > _end; i += step) { 78 | slice.add(this[i]); 79 | } 80 | } 81 | return slice; 82 | } 83 | 84 | /// Returns a sorted copy of this list. 85 | List sortedCopy() { 86 | return List.of(this)..sort(); 87 | } 88 | 89 | /// Sorts this list by the value returned by [sortKey] for each element. 90 | /// 91 | /// This method is guaranteed to calculate [sortKey] only once for each 92 | /// element. 93 | /// 94 | /// Example: 95 | /// ```dart 96 | /// var list = [-12, 3, 10]; 97 | /// list.sortBy((e) => e.toString().length); // list is now [3, 10, -12]. 98 | /// ``` 99 | void sortBy(Comparable Function(E) sortKey) { 100 | final sortKeyCache = >{}; 101 | this.sort((a, b) => sortKeyCompare(a, b, sortKey, sortKeyCache)); 102 | } 103 | 104 | /// Returns a copy of this list sorted by the value returned by [sortKey] for 105 | /// each element. 106 | /// 107 | /// This method is guaranteed to calculate [sortKey] only once for each 108 | /// element. 109 | /// 110 | /// Example: 111 | /// ```dart 112 | /// var list = [-12, 3, 10]; 113 | /// var sorted = list.sortedCopyBy((e) => e.toString().length); 114 | /// // list is still [-12, 3, 10]. sorted is [3, 10, -12]. 115 | /// ``` 116 | List sortedCopyBy(Comparable Function(E) sortKey) { 117 | return List.of(this)..sortBy(sortKey); 118 | } 119 | 120 | /// Removes a random element of [this] and returns it. 121 | /// 122 | /// Returns [null] if [this] is empty. 123 | /// 124 | /// If [seed] is provided, will be used as the random seed for determining 125 | /// which element to select. (See [math.Random].) 126 | E? takeRandom({int? seed}) => this.isEmpty 127 | ? null 128 | : this.removeAt(math.Random(seed).nextInt(this.length)); 129 | } 130 | -------------------------------------------------------------------------------- /lib/map_basics.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019, Google Inc. Please see the AUTHORS file for details. 2 | // All rights reserved. Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | /// Utility extension methods for the native [Map] class. 6 | extension MapBasics on Map { 7 | /// A type-checked version of [operator []] that additionally supports 8 | /// returning a default value. 9 | /// 10 | /// Returns [defaultValue] if the key is not found. This is slightly 11 | /// different from `map[key] ?? defaultValue` if the [Map] stores `null` 12 | /// values. 13 | // 14 | // Remove if implemented upstream: 15 | // https://github.com/dart-lang/sdk/issues/37392 16 | V? get(K key, {V? defaultValue}) => 17 | this.containsKey(key) ? this[key] : defaultValue; 18 | 19 | /// Returns a new [Map] containing all the entries of [this] for which the key 20 | /// satisfies [test]. 21 | /// 22 | /// Example: 23 | /// ```dart 24 | /// var map = {'a': 1, 'bb': 2, 'ccc': 3} 25 | /// map.whereKey((key) => key.length > 1); // {'bb': 2, 'ccc': 3} 26 | /// ``` 27 | Map whereKey(bool Function(K) test) => 28 | // Entries do not need to be cloned because they are const. 29 | Map.fromEntries(this.entries.where((entry) => test(entry.key))); 30 | 31 | /// Returns a new [Map] containing all the entries of [this] for which the 32 | /// value satisfies [test]. 33 | /// 34 | /// Example: 35 | /// ```dart 36 | /// var map = {'a': 1, 'b': 2, 'c': 3}; 37 | /// map.whereValue((value) => value > 1); // {'b': 2, 'c': 3} 38 | /// ``` 39 | Map whereValue(bool Function(V) test) => 40 | // Entries do not need to be cloned because they are const. 41 | Map.fromEntries(this.entries.where((entry) => test(entry.value))); 42 | 43 | /// Returns a new [Map] where each entry is inverted, with the key becoming 44 | /// the value and the value becoming the key. 45 | /// 46 | /// Example: 47 | /// ```dart 48 | /// var map = {'a': 1, 'b': 2, 'c': 3}; 49 | /// map.invert(); // {1: 'a', 2: 'b', 3: 'c'} 50 | /// ``` 51 | /// 52 | /// As Map does not guarantee an order of iteration over entries, this method 53 | /// does not guarantee which key will be preserved as the value in the case 54 | /// where more than one key is associated with the same value. 55 | /// 56 | /// Example: 57 | /// ```dart 58 | /// var map = {'a': 1, 'b': 2, 'c': 2}; 59 | /// map.invert(); // May return {1: 'a', 2: 'b'} or {1: 'a', 2: 'c'}. 60 | /// ``` 61 | Map invert() => Map.fromEntries( 62 | this.entries.map((entry) => MapEntry(entry.value, entry.key))); 63 | } 64 | -------------------------------------------------------------------------------- /lib/set_basics.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019, Google Inc. Please see the AUTHORS file for details. 2 | // All rights reserved. Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | import 'dart:math' as math; 6 | 7 | /// Utility extension methods for the native [Set] class. 8 | extension SetBasics on Set { 9 | /// Returns `true` if [this] and [other] contain exactly the same elements. 10 | /// 11 | /// Example: 12 | /// ```dart 13 | /// var set = {'a', 'b', 'c'}; 14 | /// set.isEqualTo({'b', 'a', 'c'}); // true 15 | /// set.isEqualTo({'b', 'a', 'f'}); // false 16 | /// set.isEqualTo({'a', 'b'}); // false 17 | /// set.isEqualTo({'a', 'b', 'c', 'd'}); // false 18 | /// ``` 19 | bool isEqualTo(Set other) => 20 | this.length == other.length && this.containsAll(other); 21 | 22 | /// Returns `true` if [this] and [other] have no elements in common. 23 | /// 24 | /// Example: 25 | /// ```dart 26 | /// var set = {'a', 'b', 'c'}; 27 | /// set.isDisjointWith({'d', 'e', 'f'}); // true 28 | /// set.isDisjointWith({'d', 'e', 'b'}); // false 29 | /// ``` 30 | bool isDisjointWith(Set other) => this.intersection(other).isEmpty; 31 | 32 | /// Returns `true` if [this] and [other] have at least one element in common. 33 | /// 34 | /// Example: 35 | /// ```dart 36 | /// var set = {'a', 'b', 'c'}; 37 | /// set.isIntersectingWith({'d', 'e', 'b'}); // true 38 | /// set.isIntersectingWith({'d', 'e', 'f'}); // false 39 | /// ``` 40 | bool isIntersectingWith(Set other) => 41 | this.intersection(other).isNotEmpty; 42 | 43 | /// Returns `true` if every element of [this] is contained in [other]. 44 | /// 45 | /// Example: 46 | /// ```dart 47 | /// var set = {'a', 'b', 'c'}; 48 | /// set.isSubsetOf({'a', 'b', 'c', 'd'}); // true 49 | /// set.isSubsetOf({'a', 'b', 'c'}); // true 50 | /// set.isSubsetOf({'a', 'b', 'f'}); // false 51 | /// ``` 52 | bool isSubsetOf(Set other) => 53 | this.length <= other.length && other.containsAll(this); 54 | 55 | /// Returns `true` if every element of [other] is contained in [this]. 56 | /// 57 | /// ```dart 58 | /// var set = {'a', 'b', 'c'}; 59 | /// set.isSupersetOf({'a', 'b'}); // true 60 | /// set.isSupersetOf({'a', 'b', 'c'}); // true 61 | /// set.isSupersetOf({'a', 'b', 'f'}); // false 62 | /// ``` 63 | bool isSupersetOf(Set other) => 64 | this.length >= other.length && this.containsAll(other); 65 | 66 | /// Returns `true` if every element of [this] is contained in [other] and at 67 | /// least one element of [other] is not contained in [this]. 68 | /// 69 | /// Example: 70 | /// ```dart 71 | /// var set = {'a', 'b', 'c'}; 72 | /// set.isStrictSubsetOf({'a', 'b', 'c', 'd'}); // true 73 | /// set.isStrictSubsetOf({'a', 'b', 'c'}); // false 74 | /// set.isStrictSubsetOf({'a', 'b', 'f'}); // false 75 | /// ``` 76 | bool isStrictSubsetOf(Set other) => 77 | this.length < other.length && other.containsAll(this); 78 | 79 | /// Returns `true` if every element of [other] is contained in [this] and at 80 | /// least one element of [this] is not contained in [other]. 81 | /// 82 | /// ```dart 83 | /// var set = {'a', 'b', 'c'}; 84 | /// set.isStrictSupersetOf({'a', 'b'}); // true 85 | /// set.isStrictSupersetOf({'a', 'b', 'c'}); // false 86 | /// set.isStrictSupersetOf({'a', 'b', 'f'}); // false 87 | /// ``` 88 | bool isStrictSupersetOf(Set other) => 89 | this.length > other.length && this.containsAll(other); 90 | 91 | /// Removes a random element of [this] and returns it. 92 | /// 93 | /// Returns [null] if [this] is empty. 94 | /// 95 | /// If [seed] is provided, will be used as the random seed for determining 96 | /// which element to select. (See [math.Random].) 97 | E? takeRandom({int? seed}) { 98 | if (this.isEmpty) return null; 99 | final element = this.elementAt(math.Random(seed).nextInt(this.length)); 100 | this.remove(element); 101 | return element; 102 | } 103 | 104 | /// Returns a map grouping all elements of [this] with the same value for 105 | /// [classifier]. 106 | /// 107 | /// Example: 108 | /// ```dart 109 | /// {'aaa', 'bbb', 'cc', 'a', 'bb'}.classify((e) => e.length); 110 | /// // Returns { 111 | /// // 1: {'a'}, 112 | /// // 2: {'cc', 'bb'}, 113 | /// // 3: {'aaa', 'bbb'} 114 | /// // } 115 | /// ``` 116 | Map> classify(K classifier(E element)) { 117 | final groups = >{}; 118 | for (var e in this) { 119 | groups.putIfAbsent(classifier(e), () => {}).add(e); 120 | } 121 | return groups; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /lib/src/slice_indices.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019, Google Inc. Please see the AUTHORS file for details. 2 | // All rights reserved. Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | /// Logic shared between [ListBasics]`.slice` and [StringBasics]`.slice` for 6 | /// converting user input values to normalized indices. 7 | /// 8 | /// A null return value corresponds to an empty slice. 9 | SliceIndices? sliceIndices(int? start, int? end, int step, int length) { 10 | if (step == 0) { 11 | throw ArgumentError('Slice step cannot be zero'); 12 | } 13 | 14 | // Set default values for start and end. 15 | int _start = start != null ? start : (step > 0 ? 0 : length - 1); 16 | int _end = end != null 17 | ? end 18 | // Because end is exclusive, it should be the first unreachable value in 19 | // either step direction. 20 | : (step > 0 ? length : -(length + 1)); 21 | 22 | // Convert any end-counted indices (i.e. negative indices) into real indices. 23 | if (_start < 0) { 24 | _start = length + _start; 25 | } 26 | if (_end < 0) { 27 | _end = length + _end; 28 | } 29 | 30 | // Return null for any invalid index orderings. 31 | // 32 | // Note that this must occur before index truncation, as truncation could 33 | // otherwise alter the ordering because start and end are not truncated to 34 | // the same bounds. 35 | if ((_start == _end) || 36 | (step > 0 && _start > _end) || 37 | (step < 0 && _start < _end)) { 38 | return null; 39 | } 40 | 41 | // Truncate indices to allowed bounds. 42 | if (_start < 0) { 43 | _start = 0; 44 | } else if (_start > length - 1) { 45 | _start = length - 1; 46 | } 47 | if (_end < -1) { 48 | _end = -1; 49 | } else if (_end > length) { 50 | _end = length; 51 | } 52 | 53 | return SliceIndices(_start, _end); 54 | } 55 | 56 | class SliceIndices { 57 | final int start; 58 | final int end; 59 | SliceIndices(this.start, this.end); 60 | } 61 | -------------------------------------------------------------------------------- /lib/src/sort_key_compare.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, Google Inc. Please see the AUTHORS file for details. 2 | // All rights reserved. Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | /// Shared logic for ordering and sorting methods that compare their operands 6 | /// using a calculated [sortKey]. 7 | /// 8 | /// This method obeys the contract of [Comparable].`compareTo`, except by 9 | /// comparing `sortKey(a)` and `sortKey(b)`, rather than [a] and [b]. 10 | /// 11 | /// Note that the caller must provide a cache, to avoid repeatedly computing 12 | /// the same [sortKey] values. This cache will be mutated by this function. 13 | /// It is expected that all calls to this function within a single ordering 14 | /// or sorting will provide the same cache instance with each call. 15 | int sortKeyCompare( 16 | T a, 17 | T b, 18 | Comparable Function(T) sortKey, 19 | Map> sortKeyCache, 20 | ) { 21 | final keyA = sortKeyCache.putIfAbsent(a, () => sortKey(a)); 22 | final keyB = sortKeyCache.putIfAbsent(b, () => sortKey(b)); 23 | return keyA.compareTo(keyB); 24 | } 25 | -------------------------------------------------------------------------------- /lib/string_basics.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019, Google Inc. Please see the AUTHORS file for details. 2 | // All rights reserved. Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | import 'package:characters/characters.dart'; 6 | 7 | import 'src/slice_indices.dart'; 8 | 9 | /// Utility extension methods for the native [String] class. 10 | extension StringBasics on String { 11 | /// Returns a value according to the contract for [Comparator] indicating 12 | /// the ordering between [this] and [other], ignoring letter case. 13 | /// 14 | /// Example: 15 | /// ```dart 16 | /// 'ABC'.compareToIgnoringCase('abd'); // negative value 17 | /// 'ABC'.compareToIgnoringCase('abc'); // zero 18 | /// 'ABC'.compareToIgnoringCase('abb'); // positive value 19 | /// ``` 20 | /// 21 | /// NOTE: This implementation relies on [String].`toLowerCase`, which is not 22 | /// locale aware. Therefore, this method is likely to exhibit unexpected 23 | /// behavior for non-ASCII characters. 24 | int compareToIgnoringCase(String other) => 25 | this.toLowerCase().compareTo(other.toLowerCase()); 26 | 27 | /// Returns `true` if [this] is empty or consists solely of whitespace 28 | /// characters as defined by [String.trim]. 29 | bool get isBlank => this.trim().isEmpty; 30 | 31 | /// Returns `true` if [this] is not empty and does not consist solely of 32 | /// whitespace characters as defined by [String.trim]. 33 | bool get isNotBlank => this.trim().isNotEmpty; 34 | 35 | /// Returns a copy of [this] with [prefix] removed if it is present. 36 | /// 37 | /// If [this] does not start with [prefix], returns [this]. 38 | /// 39 | /// Example: 40 | /// ```dart 41 | /// var string = 'abc'; 42 | /// string.withoutPrefix('ab'); // 'c' 43 | /// string.withoutPrefix('z'); // 'abc' 44 | /// ``` 45 | String withoutPrefix(Pattern prefix) => this.startsWith(prefix) 46 | ? this.substring(prefix.allMatches(this).first.end) 47 | : this; 48 | 49 | /// Returns a copy of [this] with [suffix] removed if it is present. 50 | /// 51 | /// If [this] does not end with [suffix], returns [this]. 52 | /// 53 | /// Example: 54 | /// ```dart 55 | /// var string = 'abc'; 56 | /// string.withoutSuffix('bc'); // 'a'; 57 | /// string.withoutSuffix('z'); // 'abc'; 58 | /// ``` 59 | String withoutSuffix(Pattern suffix) { 60 | // Can't use endsWith because that takes a String, not a Pattern. 61 | final matches = suffix.allMatches(this); 62 | return (matches.isEmpty || matches.last.end != this.length) 63 | ? this 64 | : this.substring(0, matches.last.start); 65 | } 66 | 67 | /// Returns a copy of [this] with [other] inserted starting at [index]. 68 | /// 69 | /// Example: 70 | /// ```dart 71 | /// 'word'.insert('s', 0); // 'sword' 72 | /// 'word'.insert('ke', 3); // 'worked' 73 | /// 'word'.insert('y', 4); // 'wordy' 74 | /// ``` 75 | String insert(String other, int index) => (StringBuffer() 76 | ..write(this.substring(0, index)) 77 | ..write(other) 78 | ..write(this.substring(index))) 79 | .toString(); 80 | 81 | /// Returns the concatenation of [other] and [this]. 82 | /// 83 | /// Example: 84 | /// ```dart 85 | /// 'word'.prepend('key'); // 'keyword' 86 | /// ``` 87 | String prepend(String other) => other + this; 88 | 89 | /// Divides string into everything before [pattern], [pattern], and everything 90 | /// after [pattern]. 91 | /// 92 | /// Example: 93 | /// ```dart 94 | /// 'word'.partition('or'); // ['w', 'or', 'd'] 95 | /// ``` 96 | /// 97 | /// If [pattern] is not found, the entire string is treated as coming before 98 | /// [pattern]. 99 | /// 100 | /// Example: 101 | /// ```dart 102 | /// 'word'.partition('z'); // ['word', '', ''] 103 | /// ``` 104 | List partition(Pattern pattern) { 105 | final matches = pattern.allMatches(this); 106 | if (matches.isEmpty) return [this, '', '']; 107 | final matchStart = matches.first.start; 108 | final matchEnd = matches.first.end; 109 | return [ 110 | this.substring(0, matchStart), 111 | this.substring(matchStart, matchEnd), 112 | this.substring(matchEnd) 113 | ]; 114 | } 115 | 116 | /// Returns a new string containing the characters of [this] from [start] 117 | /// inclusive to [end] exclusive, skipping by [step]. 118 | /// 119 | /// Example: 120 | /// ```dart 121 | /// 'word'.slice(start: 1, end: 3); // 'or' 122 | /// 'word'.slice(start: 1, end: 4, step: 2); // 'od' 123 | /// ``` 124 | /// 125 | /// [start] defaults to the first character if [step] is positive and to the 126 | /// last character if [step] is negative. [end] does the opposite. 127 | /// 128 | /// Example: 129 | /// ```dart 130 | /// 'word'.slice(end: 2); // 'wo' 131 | /// 'word'.slice(start: 1); // 'ord' 132 | /// 'word'.slice(end: 1, step: -1); // 'dr' 133 | /// 'word'.slice(start: 2, step: -1); // 'row' 134 | /// ``` 135 | /// 136 | /// If [start] or [end] is negative, it will be counted backwards from the 137 | /// last character of [this]. If [step] is negative, the characters will be 138 | /// returned in reverse order. 139 | /// 140 | /// Example: 141 | /// ```dart 142 | /// 'word'.slice(start: -2); // 'rd' 143 | /// 'word'.slice(end: -1); // 'wor' 144 | /// 'word'.slice(step: -1); // 'drow' 145 | /// ``` 146 | /// 147 | /// Any out-of-range values for [start] or [end] will be truncated to the 148 | /// maximum in-range value in that direction. 149 | /// 150 | /// Example: 151 | /// ```dart 152 | /// 'word'.slice(start: -100); // 'word' 153 | /// 'word'.slice(end: 100); // 'word' 154 | /// ``` 155 | /// 156 | /// Will return an empty string if [start] and [end] are equal, [start] is 157 | /// greater than [end] while [step] is positive, or [end] is greater than 158 | /// [start] while [step] is negative. 159 | /// 160 | /// Example: 161 | /// ```dart 162 | /// 'word'.slice(start: 1, end: -3); // '' 163 | /// 'word'.slice(start: 3, end: 1); // '' 164 | /// 'word'.slice(start: 1, end: 3, step: -1); // '' 165 | /// ``` 166 | String slice({int? start, int? end, int step = 1}) { 167 | final indices = sliceIndices(start, end, step, this.length); 168 | if (indices == null) { 169 | return ''; 170 | } 171 | 172 | final _start = indices.start; 173 | final _end = indices.end; 174 | final stringBuffer = StringBuffer(); 175 | 176 | if (step > 0) { 177 | for (var i = _start; i < _end; i += step) { 178 | stringBuffer.write(this[i]); 179 | } 180 | } else { 181 | for (var i = _start; i > _end; i += step) { 182 | stringBuffer.write(this[i]); 183 | } 184 | } 185 | return stringBuffer.toString(); 186 | } 187 | 188 | /// Returns [this] with characters in reverse order. 189 | /// 190 | /// Example: 191 | /// ```dart 192 | /// 'word'.reverse(); // 'drow' 193 | /// ``` 194 | /// 195 | /// WARNING: This is the naive-est possible implementation, relying on native 196 | /// string indexing. Therefore, this method is almost guaranteed to exhibit 197 | /// unexpected behavior for non-ASCII characters. 198 | String reverse() { 199 | final stringBuffer = StringBuffer(); 200 | for (var i = this.length - 1; i >= 0; i--) { 201 | stringBuffer.write(this[i]); 202 | } 203 | return stringBuffer.toString(); 204 | } 205 | 206 | /// Returns a truncated version of the string. 207 | /// 208 | /// Example: 209 | /// ```dart 210 | /// final sentence = 'The quick brown fox jumps over the lazy dog'; 211 | /// final truncated = sentence.truncate(20); // 'The quick brown fox...' 212 | /// ``` 213 | /// 214 | /// The [length] is the truncated length of the string. 215 | /// The [substitution] is the substituting string of the truncated characters. 216 | /// If not null or empty it will be appended at the end of the truncated string. 217 | /// The [trimTrailingWhitespace] is whether or not to trim the spaces of the truncated string 218 | /// before appending the ending string. 219 | /// The [includeSubstitutionInLength] is whether or not that the length of the substitution string will be included 220 | /// with the intended truncated length. 221 | String truncate( 222 | int length, { 223 | String substitution = '', 224 | bool trimTrailingWhitespace = true, 225 | bool includeSubstitutionInLength = false, 226 | }) { 227 | if (this.length <= length) { 228 | return this; 229 | } 230 | 231 | // calculate the final truncate length where whether or not to include the length of substitution string 232 | final truncatedLength = includeSubstitutionInLength 233 | ? (length - substitution.characters.length) 234 | : length; 235 | final truncated = this.characters.take(truncatedLength).toString(); 236 | 237 | // finally trim the trailing white space if needed 238 | return (trimTrailingWhitespace ? truncated.trimRight() : truncated) + 239 | substitution; 240 | } 241 | 242 | /// Returns a string with the first character in upper case. 243 | /// 244 | /// This method can capitalize first character 245 | /// that is either alphabetic or accented. 246 | /// 247 | /// If the first character is not alphabetic then return the same string. 248 | /// If [this] is empty, returns and empty string. 249 | /// 250 | /// Example: 251 | /// ```dart 252 | /// final foo = 'bar'; 253 | /// final baz = foo.capitalizeFirst(); // 'Bar' 254 | /// 255 | /// // accented first character 256 | /// final og = 'éfoo'; 257 | /// final capitalized = og.capitalizeFirst() // 'Éfoo' 258 | /// 259 | /// // non alphabetic first character 260 | /// final foo1 = '1bar'; 261 | /// final baz1 = foo1.capitalizeFirst(); // '1bar' 262 | /// 263 | /// final test = ''; 264 | /// final result = test.capitalizeFirst(); // '' 265 | /// ``` 266 | String capitalize() { 267 | if (this.isEmpty) return ''; 268 | 269 | // trim this string first 270 | final trimmed = this.trimLeft(); 271 | 272 | // convert the first character to upper case 273 | final firstCharacter = trimmed[0].toUpperCase(); 274 | 275 | return trimmed.replaceRange(0, 1, firstCharacter); 276 | } 277 | } 278 | 279 | extension NullableStringBasics on String? { 280 | /// Returns `true` if [this] is null, empty, or consists solely of 281 | /// whitespace characters as defined by [String.trim]. 282 | bool get isNullOrBlank => this?.trim().isEmpty ?? true; 283 | 284 | /// Returns `true` if [this] is not null, not empty, and does not consist 285 | /// solely of whitespace characters as defined by [String.trim]. 286 | bool get isNotNullOrBlank => this?.trim().isNotEmpty ?? false; 287 | } 288 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019, Google Inc. Please see the AUTHORS file for details. 2 | # All rights reserved. Use of this source code is governed by a BSD-style 3 | # license that can be found in the LICENSE file. 4 | 5 | name: basics 6 | version: 0.10.0 7 | description: A Dart library containing convenient extension methods on basic Dart objects. 8 | repository: https://github.com/google/dart-basics 9 | 10 | environment: 11 | sdk: ">=2.17.0 <3.0.0" 12 | 13 | dependencies: 14 | characters: ^1.2.1 15 | 16 | dev_dependencies: 17 | pedantic: ^1.8.0 18 | test: ^1.16.0 19 | -------------------------------------------------------------------------------- /test/comparable_basics_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, Google Inc. Please see the AUTHORS file for details. 2 | // All rights reserved. Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | import 'dart:math' as math; 6 | 7 | import 'package:basics/comparable_basics.dart'; 8 | import 'package:test/test.dart'; 9 | 10 | void main() { 11 | var date1 = _HideComparisonOperators(DateTime.now()); 12 | var date2 = 13 | _HideComparisonOperators(date1.value.add(const Duration(seconds: 1))); 14 | var s1 = _HideComparisonOperators('aardvark'); 15 | var s2 = _HideComparisonOperators('zebra'); 16 | 17 | test('comparison operators work', () { 18 | expect('aardvark' < 'zebra', true); 19 | 20 | expect(date1 < date2, true); 21 | 22 | expect(s1 < s2, true); 23 | expect(s2 < s1, false); 24 | expect(s1 > s2, false); 25 | expect(s2 > s1, true); 26 | 27 | expect(s1 <= s2, true); 28 | expect(s2 <= s1, false); 29 | expect(s1 >= s2, false); 30 | expect(s2 >= s1, true); 31 | 32 | expect(s1 <= s1, true); 33 | expect(s1 >= s1, true); 34 | }); 35 | 36 | test('min/max work', () { 37 | expect(max(date1, date2), date2); 38 | expect(max(date2, date1), date2); 39 | 40 | expect(min(date1, date2), date1); 41 | expect(min(date2, date1), date1); 42 | 43 | // [int] and [double] (and combinations of them) can be potentially tricky 44 | // because their inheritance tree requires both of them to implement 45 | // `Comparable` instead of `Comparable` and `Comparable` 46 | // respectively. 47 | var i = 3; 48 | var d = 3.1415; 49 | expect(max(i, d), d); 50 | expect(max(d, i), d); 51 | expect(min(i, d), i); 52 | expect(min(d, i), i); 53 | 54 | expect(max(i, i), i); 55 | expect(min(i, i), i); 56 | expect(max(d, d), d); 57 | expect(min(d, d), d); 58 | }); 59 | 60 | test("min/max behave like dart:math's min/max", () { 61 | expect(min(-0.0, 0.0), math.min(-0.0, 0.0)); 62 | expect(min(0.0, -0.0), math.min(0.0, -0.0)); 63 | expect(max(-0.0, 0.0), math.max(-0.0, 0.0)); 64 | expect(max(0.0, -0.0), math.max(0.0, -0.0)); 65 | 66 | expect( 67 | min(double.nan, 0.0), 68 | _matchesDouble(math.min(double.nan, 0.0)), 69 | ); 70 | expect( 71 | min(0.0, double.nan), 72 | _matchesDouble(math.min(0.0, double.nan)), 73 | ); 74 | expect( 75 | min(double.nan, double.nan), 76 | _matchesDouble(math.min(double.nan, double.nan)), 77 | ); 78 | expect( 79 | max(double.nan, 0.0), 80 | _matchesDouble(math.max(double.nan, 0.0)), 81 | ); 82 | expect( 83 | max(0.0, double.nan), 84 | _matchesDouble(math.max(0.0, double.nan)), 85 | ); 86 | expect( 87 | max(double.nan, double.nan), 88 | _matchesDouble(math.max(double.nan, double.nan)), 89 | ); 90 | }); 91 | 92 | test('_matchesDouble works', () { 93 | expect(_matchesDouble(0.5).matches(0.5, {}), true); 94 | expect(_matchesDouble(0.5).matches(0.0, {}), false); 95 | expect(_matchesDouble(double.nan).matches(double.nan, {}), true); 96 | expect(_matchesDouble(0.0).matches(double.nan, {}), false); 97 | expect(_matchesDouble(double.nan).matches(0.0, {}), false); 98 | }); 99 | } 100 | 101 | /// A wrapper class that provides a [Comparable.compareTo] implementation but 102 | /// that explicitly does not provide its own comparison operators. 103 | /// 104 | /// Used to test the [ComparableBasics] extension. We intentionally avoid 105 | /// using `T` directly in case it has its own comparison operators. 106 | class _HideComparisonOperators> 107 | implements Comparable<_HideComparisonOperators> { 108 | _HideComparisonOperators(this.value); 109 | 110 | final T value; 111 | 112 | @override 113 | int compareTo(_HideComparisonOperators other) => 114 | value.compareTo(other.value); 115 | } 116 | 117 | /// Returns a [Matcher] for [double] equality that, unlike [equals]. considers 118 | /// NaN values to be equal to other NaN values. 119 | Matcher _matchesDouble(double expectedValue) { 120 | bool matchesDoubleInternal(double actualValue) => 121 | (expectedValue == actualValue) || 122 | (expectedValue.isNaN && actualValue.isNaN); 123 | 124 | return predicate(matchesDoubleInternal, '<$expectedValue>'); 125 | } 126 | -------------------------------------------------------------------------------- /test/date_time_basics_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019, Google Inc. Please see the AUTHORS file for details. 2 | // All rights reserved. Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | import 'package:basics/date_time_basics.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | void main() { 9 | final utcTime = DateTime.utc(2020, 3, 27, 13, 40, 56); 10 | final utcTimeAfter = DateTime.utc(2020, 3, 27, 13, 40, 57); 11 | 12 | final localTime = utcTime.toLocal(); 13 | 14 | group('comparison operators:', () { 15 | test('DateTime < DateTime works', () { 16 | expect(utcTime < utcTime, false); 17 | expect(utcTime < utcTimeAfter, true); 18 | expect(utcTimeAfter < utcTime, false); 19 | 20 | expect(localTime < utcTime, false); 21 | expect(localTime < utcTimeAfter, true); 22 | }); 23 | 24 | test('DateTime > DateTime works', () { 25 | expect(utcTime > utcTime, false); 26 | expect(utcTime > utcTimeAfter, false); 27 | expect(utcTimeAfter > utcTime, true); 28 | 29 | expect(localTime > utcTime, false); 30 | expect(utcTimeAfter > localTime, true); 31 | }); 32 | 33 | test('DateTime <= DateTime works', () { 34 | expect(utcTime <= utcTime, true); 35 | expect(utcTime <= utcTimeAfter, true); 36 | expect(utcTimeAfter <= utcTime, false); 37 | 38 | expect(localTime <= utcTime, true); 39 | expect(localTime <= utcTimeAfter, true); 40 | }); 41 | 42 | test('DateTime >= DateTime works', () { 43 | expect(utcTime >= utcTime, true); 44 | expect(utcTime >= utcTimeAfter, false); 45 | expect(utcTimeAfter >= utcTime, true); 46 | 47 | expect(localTime >= utcTime, true); 48 | expect(utcTimeAfter >= localTime, true); 49 | }); 50 | }); 51 | 52 | group('arithmetic operators:', () { 53 | test('DateTime + Duration works', () { 54 | expect(utcTime + Duration(seconds: 1), utcTimeAfter); 55 | expect((localTime + Duration(seconds: 1)).toUtc(), utcTimeAfter); 56 | 57 | expect(utcTimeAfter + -Duration(seconds: 1), utcTime); 58 | }); 59 | 60 | test('DateTime - DateTime works', () { 61 | expect(utcTimeAfter - utcTime, Duration(seconds: 1)); 62 | expect(utcTime - utcTimeAfter, -Duration(seconds: 1)); 63 | expect(utcTimeAfter - localTime, Duration(seconds: 1)); 64 | }); 65 | }); 66 | 67 | group('isAtOrBefore', () { 68 | test('works as expected', () { 69 | expect(utcTime.isAtOrBefore(utcTime), true); 70 | expect(utcTime.isAtOrBefore(utcTimeAfter), true); 71 | expect(utcTimeAfter.isAtOrBefore(utcTime), false); 72 | 73 | expect(localTime.isAtOrBefore(utcTime), true); 74 | expect(localTime.isAtOrBefore(utcTimeAfter), true); 75 | }); 76 | }); 77 | 78 | group('isAtOrAfter', () { 79 | test('works as expected', () { 80 | expect(utcTime.isAtOrAfter(utcTime), true); 81 | expect(utcTime.isAtOrAfter(utcTimeAfter), false); 82 | expect(utcTimeAfter.isAtOrAfter(utcTime), true); 83 | 84 | expect(localTime.isAtOrAfter(utcTime), true); 85 | expect(utcTimeAfter.isAtOrAfter(localTime), true); 86 | }); 87 | }); 88 | 89 | group('copyWith:', () { 90 | test('Copies existing values', () { 91 | var copy = utcTime.copyWith(); 92 | expect(utcTime, isNot(same(copy))); 93 | expect(utcTime, copy); 94 | 95 | copy = localTime.copyWith(); 96 | expect(localTime, isNot(same(copy))); 97 | expect(localTime, copy); 98 | }); 99 | 100 | test('Overrides existing values', () { 101 | final utcOverrides = DateTime.utc(2000, 1, 2, 3, 4, 5, 6, 7); 102 | final localOverrides = utcOverrides.toLocal(); 103 | 104 | var copy = utcTime.copyWith( 105 | year: utcOverrides.year, 106 | month: utcOverrides.month, 107 | day: utcOverrides.day, 108 | hour: utcOverrides.hour, 109 | minute: utcOverrides.minute, 110 | second: utcOverrides.second, 111 | millisecond: utcOverrides.millisecond, 112 | microsecond: utcOverrides.microsecond, 113 | ); 114 | expect(copy, utcOverrides); 115 | 116 | copy = localTime.copyWith( 117 | year: localOverrides.year, 118 | month: localOverrides.month, 119 | day: localOverrides.day, 120 | hour: localOverrides.hour, 121 | minute: localOverrides.minute, 122 | second: localOverrides.second, 123 | millisecond: localOverrides.millisecond, 124 | microsecond: localOverrides.microsecond, 125 | ); 126 | expect(copy, localOverrides); 127 | }); 128 | }); 129 | 130 | test('calendarDayTo works', () { 131 | expect(DateTime(2020, 12, 31).calendarDaysTill(2021, 1, 1), 1); 132 | expect(DateTime(2020, 12, 31, 23, 59).calendarDaysTill(2021, 1, 1), 1); 133 | expect(DateTime(2021, 1, 1).calendarDaysTill(2020, 12, 31), -1); 134 | 135 | expect(DateTime(2021, 3, 1).calendarDaysTill(2021, 5, 1), 31 + 30); 136 | expect(DateTime(2021, 10, 1).calendarDaysTill(2021, 12, 1), 31 + 30); 137 | 138 | expect(DateTime.utc(2020, 12, 31).calendarDaysTill(2021, 1, 1), 1); 139 | expect(DateTime.utc(2020, 12, 31, 23, 59).calendarDaysTill(2021, 1, 1), 1); 140 | expect(DateTime.utc(2021, 1, 1).calendarDaysTill(2020, 12, 31), -1); 141 | 142 | expect(DateTime.utc(2021, 3, 1).calendarDaysTill(2021, 5, 1), 31 + 30); 143 | expect(DateTime.utc(2021, 10, 1).calendarDaysTill(2021, 12, 1), 31 + 30); 144 | }); 145 | 146 | group('addCalendarDays:', () { 147 | // Pick an hour that is likely to always be valid. 148 | final startDate = DateTime(2020, 1, 1, 12, 34, 56); 149 | 150 | const daysInYear = 366; // `startDate` is in a leap year. 151 | 152 | test('Adds the correct number of days', () { 153 | for (var i = 0; i <= daysInYear; i += 1) { 154 | var futureDate = startDate.addCalendarDays(i); 155 | expect( 156 | startDate.calendarDaysTill( 157 | futureDate.year, futureDate.month, futureDate.day), 158 | i, 159 | ); 160 | } 161 | }); 162 | 163 | test('Preserves time of day', () { 164 | for (var i = 0; i <= daysInYear; i += 1) { 165 | var futureDate = startDate.addCalendarDays(i); 166 | expect( 167 | [futureDate.hour, futureDate.minute, futureDate.second], 168 | [startDate.hour, startDate.minute, startDate.second], 169 | ); 170 | } 171 | 172 | // This test is unfortunately dependent on the local timezone and is not 173 | // meaningful if the local timezone does not observe daylight saving time. 174 | }, skip: !_observesDaylightSaving()); 175 | }); 176 | } 177 | 178 | /// Tries to empirically determine if the local timezone observes daylight 179 | /// saving time changes. 180 | /// 181 | /// We can't control the local timezone used by [DateTime] in tests, so we have 182 | /// to guess. 183 | bool _observesDaylightSaving() { 184 | final startDate = DateTime(2020, 1, 1, 12, 0); 185 | var localDate = startDate.copyWith(); 186 | 187 | const oneDay = Duration(days: 1); 188 | while (localDate.year < startDate.year + 1) { 189 | localDate = localDate.add(oneDay); 190 | if (localDate.hour != startDate.hour || 191 | localDate.minute != startDate.minute || 192 | localDate.second != startDate.second) { 193 | return true; 194 | } 195 | } 196 | return false; 197 | } 198 | -------------------------------------------------------------------------------- /test/int_basics_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019, Google Inc. Please see the AUTHORS file for details. 2 | // All rights reserved. Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | import 'package:basics/basics.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | void main() { 9 | group('range', () { 10 | test('returns iterable of provided length', () { 11 | expect(5.range.toList(), [0, 1, 2, 3, 4]); 12 | }); 13 | 14 | test('returns empty iterable when called on 0', () { 15 | expect(0.range.isEmpty, isTrue); 16 | }); 17 | 18 | test('returns empty iterable when called on negative numbers', () { 19 | expect((-5).range.isEmpty, isTrue); 20 | }); 21 | }); 22 | 23 | group('to', () { 24 | test('returns iterable from a lesser to a greater value', () { 25 | expect(3.to(6).toList(), [3, 4, 5]); 26 | }); 27 | 28 | test('returns iterable from a greater to a lesser value', () { 29 | expect(6.to(4).toList(), [6, 5]); 30 | }); 31 | 32 | test('returns iterable from a lesser to a greater value by step', () { 33 | expect(3.to(10, by: 3).toList(), [3, 6, 9]); 34 | }); 35 | 36 | test('returns iterable from a greater to a lesser value by step', () { 37 | expect(8.to(2, by: 2).toList(), [8, 6, 4]); 38 | }); 39 | 40 | test('works with a negative start', () { 41 | expect((-3).to(1).toList(), [-3, -2, -1, 0]); 42 | }); 43 | 44 | test('works with a negative end', () { 45 | expect((1).to(-2).toList(), [1, 0, -1]); 46 | }); 47 | 48 | test('works with a negative start and negative end', () { 49 | expect((-4).to(-1).toList(), [-4, -3, -2]); 50 | }); 51 | 52 | test('returns an empty iterable when start and end are equal', () { 53 | expect(4.to(4).isEmpty, isTrue); 54 | }); 55 | }); 56 | 57 | group('$DateTime constructors', () { 58 | test('create proper durations with positive values', () { 59 | expect(5.days, Duration(days: 5)); 60 | expect(5.hours, Duration(hours: 5)); 61 | expect(5.minutes, Duration(minutes: 5)); 62 | expect(5.seconds, Duration(seconds: 5)); 63 | expect(5.milliseconds, Duration(milliseconds: 5)); 64 | expect(5.microseconds, Duration(microseconds: 5)); 65 | }); 66 | 67 | test('create proper durations with 0 values', () { 68 | expect(0.days, Duration(days: 0)); 69 | expect(0.hours, Duration(hours: 0)); 70 | expect(0.minutes, Duration(minutes: 0)); 71 | expect(0.seconds, Duration(seconds: 0)); 72 | expect(0.milliseconds, Duration(milliseconds: 0)); 73 | expect(0.microseconds, Duration(microseconds: 0)); 74 | }); 75 | 76 | test('create proper durations with negative values', () { 77 | expect((-5).days, Duration(days: -5)); 78 | expect((-5).hours, Duration(hours: -5)); 79 | expect((-5).minutes, Duration(minutes: -5)); 80 | expect((-5).seconds, Duration(seconds: -5)); 81 | expect((-5).milliseconds, Duration(milliseconds: -5)); 82 | expect((-5).microseconds, Duration(microseconds: -5)); 83 | }); 84 | }); 85 | } 86 | -------------------------------------------------------------------------------- /test/iterable_basics_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019, Google Inc. Please see the AUTHORS file for details. 2 | // All rights reserved. Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | import 'package:basics/basics.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | void main() { 9 | group('none', () { 10 | test('returns true when no elements satisfy test', () { 11 | final nums = [1, 4, 8, 3]; 12 | final result = nums.none((n) => n < 0); 13 | expect(result, isTrue); 14 | }); 15 | 16 | test('returns false when any element satisfies test', () { 17 | final strings = ['aa', 'bb', 'ccc', 'dd']; 18 | final result = strings.none((s) => s.length > 2); 19 | expect(result, isFalse); 20 | }); 21 | 22 | test('returns true for an empty iterable', () { 23 | final empty = []; 24 | final result = empty.none((e) => e.toString() == 'foo'); 25 | expect(result, isTrue); 26 | }); 27 | }); 28 | 29 | group('one', () { 30 | test('returns true when exactly one element satisfies test', () { 31 | final strings = ['aa', 'bb', 'ccc', 'dd']; 32 | final result = strings.one((s) => s.length == 3); 33 | expect(result, isTrue); 34 | }); 35 | 36 | test('returns false when no elements satisfy test', () { 37 | final nums = [1, 4, 8, 3]; 38 | final result = nums.one((n) => n < 0); 39 | expect(result, isFalse); 40 | }); 41 | 42 | test('returns false when more than one element satisfies test', () { 43 | final nums = [1, 4, 8, 3]; 44 | final result = nums.one((n) => n > 2); 45 | expect(result, isFalse); 46 | }); 47 | 48 | test('returns false for an empty iterable', () { 49 | final empty = []; 50 | final result = empty.one((e) => e.toString() == 'foo'); 51 | expect(result, isFalse); 52 | }); 53 | }); 54 | 55 | group('containsAny', () { 56 | test('returns true when exactly one element overlaps', () { 57 | final strings1 = ['a', 'b', 'c']; 58 | final strings2 = ['c', 'd', 'e']; 59 | 60 | expect(strings1.containsAny(strings2), isTrue); 61 | expect(strings2.containsAny(strings1), isTrue); 62 | }); 63 | 64 | test('returns true when more than one element overlaps', () { 65 | final strings1 = ['a', 'b', 'c']; 66 | final strings2 = ['b', 'c', 'd']; 67 | 68 | expect(strings1.containsAny(strings2), isTrue); 69 | expect(strings2.containsAny(strings1), isTrue); 70 | }); 71 | 72 | test('returns true when all elements overlap', () { 73 | final strings1 = ['a', 'b', 'c']; 74 | final strings2 = ['a', 'b', 'c']; 75 | 76 | expect(strings1.containsAny(strings2), isTrue); 77 | expect(strings2.containsAny(strings1), isTrue); 78 | }); 79 | 80 | test('returns false when no elements match', () { 81 | final nums1 = [1, 2, 3]; 82 | final nums2 = [4, 5, 6]; 83 | 84 | expect(nums1.containsAny(nums2), isFalse); 85 | expect(nums2.containsAny(nums1), isFalse); 86 | }); 87 | 88 | test('returns false for one empty iterable', () { 89 | final empty = []; 90 | final nonEmpty = [1, 2, 3]; 91 | 92 | expect(empty.containsAny(nonEmpty), isFalse); 93 | expect(nonEmpty.containsAny(empty), isFalse); 94 | }); 95 | 96 | test('returns false for two empty iterables', () { 97 | final empty1 = []; 98 | final empty2 = []; 99 | 100 | expect(empty1.containsAny(empty2), isFalse); 101 | expect(empty2.containsAny(empty1), isFalse); 102 | }); 103 | }); 104 | 105 | group('containsAll', () { 106 | test('returns true when all elements match', () { 107 | final strings1 = ['a', 'b', 'c']; 108 | final strings2 = ['a', 'b', 'c']; 109 | 110 | expect(strings1.containsAll(strings2), isTrue); 111 | expect(strings2.containsAll(strings1), isTrue); 112 | }); 113 | 114 | test('returns true when second list is a subset of the first', () { 115 | final strings1 = ['a', 'b', 'c', 'd', 'e']; 116 | final strings2 = ['a', 'b', 'c']; 117 | 118 | expect(strings1.containsAll(strings2), isTrue); 119 | }); 120 | 121 | test('returns false when no elements match', () { 122 | final nums1 = [1, 2, 3]; 123 | final nums2 = [4, 5, 6]; 124 | 125 | expect(nums1.containsAll(nums2), isFalse); 126 | }); 127 | 128 | test('returns false when only one element overlaps', () { 129 | final strings1 = ['a', 'b', 'c']; 130 | final strings2 = ['c', 'd', 'e']; 131 | 132 | expect(strings1.containsAll(strings2), isFalse); 133 | }); 134 | 135 | test('returns true for empty iterable as argument', () { 136 | final empty = []; 137 | final nonEmpty = [1, 2, 3]; 138 | 139 | expect(nonEmpty.containsAll(empty), isTrue); 140 | }); 141 | 142 | test('returns false when called on empty iterable', () { 143 | final empty = []; 144 | final nonEmpty = [1, 2, 3]; 145 | 146 | expect(empty.containsAll(nonEmpty), isFalse); 147 | }); 148 | 149 | test('returns true for two empty iterables', () { 150 | final empty1 = []; 151 | final empty2 = []; 152 | 153 | expect(empty1.containsAll(empty2), isTrue); 154 | }); 155 | 156 | test('collapses duplicates by default', () { 157 | final nums1 = [1, 2, 3]; 158 | final nums2 = [1, 1, 1, 1, 1, 1, 2]; 159 | 160 | expect(nums1.containsAll(nums2), isTrue); 161 | }); 162 | 163 | test('does not collapse duplicates when collapseDuplicates is false', () { 164 | final nums1 = [1, 2, 3]; 165 | final nums2 = [1, 1, 1, 1, 1, 1, 2]; 166 | 167 | expect(nums1.containsAll(nums2, collapseDuplicates: false), isFalse); 168 | }); 169 | 170 | test( 171 | 'returns false when duplicates are not collapsed and other iterable ' 172 | 'contains more occurrences of an element than this iterable', () { 173 | final nums1 = [1, 2, 3, 4, 5]; 174 | final nums2 = [1, 1, 1]; 175 | 176 | expect(nums1.containsAll(nums2, collapseDuplicates: false), isFalse); 177 | }); 178 | 179 | test('returns true when duplicates are not collapsed and have equal counts', 180 | () { 181 | final nums1 = [1, 1, 1, 2, 2, 3, 4, 5]; 182 | final nums2 = [1, 1, 1, 2, 2, 3]; 183 | 184 | expect(nums1.containsAll(nums2, collapseDuplicates: false), isTrue); 185 | }); 186 | 187 | test( 188 | 'returns false when duplicates are not collapsed and there are no ' 189 | 'overlapping elements', () { 190 | final nums1 = [1]; 191 | final nums2 = [2]; 192 | 193 | expect(nums1.containsAll(nums2, collapseDuplicates: false), isFalse); 194 | }); 195 | }); 196 | 197 | group('max', () { 198 | test('works with custom comparator', () { 199 | final strings = ['a', 'aaa', 'aa']; 200 | 201 | expect( 202 | strings.max((a, b) => a.length.compareTo(b.length))!, equals('aaa')); 203 | }); 204 | 205 | test('works on sets', () { 206 | final strings = {'a', 'aaa', 'aa'}; 207 | 208 | expect( 209 | strings.max((a, b) => a.length.compareTo(b.length))!, equals('aaa')); 210 | }); 211 | 212 | test('returns the first result when multiple elements match', () { 213 | final strings = {'a', 'aaa', 'bbb'}; 214 | 215 | expect( 216 | strings.max((a, b) => a.length.compareTo(b.length))!, equals('aaa')); 217 | }); 218 | 219 | test('works on dynamic list with custom comparator', () { 220 | final items = [1, 'aaa', 2.0]; 221 | 222 | expect(items.max((a, b) => _getItemSize(a).compareTo(_getItemSize(b)))!, 223 | equals('aaa')); 224 | 225 | items.add(5.0); 226 | expect(items.max((a, b) => _getItemSize(a).compareTo(_getItemSize(b)))!, 227 | equals(5.0)); 228 | }); 229 | 230 | test('returns null for empty iterable', () { 231 | final emptyInts = []; 232 | final emptyStrings = {}; 233 | 234 | expect(emptyInts.max(), isNull); 235 | expect(emptyInts.max((a, b) => a.compareTo(b)), isNull); 236 | expect(emptyStrings.max((a, b) => a.length.compareTo(b.length)), isNull); 237 | }); 238 | }); 239 | 240 | group('max on Iterable of nums', () { 241 | test('returns largest int', () { 242 | final nums = [1, 2, 3]; 243 | 244 | expect(nums.max()!, equals(3)); 245 | }); 246 | 247 | test('returns largest double', () { 248 | final nums = [1.0, 2.0, 3.0]; 249 | 250 | expect(nums.max()!, equals(3.0)); 251 | }); 252 | 253 | test('returns largest num', () { 254 | final nums = [1, 2.5, 3]; 255 | 256 | expect(nums.max()!, equals(3)); 257 | }); 258 | 259 | test('returns smallest double with custom comparator', () { 260 | final nums = [1.0, 2.5, 3.0]; 261 | 262 | expect(nums.max((a, b) => b.compareTo(a))!, equals(1.0)); 263 | }); 264 | }); 265 | 266 | group('min', () { 267 | test('works with custom comparator', () { 268 | final strings = ['a', 'aaa', 'aa']; 269 | 270 | expect(strings.min((a, b) => a.length.compareTo(b.length))!, equals('a')); 271 | }); 272 | 273 | test('works on sets', () { 274 | final strings = {'a', 'aaa', 'aa'}; 275 | 276 | expect(strings.min((a, b) => a.length.compareTo(b.length))!, equals('a')); 277 | }); 278 | 279 | test('returns the first result when multiple elements match', () { 280 | final strings = {'a', 'aaa', 'b'}; 281 | 282 | expect(strings.min((a, b) => a.length.compareTo(b.length))!, equals('a')); 283 | }); 284 | 285 | test('works on dynamic list with custom comparator', () { 286 | final items = [1, 'aaa', 2.0]; 287 | 288 | expect(items.min((a, b) => _getItemSize(a).compareTo(_getItemSize(b)))!, 289 | equals(1)); 290 | 291 | items.add(0.5); 292 | expect(items.min((a, b) => _getItemSize(a).compareTo(_getItemSize(b)))!, 293 | equals(0.5)); 294 | }); 295 | 296 | test('returns null for empty iterable', () { 297 | final emptyInts = []; 298 | final emptyStrings = {}; 299 | 300 | expect(emptyInts.min(), isNull); 301 | expect(emptyInts.min((a, b) => a.compareTo(b)), isNull); 302 | expect(emptyStrings.min((a, b) => a.length.compareTo(b.length)), isNull); 303 | }); 304 | }); 305 | 306 | group('min on Iterable of nums', () { 307 | test('returns smallest int', () { 308 | final nums = [1, 2, 3]; 309 | 310 | expect(nums.min()!, equals(1)); 311 | }); 312 | 313 | test('returns smallest double', () { 314 | final nums = [1.0, 2.0, 3.0]; 315 | 316 | expect(nums.min()!, equals(1.0)); 317 | }); 318 | 319 | test('returns smallest num', () { 320 | final nums = [1, 2.5, 3]; 321 | 322 | expect(nums.min()!, equals(1)); 323 | }); 324 | 325 | test('returns largest double with custom comparator', () { 326 | final nums = [1.0, 2.5, 3.0]; 327 | 328 | expect(nums.min((a, b) => b.compareTo(a))!, equals(3.0)); 329 | }); 330 | }); 331 | 332 | group('maxBy', () { 333 | test('returns the max value according to provided sort key', () { 334 | final list = [3, 222, 11]; 335 | expect(list.maxBy((e) => e.toString().length)!, 222); 336 | expect(list.maxBy((e) => e.toString())!, 3); 337 | }); 338 | 339 | test('works on sets', () { 340 | final values = {3, 222, 11}; 341 | expect(values.maxBy((e) => e.toString().length)!, 222); 342 | expect(values.maxBy((e) => e.toString())!, 3); 343 | }); 344 | 345 | test('returns null for empty iterable', () { 346 | final emptyInts = []; 347 | final emptyStrings = {}; 348 | 349 | expect(emptyInts.maxBy((a) => a.toString().length), isNull); 350 | expect(emptyStrings.maxBy((a) => a.length), isNull); 351 | }); 352 | 353 | test('does not calculate any sort key more than once', () { 354 | final values = [3, 222, 3, 15, 18]; 355 | 356 | final callCounts = {}; 357 | int recordCallAndReturn(int e) { 358 | callCounts.update(e, (value) => value++, ifAbsent: () => 1); 359 | return e; 360 | } 361 | 362 | final result = values.maxBy((e) => recordCallAndReturn(e)); 363 | 364 | expect(result, 222); 365 | expect(callCounts.length, values.toSet().length); 366 | expect(callCounts.keys.toSet(), values.toSet()); 367 | expect(callCounts.values.every((e) => e == 1), isTrue); 368 | }); 369 | }); 370 | 371 | group('minBy', () { 372 | test('returns the min value according to provided sort key', () { 373 | final list = [3, 222, 11]; 374 | expect(list.minBy((e) => e.toString().length)!, 3); 375 | expect(list.minBy((e) => e.toString())!, 11); 376 | }); 377 | 378 | test('works on sets', () { 379 | final values = {3, 222, 11}; 380 | expect(values.minBy((e) => e.toString().length)!, 3); 381 | expect(values.minBy((e) => e.toString())!, 11); 382 | }); 383 | 384 | test('returns null for empty iterable', () { 385 | final emptyInts = []; 386 | final emptyStrings = {}; 387 | 388 | expect(emptyInts.minBy((a) => a.toString().length), isNull); 389 | expect(emptyStrings.minBy((a) => a.length), isNull); 390 | }); 391 | 392 | test('does not calculate any sort key more than once', () { 393 | final values = [3, 222, 3, 15, 18]; 394 | 395 | final callCounts = {}; 396 | int recordCallAndReturn(int e) { 397 | callCounts.update(e, (value) => value++, ifAbsent: () => 1); 398 | return e; 399 | } 400 | 401 | final result = values.minBy((e) => recordCallAndReturn(e)); 402 | 403 | expect(result, 3); 404 | expect(callCounts.length, values.toSet().length); 405 | expect(callCounts.keys.toSet(), values.toSet()); 406 | expect(callCounts.values.every((e) => e == 1), isTrue); 407 | }); 408 | }); 409 | 410 | group('average', () { 411 | test('returns average on list of numbers', () { 412 | final nums = [2, 2, 4, 8]; 413 | final ints = [2, 2, 2]; 414 | final numbers = [1.5, 1.5, 3, 3]; 415 | 416 | expect(nums.average(), 4); 417 | expect(ints.average(), 2); 418 | expect(ints.average((n) => n * 2), 4); 419 | expect(nums.average((n) => n + 1), 5); 420 | expect(numbers.average(), 2.25); 421 | }); 422 | 423 | test('returns null on empty lists', () { 424 | expect([].average(), null); 425 | expect([].average(), null); 426 | expect([].average((n) => n * 2), null); 427 | expect([].average((s) => s.length), null); 428 | expect([].average((n) => n * 2), null); 429 | }); 430 | 431 | test('returns average of custom value function', () { 432 | final strings = ['a', 'aa', 'aaa']; 433 | 434 | expect(strings.average((s) => s.length), 2); 435 | }); 436 | 437 | test('works on sets', () { 438 | final strings = {'a', 'aa', 'aaa'}; 439 | 440 | expect(strings.average((s) => s.length), 2); 441 | }); 442 | 443 | test('works on dynamic list with custom value function', () { 444 | final items = [1, 'aaa', 2.0]; 445 | 446 | expect(items.average((a) => _getItemSize(a)), equals(2)); 447 | 448 | items.add(0.5); 449 | expect(items.average((a) => _getItemSize(a)), equals(1.625)); 450 | }); 451 | }); 452 | 453 | group('sum', () { 454 | test('returns sum on list of numbers', () { 455 | final nums = [1.5, 2, 3]; 456 | final ints = [2, 3, 4]; 457 | 458 | expect(nums.sum(), 6.5); 459 | expect(nums.sum((n) => n * 2), 13); 460 | expect(ints.sum((n) => n * 0.5), 4.5); 461 | }); 462 | 463 | test('returns 0 on empty lists', () { 464 | expect([].sum(), 0); 465 | expect([].sum(), 0); 466 | expect([].sum((n) => n * 2), 0); 467 | expect([].sum((s) => s.length), 0); 468 | expect([].sum((n) => n * 2), 0); 469 | }); 470 | 471 | test('returns sum of custom addend', () { 472 | final strings = ['a', 'aa', 'aaa']; 473 | 474 | expect(strings.sum((s) => s.length), 6); 475 | }); 476 | 477 | test('works on sets', () { 478 | final strings = {'a', 'aa', 'aaa'}; 479 | 480 | expect(strings.sum((s) => s.length), 6); 481 | }); 482 | 483 | test('works on dynamic list with custom addend', () { 484 | final items = [1, 'aaa', 2.0]; 485 | 486 | expect(items.sum((a) => _getItemSize(a)), equals(6)); 487 | 488 | items.add(0.5); 489 | expect(items.sum((a) => _getItemSize(a)), equals(6.5)); 490 | }); 491 | }); 492 | 493 | group('getRandom', () { 494 | test('returns null for an empty iterable', () { 495 | expect([].getRandom(), isNull); 496 | }); 497 | 498 | test('returns the only value of an iterable of length 1', () { 499 | expect(['a'].getRandom(), 'a'); 500 | }); 501 | 502 | test('returns a fixed element when a seed is provided', () { 503 | expect(['a', 'b', 'c', 'd'].getRandom(seed: 45), 'c'); 504 | }); 505 | }); 506 | 507 | group('getRange', () { 508 | test('returns a range from an iterable', () { 509 | final values = {3, 8, 12, 4, 1}; 510 | expect(values.getRange(2, 4), [12, 4]); 511 | expect(values.getRange(0, 2), [3, 8]); 512 | expect(values.getRange(3, 5), [4, 1]); 513 | expect(values.getRange(3, 3), []); 514 | expect(values.getRange(5, 5), []); 515 | }); 516 | 517 | test('matches the behavior of List#getRange', () { 518 | final iterableValues = {3, 8, 12, 4, 1}; 519 | final listValues = [3, 8, 12, 4, 1]; 520 | expect(iterableValues.getRange(2, 4), listValues.getRange(2, 4)); 521 | expect(iterableValues.getRange(0, 2), listValues.getRange(0, 2)); 522 | expect(iterableValues.getRange(3, 5), listValues.getRange(3, 5)); 523 | expect(iterableValues.getRange(3, 3), listValues.getRange(3, 3)); 524 | expect(iterableValues.getRange(5, 5), listValues.getRange(5, 5)); 525 | }); 526 | 527 | test('throws error when start > number of elements', () { 528 | final values = {3, 8, 12, 4, 1}; 529 | expect(() => values.getRange(6, 7), throwsRangeError); 530 | }); 531 | 532 | test('matches List#getRange when start > number of elements', () { 533 | final iterableValues = {3, 8, 12, 4, 1}; 534 | final listValues = [3, 8, 12, 4, 1]; 535 | final iterableError = _getRangeError(() => iterableValues.getRange(6, 7)); 536 | final listError = _getRangeError(() => listValues.getRange(6, 7)); 537 | _expectRangeErrorMatches(iterableError, listError); 538 | }); 539 | 540 | test('throws error when end > number of elements', () { 541 | final values = {3, 8, 12, 4, 1}; 542 | expect(() => values.getRange(4, 8), throwsRangeError); 543 | }); 544 | 545 | test('matches List#getRange when end > number of elements', () { 546 | final iterableValues = {3, 8, 12, 4, 1}; 547 | final listValues = [3, 8, 12, 4, 1]; 548 | final iterableError = _getRangeError(() => iterableValues.getRange(4, 8)); 549 | final listError = _getRangeError(() => listValues.getRange(4, 8)); 550 | _expectRangeErrorMatches(iterableError, listError); 551 | }); 552 | }); 553 | } 554 | 555 | num _getItemSize(dynamic item) { 556 | if (item is num) return item; 557 | if (item is String) return item.length; 558 | throw UnimplementedError(); 559 | } 560 | 561 | RangeError _getRangeError(Iterable Function() f) { 562 | try { 563 | f(); 564 | } on RangeError catch (e) { 565 | return e; 566 | } 567 | throw AssertionError("Expected RangeError but none was thrown"); 568 | } 569 | 570 | void _expectRangeErrorMatches(RangeError actual, RangeError expected) { 571 | expect(actual.start, expected.start); 572 | expect(actual.end, expected.end); 573 | expect(actual.name, expected.name); 574 | expect(actual.message, expected.message); 575 | } 576 | -------------------------------------------------------------------------------- /test/list_basics_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019, Google Inc. Please see the AUTHORS file for details. 2 | // All rights reserved. Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | import 'package:basics/basics.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | void main() { 9 | group('slice', () { 10 | test('returns the elements from start inclusive', () { 11 | final list = [1, 2, 3, 4]; 12 | expect(list.slice(start: 1), [2, 3, 4]); 13 | expect(list.slice(start: 2, step: -2), [3, 1]); 14 | }); 15 | 16 | test('returns the elements until end exclusive', () { 17 | final list = [1, 2, 3, 4, 5]; 18 | expect(list.slice(end: 2), [1, 2]); 19 | expect(list.slice(end: 1, step: -1), [5, 4, 3]); 20 | }); 21 | 22 | test('returns the elements between start and end', () { 23 | final list = [1, 2, 3, 4]; 24 | expect(list.slice(start: 1, end: 3), [2, 3]); 25 | }); 26 | 27 | test('skips elements by step', () { 28 | final list = [1, 2, 3, 4]; 29 | expect(list.slice(step: 2), [1, 3]); 30 | }); 31 | 32 | test('returns the elements between start and end by step', () { 33 | final list = [1, 2, 3, 4]; 34 | expect(list.slice(start: 1, end: 4, step: 2), [2, 4]); 35 | }); 36 | 37 | test('accepts steps that do not evenly divide the total number of elements', 38 | () { 39 | final list = [1, 2, 3, 4]; 40 | expect(list.slice(step: 3), [1, 4]); 41 | }); 42 | 43 | test('returns elements in reverse order when step is negative', () { 44 | final list = [1, 2, 3]; 45 | expect(list.slice(step: -1), [3, 2, 1]); 46 | }); 47 | 48 | test('counts a negative start index from the end of the list', () { 49 | final list = [1, 2, 3, 4]; 50 | expect(list.slice(start: -2), [3, 4]); 51 | }); 52 | 53 | test('counts a negative end index from the end of the list', () { 54 | final list = [1, 2, 3, 4, 5]; 55 | expect(list.slice(end: -2), [1, 2, 3]); 56 | }); 57 | 58 | test( 59 | 'starts from the final element if start is greater than the length ' 60 | 'of the list', () { 61 | final list = [1, 2, 3, 4]; 62 | expect(list.slice(start: 100, end: 1), []); 63 | expect(list.slice(start: 100, end: 1, step: -1), [4, 3]); 64 | }); 65 | 66 | test( 67 | 'ends with the final element if end is greater than the length of the ' 68 | 'list', () { 69 | final list = [1, 2, 3, 4, 5]; 70 | expect(list.slice(start: 2, end: 100), [3, 4, 5]); 71 | }); 72 | 73 | test( 74 | 'starts from the first element if start is less than the negative ' 75 | 'length of the list', () { 76 | final list = [1, 2, 3]; 77 | expect(list.slice(start: -100, end: 3), [1, 2, 3]); 78 | }); 79 | 80 | test( 81 | 'ends with the first element if end is less than the negative length ' 82 | 'of the list', () { 83 | final list = [1, 2, 3, 4]; 84 | expect(list.slice(start: 2, end: -100), []); 85 | expect(list.slice(start: 2, end: -100, step: -1), [3, 2, 1]); 86 | }); 87 | 88 | test('returns an empty list if start and end are equal', () { 89 | final list = [1, 2, 3, 4, 5]; 90 | expect(list.slice(start: 2, end: 2), []); 91 | expect(list.slice(start: -2, end: -2), []); 92 | }); 93 | 94 | test( 95 | 'returns an empty list if start and end are not equal but correspond ' 96 | 'to the same element', () { 97 | final list = [1, 2, 3, 4]; 98 | expect(list.slice(start: 1, end: -3), []); 99 | expect(list.slice(start: 2, end: -2), []); 100 | }); 101 | 102 | test( 103 | 'returns an empty list if start (or its equivalent index) is greater ' 104 | 'than end (or its equivalent index) and step is positive', () { 105 | final list = [1, 2, 3, 4]; 106 | expect(list.slice(start: 3, end: 1), []); 107 | expect(list.slice(start: -1, end: 1), []); 108 | expect(list.slice(start: 3, end: -3), []); 109 | expect(list.slice(start: -1, end: -3), []); 110 | expect(list.slice(start: 3, end: -100), []); 111 | }); 112 | 113 | test( 114 | 'returns an empty list if start (or its equivalent index) is less than ' 115 | 'end (or its equivalent index) and step is negative', () { 116 | final list = [1, 2, 3, 4]; 117 | expect(list.slice(start: 1, end: 3, step: -1), []); 118 | expect(list.slice(start: -3, end: 3, step: -1), []); 119 | expect(list.slice(start: 1, end: -1, step: -1), []); 120 | expect(list.slice(start: -3, end: -1, step: -1), []); 121 | expect(list.slice(start: 1, end: 100, step: -1), []); 122 | }); 123 | 124 | test('behaves predictably when the bounds are increased in any direction', 125 | () { 126 | final list = [1, 2, 3, 4]; 127 | 128 | expect(list.slice(start: 0, end: 0), []); 129 | expect(list.slice(start: 0, end: 1), [1]); 130 | expect(list.slice(start: 0, end: 2), [1, 2]); 131 | expect(list.slice(start: 0, end: 3), [1, 2, 3]); 132 | expect(list.slice(start: 0, end: 4), [1, 2, 3, 4]); 133 | expect(list.slice(start: 0, end: 5), [1, 2, 3, 4]); 134 | 135 | expect(list.slice(start: 0, end: -5), []); 136 | expect(list.slice(start: 0, end: -4), []); 137 | expect(list.slice(start: 0, end: -3), [1]); 138 | expect(list.slice(start: 0, end: -2), [1, 2]); 139 | expect(list.slice(start: 0, end: -1), [1, 2, 3]); 140 | 141 | expect(list.slice(start: 5, end: 4), []); 142 | expect(list.slice(start: 4, end: 4), []); 143 | expect(list.slice(start: 3, end: 4), [4]); 144 | expect(list.slice(start: 2, end: 4), [3, 4]); 145 | expect(list.slice(start: 1, end: 4), [2, 3, 4]); 146 | 147 | expect(list.slice(start: -1, end: 4), [4]); 148 | expect(list.slice(start: -2, end: 4), [3, 4]); 149 | expect(list.slice(start: -3, end: 4), [2, 3, 4]); 150 | expect(list.slice(start: -4, end: 4), [1, 2, 3, 4]); 151 | expect(list.slice(start: -5, end: 4), [1, 2, 3, 4]); 152 | 153 | expect(list.slice(start: 3, end: 3, step: -1), []); 154 | expect(list.slice(start: 3, end: 2, step: -1), [4]); 155 | expect(list.slice(start: 3, end: 1, step: -1), [4, 3]); 156 | expect(list.slice(start: 3, end: 0, step: -1), [4, 3, 2]); 157 | 158 | expect(list.slice(start: 3, end: -1, step: -1), []); 159 | expect(list.slice(start: 3, end: -2, step: -1), [4]); 160 | expect(list.slice(start: 3, end: -3, step: -1), [4, 3]); 161 | expect(list.slice(start: 3, end: -4, step: -1), [4, 3, 2]); 162 | expect(list.slice(start: 3, end: -5, step: -1), [4, 3, 2, 1]); 163 | 164 | expect(list.slice(start: 0, end: 0, step: -1), []); 165 | expect(list.slice(start: 1, end: 0, step: -1), [2]); 166 | expect(list.slice(start: 2, end: 0, step: -1), [3, 2]); 167 | expect(list.slice(start: 3, end: 0, step: -1), [4, 3, 2]); 168 | expect(list.slice(start: 4, end: 0, step: -1), [4, 3, 2]); 169 | expect(list.slice(start: 5, end: 0, step: -1), [4, 3, 2]); 170 | 171 | expect(list.slice(start: -1, end: 0, step: -1), [4, 3, 2]); 172 | expect(list.slice(start: -2, end: 0, step: -1), [3, 2]); 173 | expect(list.slice(start: -3, end: 0, step: -1), [2]); 174 | expect(list.slice(start: -4, end: 0, step: -1), []); 175 | 176 | expect(list.slice(start: -1, end: -5, step: -1), [4, 3, 2, 1]); 177 | expect(list.slice(start: -2, end: -5, step: -1), [3, 2, 1]); 178 | expect(list.slice(start: -3, end: -5, step: -1), [2, 1]); 179 | expect(list.slice(start: -4, end: -5, step: -1), [1]); 180 | expect(list.slice(start: -5, end: -5, step: -1), []); 181 | }); 182 | }); 183 | 184 | group('sortedCopy', () { 185 | test('copies and sorts the list', () { 186 | final original = [2, 3, 1]; 187 | final sortedCopy = original.sortedCopy(); 188 | expect(original, [2, 3, 1]); 189 | expect(sortedCopy, [1, 2, 3]); 190 | }); 191 | }); 192 | 193 | group('sortBy', () { 194 | test('sorts by provided sort key', () { 195 | final listA = [3, 222, 11]; 196 | final listB = [3, 222, 11]; 197 | 198 | listA.sortBy((e) => e.toString().length); 199 | listB.sortBy((e) => e.toString()); 200 | 201 | expect(listA, [3, 11, 222]); 202 | expect(listB, [11, 222, 3]); 203 | }); 204 | 205 | test('does not calculate any sort key more than once', () { 206 | final values = [3, 222, 3, 15, 18]; 207 | 208 | final callCounts = {}; 209 | int recordCallAndReturn(int e) { 210 | callCounts.update(e, (value) => value++, ifAbsent: () => 1); 211 | return e; 212 | } 213 | 214 | values.sortBy((e) => recordCallAndReturn(e)); 215 | 216 | expect(values, [3, 3, 15, 18, 222]); 217 | expect(callCounts.length, values.toSet().length); 218 | expect(callCounts.keys.toSet(), values.toSet()); 219 | expect(callCounts.values.every((e) => e == 1), isTrue); 220 | }); 221 | }); 222 | 223 | group('sortedCopyBy', () { 224 | test('copies and sorts the list by provided sort key', () { 225 | final original = [3, 222, 11]; 226 | final sortedCopy = original.sortedCopyBy((e) => e.toString()); 227 | expect(original, [3, 222, 11]); 228 | expect(sortedCopy, [11, 222, 3]); 229 | }); 230 | 231 | test('does not calculate any sort key more than once', () { 232 | final original = [3, 222, 3, 15, 18]; 233 | 234 | final callCounts = {}; 235 | int recordCallAndReturn(int e) { 236 | callCounts.update(e, (value) => value++, ifAbsent: () => 1); 237 | return e; 238 | } 239 | 240 | final sortedCopy = original.sortedCopyBy((e) => recordCallAndReturn(e)); 241 | 242 | expect(sortedCopy, [3, 3, 15, 18, 222]); 243 | expect(callCounts.length, original.toSet().length); 244 | expect(callCounts.keys.toSet(), original.toSet()); 245 | expect(callCounts.values.every((e) => e == 1), isTrue); 246 | }); 247 | }); 248 | 249 | group('takeRandom', () { 250 | test('returns null for an empty list', () { 251 | expect([].takeRandom(), isNull); 252 | }); 253 | 254 | test('removes the only element of a list of length 1', () { 255 | final list = ['a']; 256 | expect(list.takeRandom(), 'a'); 257 | expect(list, isEmpty); 258 | }); 259 | 260 | test('removes a fixed element when a seed is provided', () { 261 | final list = ['a', 'b', 'c', 'd']; 262 | expect(list.takeRandom(seed: 45), 'c'); 263 | expect(list, ['a', 'b', 'd']); 264 | }); 265 | }); 266 | } 267 | -------------------------------------------------------------------------------- /test/map_basics_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019, Google Inc. Please see the AUTHORS file for details. 2 | // All rights reserved. Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | import 'package:basics/basics.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | void main() { 9 | group('get', () { 10 | test('returns the value for a found key', () { 11 | final map = {'a': 1, 'b': 2, 'c': null}; 12 | expect(map.get('a'), 1); 13 | expect(map.get('a', defaultValue: 0), 1); 14 | 15 | expect(map.get('c'), null); 16 | expect(map.get('c', defaultValue: 0), null); 17 | }); 18 | 19 | test('returns the default value if no key found', () { 20 | final map = {'a': 1, 'b': 2, 'c': null}; 21 | expect(map.get('d'), null); 22 | expect(map.get('d', defaultValue: 0), 0); 23 | }); 24 | }); 25 | 26 | group('whereKey', () { 27 | test('returns all entries with key satisfying test', () { 28 | final map = {'a': 1, 'bb': 2, 'ccc': 3}; 29 | expect(map.whereKey((key) => key.length > 1), {'bb': 2, 'ccc': 3}); 30 | }); 31 | }); 32 | 33 | group('whereValue', () { 34 | test('returns all entries with value satisfying test', () { 35 | final map = {'a': 1, 'b': 2, 'c': 3}; 36 | expect(map.whereValue((value) => value > 1), {'b': 2, 'c': 3}); 37 | }); 38 | }); 39 | 40 | group('invert', () { 41 | test('inverts each entry', () { 42 | final map = {'a': 1, 'b': 2, 'c': 3}; 43 | expect(map.invert(), {1: 'a', 2: 'b', 3: 'c'}); 44 | }); 45 | 46 | test('works with duplicate values', () { 47 | final map = {'a': 1, 'b': 2, 'c': 2}; 48 | final inverted = map.invert(); 49 | 50 | expect(inverted.entries.length, 2); 51 | expect(inverted[1], 'a'); 52 | // Value for key 2 is dependent on iteration order and cannot be 53 | // guaranteed. 54 | expect(inverted.containsKey(2), isTrue); 55 | }); 56 | 57 | test('works with null values', () { 58 | final map = {'a': 1, 'b': null}; 59 | expect(map.invert(), {1: 'a', null: 'b'}); 60 | }); 61 | 62 | test('returns an empty map when called on an empty map', () { 63 | final map = {}; 64 | expect(map.invert(), {}); 65 | }); 66 | }); 67 | } 68 | -------------------------------------------------------------------------------- /test/set_basics_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019, Google Inc. Please see the AUTHORS file for details. 2 | // All rights reserved. Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | import 'package:basics/basics.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | void main() { 9 | group('isEqualTo', () { 10 | test('returns true if both sets contain the same elements', () { 11 | final values = {'a', 'b', 'c'}; 12 | expect(values.isEqualTo({'c', 'b', 'a'}), isTrue); 13 | }); 14 | 15 | test('returns false if this set is a strict subset of other set', () { 16 | final values = {'a', 'b', 'c'}; 17 | expect(values.isEqualTo({'c', 'b', 'a', 'd'}), isFalse); 18 | }); 19 | 20 | test('returns false if this set is a strict superset of other set', () { 21 | final values = {'a', 'b', 'c'}; 22 | expect(values.isEqualTo({'c', 'b'}), isFalse); 23 | }); 24 | 25 | test('returns false if other set contains elements not in this set', () { 26 | final values = {'a', 'b', 'c'}; 27 | expect(values.isEqualTo({'c', 'b', 'f'}), isFalse); 28 | }); 29 | 30 | test('returns false if this set is empty and other set is not', () { 31 | final values = {}; 32 | expect(values.isEqualTo({'c', 'b', 'a'}), isFalse); 33 | }); 34 | 35 | test('returns false if other set is empty and this set is not', () { 36 | final values = {'a', 'b', 'c'}; 37 | expect(values.isEqualTo({}), isFalse); 38 | }); 39 | 40 | test('returns true if both sets are empty', () { 41 | final values = {}; 42 | expect(values.isEqualTo({}), isTrue); 43 | }); 44 | }); 45 | 46 | group('isDisjointWith', () { 47 | test('returns true if both sets have no elements in common', () { 48 | final values = {'a', 'b', 'c'}; 49 | expect(values.isDisjointWith({'d', 'e', 'f'}), isTrue); 50 | }); 51 | 52 | test('returns false if both sets have any elements in common', () { 53 | final values = {'a', 'b', 'c'}; 54 | expect(values.isDisjointWith({'d', 'e', 'c'}), isFalse); 55 | }); 56 | 57 | test('returns true if either set or both sets are empty', () { 58 | final empty = {}; 59 | final nonEmpty = {'a', 'b', 'c'}; 60 | expect(empty.isDisjointWith(nonEmpty), isTrue); 61 | expect(nonEmpty.isDisjointWith(empty), isTrue); 62 | expect(empty.isDisjointWith({}), isTrue); 63 | }); 64 | }); 65 | 66 | group('isIntersectingWith', () { 67 | test('returns true if both sets have any elements in common', () { 68 | final values = {'a', 'b', 'c'}; 69 | expect(values.isIntersectingWith({'d', 'e', 'c'}), isTrue); 70 | }); 71 | 72 | test('returns false if both sets have no elements in common', () { 73 | final values = {'a', 'b', 'c'}; 74 | expect(values.isIntersectingWith({'d', 'e', 'f'}), isFalse); 75 | }); 76 | 77 | test('returns false if either set or both sets are empty', () { 78 | final empty = {}; 79 | final nonEmpty = {'a', 'b', 'c'}; 80 | expect(empty.isIntersectingWith(nonEmpty), isFalse); 81 | expect(nonEmpty.isIntersectingWith(empty), isFalse); 82 | expect(empty.isIntersectingWith({}), isFalse); 83 | }); 84 | }); 85 | 86 | group('isSubsetOf', () { 87 | test('returns true if every element of this set is contained in other set', 88 | () { 89 | final values = {'a', 'b', 'c'}; 90 | expect(values.isSubsetOf({'a', 'b', 'c', 'd'}), isTrue); 91 | }); 92 | 93 | test('returns true if both sets are equal', () { 94 | final values = {'a', 'b', 'c'}; 95 | expect(values.isSubsetOf({'a', 'b', 'c'}), isTrue); 96 | }); 97 | 98 | test( 99 | 'returns false if any element of this set is not contained in other ' 100 | 'set', () { 101 | final values = {'a', 'b', 'c'}; 102 | expect(values.isSubsetOf({'a', 'b', 'f'}), isFalse); 103 | }); 104 | 105 | test('returns true if this set is empty and other set is not', () { 106 | final values = {}; 107 | expect(values.isSubsetOf({'a', 'b', 'f'}), isTrue); 108 | }); 109 | 110 | test('returns false if other set is empty and this set is not', () { 111 | final values = {'a', 'b', 'c'}; 112 | expect(values.isSubsetOf({}), isFalse); 113 | }); 114 | 115 | test('returns true if both sets are empty', () { 116 | final values = {}; 117 | expect(values.isSubsetOf({}), isTrue); 118 | }); 119 | }); 120 | 121 | group('isSupersetOf', () { 122 | test('returns true if every element of other set is contained in this set', 123 | () { 124 | final values = {'a', 'b', 'c'}; 125 | expect(values.isSupersetOf({'b', 'c'}), isTrue); 126 | }); 127 | 128 | test('returns true if both sets are equal', () { 129 | final values = {'a', 'b', 'c'}; 130 | expect(values.isSupersetOf({'a', 'b', 'c'}), isTrue); 131 | }); 132 | 133 | test( 134 | 'returns false if any element of other set is not contained in this ' 135 | 'set', () { 136 | final values = {'a', 'b', 'c'}; 137 | expect(values.isSupersetOf({'a', 'b', 'f'}), isFalse); 138 | }); 139 | 140 | test('returns false if this set is empty and other set is not', () { 141 | final values = {}; 142 | expect(values.isSupersetOf({'a', 'b', 'f'}), isFalse); 143 | }); 144 | 145 | test('returns true if other set is empty and this set is not', () { 146 | final values = {'a', 'b', 'c'}; 147 | expect(values.isSupersetOf({}), isTrue); 148 | }); 149 | 150 | test('returns true if both sets are empty', () { 151 | final values = {}; 152 | expect(values.isSupersetOf({}), isTrue); 153 | }); 154 | }); 155 | 156 | group('isStrictSubsetOf', () { 157 | test('returns true if every element of this set is contained in other set', 158 | () { 159 | final values = {'a', 'b', 'c'}; 160 | expect(values.isStrictSubsetOf({'a', 'b', 'c', 'd'}), isTrue); 161 | }); 162 | 163 | test('returns false if both sets are equal', () { 164 | final values = {'a', 'b', 'c'}; 165 | expect(values.isStrictSubsetOf({'a', 'b', 'c'}), isFalse); 166 | }); 167 | 168 | test( 169 | 'returns false if any element of this set is not contained in other ' 170 | 'set', () { 171 | final values = {'a', 'b', 'c'}; 172 | expect(values.isStrictSubsetOf({'a', 'b', 'f'}), isFalse); 173 | }); 174 | 175 | test('returns true if this set is empty and other set is not', () { 176 | final values = {}; 177 | expect(values.isStrictSubsetOf({'a', 'b', 'f'}), isTrue); 178 | }); 179 | 180 | test('returns false if other set is empty and this set is not', () { 181 | final values = {'a', 'b', 'c'}; 182 | expect(values.isStrictSubsetOf({}), isFalse); 183 | }); 184 | 185 | test('returns false if both sets are empty', () { 186 | final values = {}; 187 | expect(values.isStrictSubsetOf({}), isFalse); 188 | }); 189 | }); 190 | 191 | group('isStrictSupersetOf', () { 192 | test('returns true if every element of other set is contained in this set', 193 | () { 194 | final values = {'a', 'b', 'c'}; 195 | expect(values.isStrictSupersetOf({'b', 'c'}), isTrue); 196 | }); 197 | 198 | test('returns false if both sets are equal', () { 199 | final values = {'a', 'b', 'c'}; 200 | expect(values.isStrictSupersetOf({'a', 'b', 'c'}), isFalse); 201 | }); 202 | 203 | test( 204 | 'returns false if any element of other set is not contained in this ' 205 | 'set', () { 206 | final values = {'a', 'b', 'c'}; 207 | expect(values.isStrictSupersetOf({'a', 'b', 'f'}), isFalse); 208 | }); 209 | 210 | test('returns false if this set is empty and other set is not', () { 211 | final values = {}; 212 | expect(values.isStrictSupersetOf({'a', 'b', 'f'}), isFalse); 213 | }); 214 | 215 | test('returns true if other set is empty and this set is not', () { 216 | final values = {'a', 'b', 'c'}; 217 | expect(values.isStrictSupersetOf({}), isTrue); 218 | }); 219 | 220 | test('returns false if both sets are empty', () { 221 | final values = {}; 222 | expect(values.isStrictSupersetOf({}), isFalse); 223 | }); 224 | }); 225 | 226 | group('takeRandom', () { 227 | test('returns null for an empty set', () { 228 | expect({}.takeRandom(), isNull); 229 | }); 230 | 231 | test('removes the only element of a set of length 1', () { 232 | final values = {'a'}; 233 | expect(values.takeRandom(), 'a'); 234 | expect(values, isEmpty); 235 | }); 236 | 237 | test('removes a fixed element when a seed is provided', () { 238 | final values = {'a', 'b', 'c', 'd'}; 239 | expect(values.takeRandom(seed: 45), 'c'); 240 | expect(values, {'a', 'b', 'd'}); 241 | }); 242 | }); 243 | 244 | group('classify', () { 245 | test('groups values by provided classifier', () { 246 | final values = {'aaa', 'bbb', 'cc', 'a', 'bb'}; 247 | final groups = values.classify((e) => e.length); 248 | 249 | expect(groups.entries.length, 3); 250 | expect(groups[1], {'a'}); 251 | expect(groups[2], {'cc', 'bb'}); 252 | expect(groups[3], {'aaa', 'bbb'}); 253 | }); 254 | 255 | test('returns an empty map if called on an empty set', () { 256 | final values = {}; 257 | expect(values.classify((e) => e.length), >{}); 258 | }); 259 | }); 260 | } 261 | -------------------------------------------------------------------------------- /test/string_basics_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019, Google Inc. Please see the AUTHORS file for details. 2 | // All rights reserved. Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | import 'package:basics/basics.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | void main() { 9 | group('compareToIgnoringCase', () { 10 | test('compares strings ignoring case', () { 11 | final string = 'ABC'; 12 | expect(string.compareToIgnoringCase('abd') < 0, isTrue); 13 | expect(string.compareToIgnoringCase('abc'), 0); 14 | expect(string.compareToIgnoringCase('abb') > 0, isTrue); 15 | }); 16 | }); 17 | 18 | group('withoutPrefix', () { 19 | test('returns string without prefix if prefix is present', () { 20 | final string = 'abc'; 21 | expect(string.withoutPrefix('ab'), 'c'); 22 | }); 23 | 24 | test('returns original string if prefix is not present', () { 25 | final string = 'abc'; 26 | expect(string.withoutPrefix('z'), 'abc'); 27 | }); 28 | 29 | test('works with a regex', () { 30 | final string = 'aaaaaaaaabc'; 31 | expect(string.withoutPrefix(RegExp(r'a+')), 'bc'); 32 | }); 33 | 34 | test('ignores matches not at the start of the string', () { 35 | final inMiddle = 'baaaaaaaaaaac'; 36 | final atEnd = 'bcaaaaaaaaaaa'; 37 | expect(inMiddle.withoutPrefix(RegExp(r'a+')), inMiddle); 38 | expect(atEnd.withoutPrefix(RegExp(r'a+')), atEnd); 39 | }); 40 | }); 41 | 42 | group('withoutSuffix', () { 43 | test('returns string without suffix if suffix is present', () { 44 | final string = 'abc'; 45 | expect(string.withoutSuffix('c'), 'ab'); 46 | }); 47 | 48 | test('returns original string if suffix is not present', () { 49 | final string = 'abc'; 50 | expect(string.withoutSuffix('z'), 'abc'); 51 | }); 52 | 53 | test('works with a regex', () { 54 | final string = 'abccccccccccccccc'; 55 | expect(string.withoutSuffix(RegExp(r'c+')), 'ab'); 56 | }); 57 | 58 | test('ignores matches not at the end of the string', () { 59 | final atStart = 'aaaaaaaaaaaaabc'; 60 | final inMiddle = 'baaaaaaaaaaac'; 61 | expect(atStart.withoutSuffix(RegExp(r'a+')), atStart); 62 | expect(inMiddle.withoutSuffix(RegExp(r'a+')), inMiddle); 63 | }); 64 | }); 65 | 66 | group('insert', () { 67 | test('inserts provided string at provided index', () { 68 | final string = 'word'; 69 | expect(string.insert('s', 0), 'sword'); 70 | expect(string.insert('ke', 3), 'worked'); 71 | expect(string.insert('y', 4), 'wordy'); 72 | }); 73 | 74 | test('works when called on an empty string', () { 75 | expect(''.insert('word', 0), 'word'); 76 | }); 77 | 78 | test('works when provided an empty string', () { 79 | expect('word'.insert('', 1), 'word'); 80 | }); 81 | }); 82 | 83 | group('prepend', () { 84 | test('prepends other string to this string', () { 85 | final string = 'word'; 86 | expect(string.prepend('key'), 'keyword'); 87 | }); 88 | }); 89 | 90 | group('partition', () { 91 | test('partitions a string around the first occurrence of pattern', () { 92 | final string = 'word'; 93 | expect(string.partition('or'), ['w', 'or', 'd']); 94 | }); 95 | 96 | test('works when string starts with pattern', () { 97 | final string = 'word'; 98 | expect(string.partition('wo'), ['', 'wo', 'rd']); 99 | }); 100 | 101 | test('works when string ends with pattern', () { 102 | final string = 'word'; 103 | expect(string.partition('rd'), ['wo', 'rd', '']); 104 | }); 105 | 106 | test( 107 | 'treats whole string as occurring before pattern if pattern is not ' 108 | 'found', () { 109 | final string = 'word'; 110 | expect(string.partition('z'), ['word', '', '']); 111 | }); 112 | 113 | test('works with a regex', () { 114 | final string = 'wooooooord'; 115 | expect(string.partition(RegExp(r'o+r')), ['w', 'ooooooor', 'd']); 116 | }); 117 | }); 118 | 119 | group('slice', () { 120 | test('returns the characters from start inclusive', () { 121 | final string = 'word'; 122 | expect(string.slice(start: 1), 'ord'); 123 | expect(string.slice(start: 2, step: -2), 'rw'); 124 | }); 125 | 126 | test('returns the characters until end exclusive', () { 127 | final string = 'stuff'; 128 | expect(string.slice(end: 2), 'st'); 129 | expect(string.slice(end: 1, step: -1), 'ffu'); 130 | }); 131 | 132 | test('returns the characters between start and end', () { 133 | final string = 'word'; 134 | expect(string.slice(start: 1, end: 3), 'or'); 135 | }); 136 | 137 | test('skips characters by step', () { 138 | final string = 'word'; 139 | expect(string.slice(step: 2), 'wr'); 140 | }); 141 | 142 | test('returns the characters between start and end by step', () { 143 | final string = 'word'; 144 | expect(string.slice(start: 1, end: 4, step: 2), 'od'); 145 | }); 146 | 147 | test( 148 | 'accepts steps that do not evenly divide the total number of ' 149 | 'characters', () { 150 | final string = 'word'; 151 | expect(string.slice(step: 3), 'wd'); 152 | }); 153 | 154 | test('returns characters in reverse order when step is negative', () { 155 | final string = 'hat'; 156 | expect(string.slice(step: -1), 'tah'); 157 | }); 158 | 159 | test('counts a negative start index from the end of the string', () { 160 | final string = 'word'; 161 | expect(string.slice(start: -2), 'rd'); 162 | }); 163 | 164 | test('counts a negative end index from the end of the string', () { 165 | final string = 'stuff'; 166 | expect(string.slice(end: -2), 'stu'); 167 | }); 168 | 169 | test( 170 | 'starts from the final character if start is greater than the length ' 171 | 'of the string', () { 172 | final string = 'word'; 173 | expect(string.slice(start: 100, end: 1), ''); 174 | expect(string.slice(start: 100, end: 1, step: -1), 'dr'); 175 | }); 176 | 177 | test( 178 | 'ends with the final character if end is greater than the length of ' 179 | 'the string', () { 180 | final string = 'stuff'; 181 | expect(string.slice(start: 2, end: 100), 'uff'); 182 | }); 183 | 184 | test( 185 | 'starts from the first character if start is less than the negative ' 186 | 'length of the string', () { 187 | final string = 'hat'; 188 | expect(string.slice(start: -100, end: 3), 'hat'); 189 | }); 190 | 191 | test( 192 | 'ends with the first character if end is less than the negative length ' 193 | 'of the string', () { 194 | final string = 'word'; 195 | expect(string.slice(start: 2, end: -100), ''); 196 | expect(string.slice(start: 2, end: -100, step: -1), 'row'); 197 | }); 198 | 199 | test('returns an empty string if start and end are equal', () { 200 | final string = 'stuff'; 201 | expect(string.slice(start: 2, end: 2), ''); 202 | expect(string.slice(start: -2, end: -2), ''); 203 | }); 204 | 205 | test( 206 | 'returns an empty string if start and end are not equal but correspond ' 207 | 'to the same index', () { 208 | final string = 'word'; 209 | expect(string.slice(start: 1, end: -3), ''); 210 | expect(string.slice(start: 2, end: -2), ''); 211 | }); 212 | 213 | test( 214 | 'returns an empty string if start (or its equivalent index) is greater ' 215 | 'than end (or its equivalent index) and step is positive', () { 216 | final string = 'word'; 217 | expect(string.slice(start: 3, end: 1), ''); 218 | expect(string.slice(start: -1, end: 1), ''); 219 | expect(string.slice(start: 3, end: -3), ''); 220 | expect(string.slice(start: -1, end: -3), ''); 221 | expect(string.slice(start: 3, end: -100), ''); 222 | }); 223 | 224 | test( 225 | 'returns an empty string if start (or its equivalent index) is less ' 226 | 'than end (or its equivalent index) and step is negative', () { 227 | final string = 'word'; 228 | expect(string.slice(start: 1, end: 3, step: -1), ''); 229 | expect(string.slice(start: -3, end: 3, step: -1), ''); 230 | expect(string.slice(start: 1, end: -1, step: -1), ''); 231 | expect(string.slice(start: -3, end: -1, step: -1), ''); 232 | expect(string.slice(start: 1, end: 100, step: -1), ''); 233 | }); 234 | 235 | test('behaves predictably when the bounds are increased in any direction', 236 | () { 237 | final string = 'word'; 238 | 239 | expect(string.slice(start: 0, end: 0), ''); 240 | expect(string.slice(start: 0, end: 1), 'w'); 241 | expect(string.slice(start: 0, end: 2), 'wo'); 242 | expect(string.slice(start: 0, end: 3), 'wor'); 243 | expect(string.slice(start: 0, end: 4), 'word'); 244 | expect(string.slice(start: 0, end: 5), 'word'); 245 | 246 | expect(string.slice(start: 0, end: -5), ''); 247 | expect(string.slice(start: 0, end: -4), ''); 248 | expect(string.slice(start: 0, end: -3), 'w'); 249 | expect(string.slice(start: 0, end: -2), 'wo'); 250 | expect(string.slice(start: 0, end: -1), 'wor'); 251 | 252 | expect(string.slice(start: 5, end: 4), ''); 253 | expect(string.slice(start: 4, end: 4), ''); 254 | expect(string.slice(start: 3, end: 4), 'd'); 255 | expect(string.slice(start: 2, end: 4), 'rd'); 256 | expect(string.slice(start: 1, end: 4), 'ord'); 257 | 258 | expect(string.slice(start: -1, end: 4), 'd'); 259 | expect(string.slice(start: -2, end: 4), 'rd'); 260 | expect(string.slice(start: -3, end: 4), 'ord'); 261 | expect(string.slice(start: -4, end: 4), 'word'); 262 | expect(string.slice(start: -5, end: 4), 'word'); 263 | 264 | expect(string.slice(start: 3, end: 3, step: -1), ''); 265 | expect(string.slice(start: 3, end: 2, step: -1), 'd'); 266 | expect(string.slice(start: 3, end: 1, step: -1), 'dr'); 267 | expect(string.slice(start: 3, end: 0, step: -1), 'dro'); 268 | 269 | expect(string.slice(start: 3, end: -1, step: -1), ''); 270 | expect(string.slice(start: 3, end: -2, step: -1), 'd'); 271 | expect(string.slice(start: 3, end: -3, step: -1), 'dr'); 272 | expect(string.slice(start: 3, end: -4, step: -1), 'dro'); 273 | expect(string.slice(start: 3, end: -5, step: -1), 'drow'); 274 | 275 | expect(string.slice(start: 0, end: 0, step: -1), ''); 276 | expect(string.slice(start: 1, end: 0, step: -1), 'o'); 277 | expect(string.slice(start: 2, end: 0, step: -1), 'ro'); 278 | expect(string.slice(start: 3, end: 0, step: -1), 'dro'); 279 | expect(string.slice(start: 4, end: 0, step: -1), 'dro'); 280 | expect(string.slice(start: 5, end: 0, step: -1), 'dro'); 281 | 282 | expect(string.slice(start: -1, end: 0, step: -1), 'dro'); 283 | expect(string.slice(start: -2, end: 0, step: -1), 'ro'); 284 | expect(string.slice(start: -3, end: 0, step: -1), 'o'); 285 | expect(string.slice(start: -4, end: 0, step: -1), ''); 286 | 287 | expect(string.slice(start: -1, end: -5, step: -1), 'drow'); 288 | expect(string.slice(start: -2, end: -5, step: -1), 'row'); 289 | expect(string.slice(start: -3, end: -5, step: -1), 'ow'); 290 | expect(string.slice(start: -4, end: -5, step: -1), 'w'); 291 | expect(string.slice(start: -5, end: -5, step: -1), ''); 292 | }); 293 | }); 294 | 295 | group('reverse', () { 296 | test('reverses a string', () { 297 | final string = 'word'; 298 | expect(string.reverse(), 'drow'); 299 | }); 300 | 301 | test('returns an empty string if called on an empty string', () { 302 | expect(''.reverse(), ''); 303 | }); 304 | }); 305 | 306 | group('isBlank', () { 307 | test('returns true if a string is blank', () { 308 | expect(''.isBlank, isTrue); 309 | }); 310 | 311 | test('returns false if a string is not blank', () { 312 | expect('a'.isBlank, isFalse); 313 | }); 314 | }); 315 | 316 | group('isNotBlank', () { 317 | test('returns false if a string is blank', () { 318 | expect(''.isNotBlank, isFalse); 319 | }); 320 | 321 | test('returns true if a string is not blank', () { 322 | expect('a'.isNotBlank, isTrue); 323 | }); 324 | }); 325 | 326 | group('isNullOrBlank', () { 327 | test('returns true if a string is null', () { 328 | final String? string = null; 329 | expect(string.isNullOrBlank, isTrue); 330 | }); 331 | 332 | test('returns true if a string is blank', () { 333 | expect(''.isNullOrBlank, isTrue); 334 | }); 335 | 336 | test('returns false if a string is not blank', () { 337 | expect('a'.isNullOrBlank, isFalse); 338 | }); 339 | }); 340 | 341 | group('isNotNullOrBlank', () { 342 | test('returns false if a string is null', () { 343 | final String? string = null; 344 | expect(string.isNotNullOrBlank, isFalse); 345 | }); 346 | 347 | test('returns false if a string is blank', () { 348 | expect(''.isNotNullOrBlank, isFalse); 349 | }); 350 | 351 | test('returns true if a string is not blank', () { 352 | expect('a'.isNotNullOrBlank, isTrue); 353 | }); 354 | }); 355 | 356 | group('truncate', () { 357 | test( 358 | 'returns a truncated string that has the length' 359 | 'based on the limit provided', () { 360 | final sentence = 'The quick brown fox jumps over the lazy dog'; 361 | expect(sentence.truncate(20), 'The quick brown fox'); 362 | }); 363 | 364 | test( 365 | 'returns the same string if the length of the string' 366 | 'is less than provided limit', () { 367 | final sentence = 'The quick brown fox'; 368 | expect(sentence.truncate(20), 'The quick brown fox'); 369 | }); 370 | 371 | test( 372 | 'returns a truncated string that has the length based on the length' 373 | 'provided without trimming the spaces at the end', () { 374 | final sentence = 'The quick brown fox jumps over the lazy dog'; 375 | expect(sentence.truncate(20, trimTrailingWhitespace: false), 376 | 'The quick brown fox '); 377 | }); 378 | 379 | test( 380 | 'returns a truncated string that has the length based on the length' 381 | 'provided with a custom substitution string', () { 382 | final sentence = 'The quick brown fox jumps over the lazy dog'; 383 | expect(sentence.truncate(20, substitution: ' (...)'), 384 | 'The quick brown fox (...)'); 385 | expect( 386 | sentence.truncate(20, substitution: '...'), 'The quick brown fox...'); 387 | }); 388 | 389 | test( 390 | 'returns a truncated string that has the length based on the length' 391 | 'provided with a custom ending string but the substitution length will be included', 392 | () { 393 | final sentence = 'The quick brown fox jumps over the lazy dog'; 394 | expect( 395 | sentence.truncate( 396 | 12, 397 | substitution: '...', 398 | includeSubstitutionInLength: true, 399 | ), 400 | 'The quick...', 401 | ); 402 | }); 403 | 404 | test( 405 | 'returns a truncated string with emojis that has the length' 406 | 'based on the length provided', () { 407 | final sentence = 'The quick brown 🦊🦊🦊 jumps over the lazy 🐶🐶🐶'; 408 | 409 | expect( 410 | sentence.truncate(42), 411 | 'The quick brown 🦊🦊🦊 jumps over the lazy 🐶🐶', 412 | ); 413 | 414 | expect( 415 | sentence.truncate(42, substitution: '🐾🐾🐾'), 416 | 'The quick brown 🦊🦊🦊 jumps over the lazy 🐶🐶🐾🐾🐾', 417 | ); 418 | 419 | expect( 420 | sentence.truncate( 421 | 18, 422 | substitution: '...', 423 | includeSubstitutionInLength: true, 424 | ), 425 | 'The quick brown...', 426 | ); 427 | 428 | expect( 429 | sentence.truncate( 430 | 18, 431 | substitution: '😀', 432 | includeSubstitutionInLength: true, 433 | ), 434 | 'The quick brown 🦊😀', 435 | ); 436 | }); 437 | }); 438 | 439 | group('capitalize', () { 440 | test('returns a new string with the first character in upper case', () { 441 | expect('foo'.capitalize(), 'Foo'); 442 | expect('hello World'.capitalize(), 'Hello World'); 443 | }); 444 | 445 | test( 446 | 'returns a new string with the first non-ASCII unicode character' 447 | 'or accented characters in upper case', () { 448 | expect('éfoo'.capitalize(), 'Éfoo'); 449 | }); 450 | 451 | test( 452 | 'returns same string if the first character is not an' 453 | 'alphabet character', () { 454 | expect('1bravo'.capitalize(), '1bravo'); 455 | expect('2nd'.capitalize(), '2nd'); 456 | expect('ßfoo'.capitalize(), 'ßfoo'); 457 | expect('文foo'.capitalize(), '文foo'); 458 | }); 459 | 460 | test('returns empty if the string is empty', () { 461 | expect(''.capitalize(), ''); 462 | }); 463 | }); 464 | } 465 | --------------------------------------------------------------------------------