Add extension methods to package:collection. (#135)

Add a number of useful extension methods to `List` and `Iterable`, and to a few other types.

A good part of these extensions are backed by the algorithms of `algorithms.dart`, which is expanded to support them:

* Added `quickSort`.
* Add extra optional `Random` argument to `shuffle`.
* Generalize some of the functions in algorithms.dart to work on specific properties of the objects (`binarySearchBy`,
`lowerBoundBy`, `insertionSortBy`, `quickSortBy`, `mergeSortBy`).

The new methods are not exported from the library yet, they are intended to be used through the extension.
diff --git a/.travis.yml b/.travis.yml
index 82091ab..4e550df 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -20,7 +20,7 @@
     - stage: test
       name: "Vm Tests"
       os: linux
-      script: pub run --enable-experiment=non-nullable test -p vm 
+      script: pub run --enable-experiment=non-nullable test -p vm
     - stage: test
       name: "Web Tests"
       os: linux
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1695647..e99669f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,14 @@
 * Remove the unusable setter `UnionSetController.set=`. This was mistakenly
   added to the public API but could never be called.
 
+* Add extra optional `Random` argument to `shuffle`.
+
+* Add a large number of extension methods on `Iterable` and `List` types,
+  and on a few other types.
+  These either provide easy access to the operations from `algorithms.dart`,
+  or provide convenience variants of existing `Iterable` and `List` methods
+  like `singleWhereOrNull` or `forEachIndexed`.
+
 ## 1.15.0-nullsafety.3
 
 * Allow 2.10 stable and 2.11.0 dev SDK versions.
diff --git a/analysis_options.yaml b/analysis_options.yaml
index 5adddb1..d813ed9 100644
--- a/analysis_options.yaml
+++ b/analysis_options.yaml
@@ -1,4 +1,4 @@
-include: package:pedantic/analysis_options.yaml
+include: package:pedantic/analysis_options.1.9.0.yaml
 analyzer:
   errors:
     unused_element: error
@@ -7,39 +7,21 @@
     dead_code: error
   enable-experiment:
     - non-nullable
-
 linter:
   rules:
     # Errors
-    - avoid_empty_else
     - control_flow_in_finally
     - empty_statements
     - hash_and_equals
     - implementation_imports
     - test_types_in_equals
     - throw_in_finally
-    - unrelated_type_equality_checks
-    - valid_regexps
 
     # Style
-    - avoid_init_to_null
     - avoid_private_typedef_functions
     - avoid_renaming_method_parameters
-    - avoid_return_types_on_setters
     - await_only_futures
     - camel_case_types
     - directives_ordering
-    - empty_catches
-    - empty_constructor_bodies
-    - library_names
-    - library_prefixes
     - non_constant_identifier_names
     - only_throw_errors
-    - prefer_equal_for_default_values
-    - prefer_final_fields
-    - prefer_generic_function_type_aliases
-    - prefer_is_not_empty
-    - slash_for_doc_comments
-    - type_init_formals
-    - unnecessary_const
-    - unnecessary_new
diff --git a/lib/algorithms.dart b/lib/algorithms.dart
index df0d712..7054141 100644
--- a/lib/algorithms.dart
+++ b/lib/algorithms.dart
@@ -6,4 +6,5 @@
 @Deprecated('Will be removed in collection 2.0.0.')
 library dart.pkg.collection.algorithms;
 
-export 'src/algorithms.dart';
+export 'src/algorithms.dart'
+    show binarySearch, insertionSort, lowerBound, mergeSort, shuffle, reverse;
diff --git a/lib/collection.dart b/lib/collection.dart
index 282d60f..27a75c8 100644
--- a/lib/collection.dart
+++ b/lib/collection.dart
@@ -2,7 +2,8 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
-export 'src/algorithms.dart';
+export 'src/algorithms.dart'
+    show binarySearch, insertionSort, lowerBound, mergeSort, shuffle, reverse;
 export 'src/canonicalized_map.dart';
 export 'src/combined_wrappers/combined_iterable.dart';
 export 'src/combined_wrappers/combined_list.dart';
@@ -12,7 +13,9 @@
 export 'src/equality_map.dart';
 export 'src/equality_set.dart';
 export 'src/functions.dart';
+export 'src/iterable_extensions.dart';
 export 'src/iterable_zip.dart';
+export 'src/list_extensions.dart';
 export 'src/priority_queue.dart';
 export 'src/queue_list.dart';
 export 'src/union_set.dart';
diff --git a/lib/src/algorithms.dart b/lib/src/algorithms.dart
index 44a2b3b..d589e1e 100644
--- a/lib/src/algorithms.dart
+++ b/lib/src/algorithms.dart
@@ -2,7 +2,10 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
-import 'dart:math' as math;
+/// A selection of data manipulation algorithms.
+library pkg.collection.algorithms;
+
+import 'dart:math' show Random;
 
 import 'utils.dart';
 
@@ -12,19 +15,35 @@
 /// is unpredictable.
 ///
 /// If [compare] is omitted, this defaults to calling [Comparable.compareTo] on
-/// the objects. If any object is not [Comparable], this throws a [TypeError]
-/// (`CastError` on some SDK versions).
+/// the objects. In this case, the objects must be [Comparable].
+///
+/// Returns -1 if [value] is not in the list.
+int binarySearch<E>(List<E> sortedList, E value,
+    {int Function(E, E)? compare}) {
+  compare ??= defaultCompare;
+  return binarySearchBy<E, E>(sortedList, identity, compare, value);
+}
+
+/// Returns a position of the [value] in [sortedList], if it is there.
+///
+/// If the list isn't sorted according to the [compare] function on the [keyOf]
+/// property of the elements, the result is unpredictable.
 ///
 /// Returns -1 if [value] is not in the list by default.
-int binarySearch<T>(List<T> sortedList, T value,
-    {int Function(T, T)? compare}) {
-  compare ??= defaultCompare();
-  var min = 0;
-  var max = sortedList.length;
+///
+/// If [start] and [end] are supplied, only that range is searched,
+/// and only that range need to be sorted.
+int binarySearchBy<E, K>(List<E> sortedList, K Function(E element) keyOf,
+    int Function(K, K) compare, E value,
+    [int start = 0, int? end]) {
+  end = RangeError.checkValidRange(start, end, sortedList.length);
+  var min = start;
+  var max = end;
+  var key = keyOf(value);
   while (min < max) {
     var mid = min + ((max - min) >> 1);
     var element = sortedList[mid];
-    var comp = compare(element, value);
+    var comp = compare(keyOf(element), key);
     if (comp == 0) return mid;
     if (comp < 0) {
       min = mid + 1;
@@ -42,19 +61,36 @@
 /// is unpredictable.
 ///
 /// If [compare] is omitted, this defaults to calling [Comparable.compareTo] on
-/// the objects. If any object is not [Comparable], this throws a [TypeError]
-/// (`CastError` on some SDK versions).
+/// the objects. In this case, the objects must be [Comparable].
 ///
 /// Returns [sortedList.length] if all the items in [sortedList] compare less
 /// than [value].
-int lowerBound<T>(List<T> sortedList, T value, {int Function(T, T)? compare}) {
-  compare ??= defaultCompare();
-  var min = 0;
-  var max = sortedList.length;
+int lowerBound<E>(List<E> sortedList, E value, {int Function(E, E)? compare}) {
+  compare ??= defaultCompare;
+  return lowerBoundBy<E, E>(sortedList, identity, compare, value);
+}
+
+/// Returns the first position in [sortedList] that is not before [value].
+///
+/// Elements are compared using the [compare] function of the [keyOf] property of
+/// the elements.
+/// If the list isn't sorted according to this order, the result is unpredictable.
+///
+/// Returns [sortedList.length] if all the items in [sortedList] are before [value].
+///
+/// If [start] and [end] are supplied, only that range is searched,
+/// and only that range need to be sorted.
+int lowerBoundBy<E, K>(List<E> sortedList, K Function(E element) keyOf,
+    int Function(K, K) compare, E value,
+    [int start = 0, int? end]) {
+  end = RangeError.checkValidRange(start, end, sortedList.length);
+  var min = start;
+  var max = end;
+  var key = keyOf(value);
   while (min < max) {
     var mid = min + ((max - min) >> 1);
     var element = sortedList[mid];
-    var comp = compare(element, value);
+    var comp = compare(keyOf(element), key);
     if (comp < 0) {
       min = mid + 1;
     } else {
@@ -67,31 +103,36 @@
 /// Shuffles a list randomly.
 ///
 /// A sub-range of a list can be shuffled by providing [start] and [end].
-void shuffle(List list, [int start = 0, int? end]) {
-  var random = math.Random();
-  end ??= list.length;
+///
+/// If [start] or [end] are omitted,
+/// they default to the start and end of the list.
+///
+/// If [random] is omitted, it defaults to a new instance of [Random].
+void shuffle(List elements, [int start = 0, int? end, Random? random]) {
+  random ??= Random();
+  end ??= elements.length;
   var length = end - start;
   while (length > 1) {
     var pos = random.nextInt(length);
     length--;
-    var tmp1 = list[start + pos];
-    list[start + pos] = list[start + length];
-    list[start + length] = tmp1;
+    var tmp1 = elements[start + pos];
+    elements[start + pos] = elements[start + length];
+    elements[start + length] = tmp1;
   }
 }
 
 /// Reverses a list, or a part of a list, in-place.
-void reverse(List list, [int start = 0, int? end]) {
-  end ??= list.length;
-  _reverse(list, start, end);
+void reverse<E>(List<E> elements, [int start = 0, int? end]) {
+  end = RangeError.checkValidRange(start, end, elements.length);
+  _reverse<E>(elements, start, end);
 }
 
 /// Internal helper function that assumes valid arguments.
-void _reverse(List list, int start, int end) {
+void _reverse<E>(List<E> elements, int start, int end) {
   for (var i = start, j = end - 1; i < j; i++, j--) {
-    var tmp = list[i];
-    list[i] = list[j];
-    list[j] = tmp;
+    var tmp = elements[i];
+    elements[i] = elements[j];
+    elements[j] = tmp;
   }
 }
 
@@ -99,8 +140,7 @@
 /// insertion sort.
 ///
 /// If [compare] is omitted, this defaults to calling [Comparable.compareTo] on
-/// the objects. If any object is not [Comparable], this throws a [TypeError]
-/// (`CastError` on some SDK versions).
+/// the objects. In this case, the objects must be [Comparable].
 ///
 /// Insertion sort is a simple sorting algorithm. For `n` elements it does on
 /// the order of `n * log(n)` comparisons but up to `n` squared moves. The
@@ -111,40 +151,50 @@
 ///
 /// This insertion sort is stable: Equal elements end up in the same order
 /// as they started in.
-void insertionSort<T>(List<T> list,
-    {int Function(T, T)? compare, int start = 0, int? end}) {
+void insertionSort<E>(List<E> elements,
+    {int Function(E, E)? compare, int start = 0, int? end}) {
   // If the same method could have both positional and named optional
   // parameters, this should be (list, [start, end], {compare}).
-  compare ??= defaultCompare();
-  end ??= list.length;
+  compare ??= defaultCompare;
+  end ??= elements.length;
 
   for (var pos = start + 1; pos < end; pos++) {
     var min = start;
     var max = pos;
-    var element = list[pos];
+    var element = elements[pos];
     while (min < max) {
       var mid = min + ((max - min) >> 1);
-      var comparison = compare(element, list[mid]);
+      var comparison = compare(element, elements[mid]);
       if (comparison < 0) {
         max = mid;
       } else {
         min = mid + 1;
       }
     }
-    list.setRange(min + 1, pos + 1, list, min);
-    list[min] = element;
+    elements.setRange(min + 1, pos + 1, elements, min);
+    elements[min] = element;
   }
 }
 
+/// Generalized insertion sort.
+///
+/// Performs insertion sort on the [elements] range from [start] to [end].
+/// Ordering is the [compare] of the [keyOf] of the elements.
+void insertionSortBy<E, K>(List<E> elements, K Function(E element) keyOf,
+    int Function(K a, K b) compare,
+    [int start = 0, int? end]) {
+  end = RangeError.checkValidRange(start, end, elements.length);
+  _movingInsertionSort(elements, keyOf, compare, start, end, elements, start);
+}
+
 /// Limit below which merge sort defaults to insertion sort.
-const int _MERGE_SORT_LIMIT = 32;
+const int _mergeSortLimit = 32;
 
 /// Sorts a list between [start] (inclusive) and [end] (exclusive) using the
 /// merge sort algorithm.
 ///
 /// If [compare] is omitted, this defaults to calling [Comparable.compareTo] on
-/// the objects. If any object is not [Comparable], this throws a [TypeError]
-/// (`CastError` on some SDK versions).
+/// the objects. If any object is not [Comparable], that throws a [TypeError].
 ///
 /// Merge-sorting works by splitting the job into two parts, sorting each
 /// recursively, and then merging the two sorted parts.
@@ -155,15 +205,53 @@
 ///
 /// This merge sort is stable: Equal elements end up in the same order
 /// as they started in.
-void mergeSort<T>(List<T> list,
-    {int start = 0, int? end, int Function(T, T)? compare}) {
-  end = RangeError.checkValidRange(start, end, list.length);
-  compare ??= defaultCompare();
+void mergeSort<E>(List<E> elements,
+    {int start = 0, int? end, int Function(E, E)? compare}) {
+  end = RangeError.checkValidRange(start, end, elements.length);
+  compare ??= defaultCompare;
 
   var length = end - start;
   if (length < 2) return;
-  if (length < _MERGE_SORT_LIMIT) {
-    insertionSort(list, compare: compare, start: start, end: end);
+  if (length < _mergeSortLimit) {
+    insertionSort(elements, compare: compare, start: start, end: end);
+    return;
+  }
+  // Special case the first split instead of directly calling
+  // _mergeSort, because the _mergeSort requires its target to
+  // be different from its source, and it requires extra space
+  // of the same size as the list to sort.
+  // This split allows us to have only half as much extra space,
+  // and allows the sorted elements to end up in the original list.
+  var firstLength = (end - start) >> 1;
+  var middle = start + firstLength;
+  var secondLength = end - middle;
+  // secondLength is always the same as firstLength, or one greater.
+  var scratchSpace = List<E>.filled(secondLength, elements[start]);
+  // TODO(linter/2097): Remove ignore when no longer required by linter.
+  // See: https://github.com/dart-lang/linter/issues/2097
+  E Function(E) id = identity; // ignore: omit_local_variable_types
+  _mergeSort(elements, id, compare, middle, end, scratchSpace, 0);
+  var firstTarget = end - firstLength;
+  _mergeSort(elements, id, compare, start, middle, elements, firstTarget);
+  _merge(id, compare, elements, firstTarget, end, scratchSpace, 0, secondLength,
+      elements, start);
+}
+
+/// Sort [elements] using a merge-sort algorithm.
+///
+/// The elements are compared using [compare] on the value provided by [keyOf]
+/// on the element.
+/// If [start] and [end] are provided, only that range is sorted.
+///
+/// Uses insertion sort for smaller sublists.
+void mergeSortBy<E, K>(List<E> elements, K Function(E element) keyOf,
+    int Function(K a, K b) compare,
+    [int start = 0, int? end]) {
+  end = RangeError.checkValidRange(start, end, elements.length);
+  var length = end - start;
+  if (length < 2) return;
+  if (length < _mergeSortLimit) {
+    _movingInsertionSort(elements, keyOf, compare, start, end, elements, start);
     return;
   }
   // Special case the first split instead of directly calling
@@ -172,34 +260,41 @@
   // of the same size as the list to sort.
   // This split allows us to have only half as much extra space,
   // and it ends up in the original place.
-  var middle = start + ((end - start) >> 1);
+  var middle = start + (length >> 1);
   var firstLength = middle - start;
   var secondLength = end - middle;
   // secondLength is always the same as firstLength, or one greater.
-  var scratchSpace = List<T>.filled(secondLength, list[start]);
-  _mergeSort(list, compare, middle, end, scratchSpace, 0);
+  var scratchSpace = List<E>.filled(secondLength, elements[start]);
+  _mergeSort(elements, keyOf, compare, middle, end, scratchSpace, 0);
   var firstTarget = end - firstLength;
-  _mergeSort(list, compare, start, middle, list, firstTarget);
-  _merge(compare, list, firstTarget, end, scratchSpace, 0, secondLength, list,
-      start);
+  _mergeSort(elements, keyOf, compare, start, middle, elements, firstTarget);
+  _merge(keyOf, compare, elements, firstTarget, end, scratchSpace, 0,
+      secondLength, elements, start);
 }
 
 /// Performs an insertion sort into a potentially different list than the
 /// one containing the original values.
 ///
 /// It will work in-place as well.
-void _movingInsertionSort<T>(List<T> list, int Function(T, T) compare,
-    int start, int end, List<T> target, int targetOffset) {
+void _movingInsertionSort<E, K>(
+    List<E> list,
+    K Function(E element) keyOf,
+    int Function(K, K) compare,
+    int start,
+    int end,
+    List<E> target,
+    int targetOffset) {
   var length = end - start;
   if (length == 0) return;
   target[targetOffset] = list[start];
   for (var i = 1; i < length; i++) {
     var element = list[start + i];
+    var elementKey = keyOf(element);
     var min = targetOffset;
     var max = targetOffset + i;
     while (min < max) {
       var mid = min + ((max - min) >> 1);
-      if (compare(element, target[mid]) < 0) {
+      if (compare(elementKey, keyOf(target[mid])) < 0) {
         max = mid;
       } else {
         min = mid + 1;
@@ -210,18 +305,25 @@
   }
 }
 
-/// Sorts [list] from [start] to [end] into [target] at [targetOffset].
+/// Sorts [elements] from [start] to [end] into [target] at [targetOffset].
 ///
 /// The `target` list must be able to contain the range from `start` to `end`
 /// after `targetOffset`.
 ///
-/// Allows target to be the same list as [list], as long as it's not
+/// Allows target to be the same list as [elements], as long as it's not
 /// overlapping the `start..end` range.
-void _mergeSort<T>(List<T> list, int Function(T, T) compare, int start, int end,
-    List<T> target, int targetOffset) {
+void _mergeSort<E, K>(
+    List<E> elements,
+    K Function(E element) keyOf,
+    int Function(K, K) compare,
+    int start,
+    int end,
+    List<E> target,
+    int targetOffset) {
   var length = end - start;
-  if (length < _MERGE_SORT_LIMIT) {
-    _movingInsertionSort(list, compare, start, end, target, targetOffset);
+  if (length < _mergeSortLimit) {
+    _movingInsertionSort<E, K>(
+        elements, keyOf, compare, start, end, target, targetOffset);
     return;
   }
   var middle = start + (length >> 1);
@@ -230,12 +332,12 @@
   // Here secondLength >= firstLength (differs by at most one).
   var targetMiddle = targetOffset + firstLength;
   // Sort the second half into the end of the target area.
-  _mergeSort(list, compare, middle, end, target, targetMiddle);
+  _mergeSort(elements, keyOf, compare, middle, end, target, targetMiddle);
   // Sort the first half into the end of the source area.
-  _mergeSort(list, compare, start, middle, list, middle);
+  _mergeSort(elements, keyOf, compare, start, middle, elements, middle);
   // Merge the two parts into the target area.
-  _merge(compare, list, middle, middle + firstLength, target, targetMiddle,
-      targetMiddle + secondLength, target, targetOffset);
+  _merge(keyOf, compare, elements, middle, middle + firstLength, target,
+      targetMiddle, targetMiddle + secondLength, target, targetOffset);
 }
 
 /// Merges two lists into a target list.
@@ -246,15 +348,16 @@
 /// For equal object, elements from [firstList] are always preferred.
 /// This allows the merge to be stable if the first list contains elements
 /// that started out earlier than the ones in [secondList]
-void _merge<T>(
-    int Function(T, T) compare,
-    List<T> firstList,
+void _merge<E, K>(
+    K Function(E element) keyOf,
+    int Function(K, K) compare,
+    List<E> firstList,
     int firstStart,
     int firstEnd,
-    List<T> secondList,
+    List<E> secondList,
     int secondStart,
     int secondEnd,
-    List<T> target,
+    List<E> target,
     int targetOffset) {
   // No empty lists reaches here.
   assert(firstStart < firstEnd);
@@ -262,16 +365,20 @@
   var cursor1 = firstStart;
   var cursor2 = secondStart;
   var firstElement = firstList[cursor1++];
+  var firstKey = keyOf(firstElement);
   var secondElement = secondList[cursor2++];
+  var secondKey = keyOf(secondElement);
   while (true) {
-    if (compare(firstElement, secondElement) <= 0) {
+    if (compare(firstKey, secondKey) <= 0) {
       target[targetOffset++] = firstElement;
       if (cursor1 == firstEnd) break; // Flushing second list after loop.
       firstElement = firstList[cursor1++];
+      firstKey = keyOf(firstElement);
     } else {
       target[targetOffset++] = secondElement;
       if (cursor2 != secondEnd) {
         secondElement = secondList[cursor2++];
+        secondKey = keyOf(secondElement);
         continue;
       }
       // Second list empties first. Flushing first list here.
@@ -286,3 +393,71 @@
   target.setRange(
       targetOffset, targetOffset + (secondEnd - cursor2), secondList, cursor2);
 }
+
+/// Sort [elements] using a quick-sort algorithm.
+///
+/// The elements are compared using [compare] on the elements.
+/// If [start] and [end] are provided, only that range is sorted.
+///
+/// Uses insertion sort for smaller sublists.
+void quickSort<E>(List<E> elements, int Function(E a, E b) compare,
+    [int start = 0, int? end]) {
+  end = RangeError.checkValidRange(start, end, elements.length);
+  _quickSort<E, E>(elements, identity, compare, Random(), start, end);
+}
+
+/// Sort [elements] using a quick-sort algorithm.
+///
+/// The elements are compared using [compare] on the value provided by [keyOf]
+/// on the element.
+/// If [start] and [end] are provided, only that range is sorted.
+///
+/// Uses insertion sort for smaller sublists.
+void quickSortBy<E, K>(
+    List<E> list, K Function(E element) keyOf, int Function(K a, K b) compare,
+    [int start = 0, int? end]) {
+  end = RangeError.checkValidRange(start, end, list.length);
+  _quickSort(list, keyOf, compare, Random(), start, end);
+}
+
+void _quickSort<E, K>(List<E> list, K Function(E element) keyOf,
+    int Function(K a, K b) compare, Random random, int start, int end) {
+  const minQuickSortLength = 24;
+  var length = end - start;
+  while (length >= minQuickSortLength) {
+    var pivotIndex = random.nextInt(length) + start;
+    var pivot = list[pivotIndex];
+    var pivotKey = keyOf(pivot);
+    var endSmaller = start;
+    var startGreater = end;
+    var startPivots = end - 1;
+    list[pivotIndex] = list[startPivots];
+    list[startPivots] = pivot;
+    while (endSmaller < startPivots) {
+      var current = list[endSmaller];
+      var relation = compare(keyOf(current), pivotKey);
+      if (relation < 0) {
+        endSmaller++;
+      } else {
+        startPivots--;
+        var currentTarget = startPivots;
+        list[endSmaller] = list[startPivots];
+        if (relation > 0) {
+          startGreater--;
+          currentTarget = startGreater;
+          list[startPivots] = list[startGreater];
+        }
+        list[currentTarget] = current;
+      }
+    }
+    if (endSmaller - start < end - startGreater) {
+      _quickSort(list, keyOf, compare, random, start, endSmaller);
+      start = startGreater;
+    } else {
+      _quickSort(list, keyOf, compare, random, startGreater, end);
+      end = endSmaller;
+    }
+    length = end - start;
+  }
+  _movingInsertionSort<E, K>(list, keyOf, compare, start, end, list, start);
+}
diff --git a/lib/src/functions.dart b/lib/src/functions.dart
index 07c8d4b..8e73f16 100644
--- a/lib/src/functions.dart
+++ b/lib/src/functions.dart
@@ -64,7 +64,7 @@
 /// Returns `null` if [values] is empty.
 S? minBy<S, T>(Iterable<S> values, T Function(S) orderBy,
     {int Function(T, T)? compare}) {
-  compare ??= defaultCompare<T>();
+  compare ??= defaultCompare;
 
   S? minValue;
   T? minOrderBy;
@@ -88,7 +88,7 @@
 /// Returns `null` if [values] is empty.
 S? maxBy<S, T>(Iterable<S> values, T Function(S?) orderBy,
     {int? Function(T, T)? compare}) {
-  compare ??= defaultCompare<T>();
+  compare ??= defaultCompare;
 
   S? maxValue;
   T? maxOrderBy;
diff --git a/lib/src/iterable_extensions.dart b/lib/src/iterable_extensions.dart
new file mode 100644
index 0000000..728c1ec
--- /dev/null
+++ b/lib/src/iterable_extensions.dart
@@ -0,0 +1,762 @@
+// Copyright (c) 2020, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:collection/src/utils.dart';
+
+import 'algorithms.dart';
+
+/// Extensions that apply to all iterables.
+///
+/// These extensions provide direct access to some of the
+/// algorithms expose by this package,
+/// as well as some generally useful convenience methods.
+///
+/// More specialized extension methods that only apply to
+/// iterables with specific element types include those of
+/// [IterableComparableExtension] and [IterableNullableExtension].
+extension IterableExtension<T> on Iterable<T> {
+  /// The elements that do not satisfy [test].
+  Iterable<T> whereNot(bool Function(T element) test) =>
+      where((element) => !test(element));
+
+  /// Creates a sorted list of the elements of the iterable.
+  ///
+  /// The elements are ordered by the [compare] [Comparator].
+  List<T> sorted(Comparator<T> compare) => [...this]..sort(compare);
+
+  /// Creates a sorted list of the elements of the iterable.
+  ///
+  /// The elements are ordered by the natural ordering of the
+  /// property [keyOf] of the element.
+  List<T> sortedBy<K extends Comparable<K>>(K Function(T element) keyOf) {
+    var elements = [...this];
+    quickSortBy<T, K>(elements, keyOf, compareComparable);
+    return elements;
+  }
+
+  /// Creates a sorted list of the elements of the iterable.
+  ///
+  /// The elements are ordered by the [compare] [Comparator] of the
+  /// property [keyOf] of the element.
+  List<T> sortedByCompare<K>(
+      K Function(T element) keyOf, Comparator<K> compare) {
+    var elements = [...this];
+    quickSortBy<T, K>(elements, keyOf, compare);
+    return elements;
+  }
+
+  /// Whether the elements are sorted by the [compare] ordering.
+  ///
+  /// Compares pairs of elements using `compare` to check that
+  /// the elements of this iterable to check
+  /// that earlier elements always compare
+  /// smaller than or equal to later elements.
+  ///
+  /// An single-element or empty iterable is trivially in sorted order.
+  bool isSorted(Comparator<T> compare) {
+    var iterator = this.iterator;
+    if (!iterator.moveNext()) return true;
+    var previousElement = iterator.current;
+    while (iterator.moveNext()) {
+      var element = iterator.current;
+      if (compare(previousElement, element) > 0) return false;
+      previousElement = element;
+    }
+    return true;
+  }
+
+  /// Whether the elements are sorted by their [keyOf] property.
+  ///
+  /// Applies [keyOf] to each element in iteration order,
+  /// then checks whether the results are in non-decreasing [Comparable] order.
+  bool isSortedBy<K extends Comparable<K>>(K Function(T element) keyOf) {
+    var iterator = this.iterator;
+    if (!iterator.moveNext()) return true;
+    var previousKey = keyOf(iterator.current);
+    while (iterator.moveNext()) {
+      var key = keyOf(iterator.current);
+      if (previousKey.compareTo(key) > 0) return false;
+      previousKey = key;
+    }
+    return true;
+  }
+
+  /// Whether the elements are [compare]-sorted by their [keyOf] property.
+  ///
+  /// Applies [keyOf] to each element in iteration order,
+  /// then checks whether the results are in non-decreasing order
+  /// using the [compare] [Comparator]..
+  bool isSortedByCompare<K>(
+      K Function(T element) keyOf, Comparator<K> compare) {
+    var iterator = this.iterator;
+    if (!iterator.moveNext()) return true;
+    var previousKey = keyOf(iterator.current);
+    while (iterator.moveNext()) {
+      var key = keyOf(iterator.current);
+      if (compare(previousKey, key) > 0) return false;
+      previousKey = key;
+    }
+    return true;
+  }
+
+  /// Takes an action for each element.
+  ///
+  /// Calls [action] for each element along with the index in the
+  /// iteration order.
+  void forEachIndexed(void Function(int index, T element) action) {
+    var index = 0;
+    for (var element in this) {
+      action(index++, element);
+    }
+  }
+
+  /// Takes an action for each element as long as desired.
+  ///
+  /// Calls [action] for each element.
+  /// Stops iteration if [action] returns `false`.
+  void forEachWhile(bool Function(T element) action) {
+    for (var element in this) {
+      if (!action(element)) break;
+    }
+  }
+
+  /// Takes an action for each element and index as long as desired.
+  ///
+  /// Calls [action] for each element along with the index in the
+  /// iteration order.
+  /// Stops iteration if [action] returns `false`.
+  void forEachIndexedWhile(bool Function(int index, T element) action) {
+    var index = 0;
+    for (var element in this) {
+      if (!action(index++, element)) break;
+    }
+  }
+
+  /// Maps each element and its index to a new value.
+  Iterable<R> mapIndexed<R>(R Function(int index, T element) convert) sync* {
+    var index = 0;
+    for (var element in this) {
+      yield convert(index++, element);
+    }
+  }
+
+  /// The elements whose value and index satisfies [test].
+  Iterable<T> whereIndexed(bool Function(int index, T element) test) sync* {
+    var index = 0;
+    for (var element in this) {
+      if (test(index++, element)) yield element;
+    }
+  }
+
+  /// The elements whose value and index do not satisfy [test].
+  Iterable<T> whereNotIndexed(bool Function(int index, T element) test) sync* {
+    var index = 0;
+    for (var element in this) {
+      if (!test(index++, element)) yield element;
+    }
+  }
+
+  /// Expands each element and index to a number of elements in a new iterable.
+  Iterable<R> expandIndexed<R>(
+      Iterable<R> Function(int index, T element) expend) sync* {
+    var index = 0;
+    for (var element in this) {
+      yield* expend(index++, element);
+    }
+  }
+
+  /// Combine the elements with each other and the current index.
+  ///
+  /// Calls [combine] for each element except the first.
+  /// The call passes the index of the current element, the result of the
+  /// previous call, or the first element for the first call, and
+  /// the current element.
+  ///
+  /// Returns the result of the last call, or the first element if
+  /// there is only one element.
+  /// There must be at least one element.
+  T reduceIndexed(T Function(int index, T previous, T element) combine) {
+    var iterator = this.iterator;
+    if (!iterator.moveNext()) {
+      throw StateError('no elements');
+    }
+    var index = 1;
+    var result = iterator.current;
+    while (iterator.moveNext()) {
+      result = combine(index++, result, iterator.current);
+    }
+    return result;
+  }
+
+  /// Combine the elements with a value and the current index.
+  ///
+  /// Calls [combine] for each element with the current index,
+  /// the result of the previous call, or [initialValue] for the first element,
+  /// and the current element.
+  ///
+  /// Returns the result of the last call to [combine],
+  /// or [initialValue] if there are no elements.
+  R foldIndexed<R>(
+      R initialValue, R Function(int index, R previous, T element) combine) {
+    var result = initialValue;
+    var index = 0;
+    for (var element in this) {
+      result = combine(index++, result, element);
+    }
+    return result;
+  }
+
+  /// The first element satisfying [test], or `null` if there are none.
+  T? firstWhereOrNull(bool Function(T element) test) {
+    for (var element in this) {
+      if (test(element)) return element;
+    }
+    return null;
+  }
+
+  /// The first element whose value and index satisfies [test].
+  ///
+  /// Returns `null` if there are no element and index satisfying [test].
+  T? firstWhereIndexedOrNull(bool Function(int index, T element) test) {
+    var index = 0;
+    for (var element in this) {
+      if (test(index++, element)) return element;
+    }
+    return null;
+  }
+
+  /// The first element, or `null` if the iterable is empty.
+  T? get firstOrNull {
+    var iterator = this.iterator;
+    if (iterator.moveNext()) return iterator.current;
+    return null;
+  }
+
+  /// The last element satisfying [test], or `null` if there are none.
+  T? lastWhereOrNull(bool Function(T element) test) {
+    T? result;
+    for (var element in this) {
+      if (test(element)) result = element;
+    }
+    return result;
+  }
+
+  /// The last element whose index and value satisfies [test].
+  ///
+  /// Returns `null` if no element and index satisfies [test].
+  T? lastWhereIndexedOrNull(bool Function(int index, T element) test) {
+    T? result;
+    var index = 0;
+    for (var element in this) {
+      if (test(index++, element)) result = element;
+    }
+    return result;
+  }
+
+  /// The last element, or `null` if the iterable is empty.
+  T? get lastOrNull {
+    if (isEmpty) return null;
+    return last;
+  }
+
+  /// The single element satisfying [test].
+  ///
+  /// Returns `null` if there are either no elements
+  /// or more than one element satisfying [test].
+  ///
+  /// **Notice**: This behavior differs from [Iterable.singleWhere]
+  /// which always throws if there are more than one match,
+  /// and only calls the `orElse` function on zero matchs.
+  T? singleWhereOrNull(bool Function(T element) test) {
+    T? result;
+    var found = false;
+    for (var element in this) {
+      if (test(element)) {
+        if (!found) {
+          result = element;
+          found = true;
+        } else {
+          return null;
+        }
+      }
+    }
+    return result;
+  }
+
+  /// The single element satisfying [test].
+  ///
+  /// Returns `null` if there are either none
+  /// or more than one element and index satisfying [test].
+  T? singleWhereIndexedOrNull(bool Function(int index, T element) test) {
+    T? result;
+    var found = false;
+    var index = 0;
+    for (var element in this) {
+      if (test(index++, element)) {
+        if (!found) {
+          result = element;
+          found = true;
+        } else {
+          return null;
+        }
+      }
+    }
+    return result;
+  }
+
+  /// The single element of the iterable, or `null`.
+  ///
+  /// The value is `null` if the iterable is empty
+  /// or it contains more than one element.
+  T? get singleOrNull {
+    var iterator = this.iterator;
+    if (iterator.moveNext()) {
+      var result = iterator.current;
+      if (!iterator.moveNext()) {
+        return result;
+      }
+    }
+    return null;
+  }
+
+  /// Groups elements by [keyOf] then folds the elements in each group.
+  ///
+  /// A key is found for each element using [keyOf].
+  /// Then the elements with the same key are all folded using [combine].
+  /// The first call to [combine] for a particular key receives [null] as
+  /// the previous value, the remaining ones receive the result of the previous call.
+  ///
+  /// Can be used to _group_ elements into arbitrary collections.
+  /// For example [groupSetsBy] could be written as:
+  /// ```dart
+  /// iterable.groupFoldBy(keyOf,
+  ///     (Set<T>? previous, T element) => (previous ?? <T>{})..add(element));
+  /// ````
+  Map<K, G> groupFoldBy<K, G>(
+      K Function(T element) keyOf, G Function(G? previous, T element) combine) {
+    var result = <K, G>{};
+    for (var element in this) {
+      var key = keyOf(element);
+      result[key] = combine(result[key], element);
+    }
+    return result;
+  }
+
+  /// Groups elements into sets by [keyOf].
+  Map<K, Set<T>> groupSetsBy<K>(K Function(T element) keyOf) {
+    var result = <K, Set<T>>{};
+    for (var element in this) {
+      (result[keyOf(element)] ??= <T>{})..add(element);
+    }
+    return result;
+  }
+
+  /// Groups elements into lists by [keyOf].
+  Map<K, List<T>> groupListsBy<K>(K Function(T element) keyOf) {
+    var result = <K, List<T>>{};
+    for (var element in this) {
+      (result[keyOf(element)] ??= [])..add(element);
+    }
+    return result;
+  }
+
+  /// Splits the elements into chunks before some elements.
+  ///
+  /// Each element except the first is checked using [test]
+  /// for whether it should start a new chunk.
+  /// If so, the elements since the previous chunk-starting element
+  /// are emitted as a list.
+  /// Any final elements are emitted at the end.
+  ///
+  /// Example:
+  /// Example:
+  /// ```dart
+  /// var parts = [1, 2, 3, 4, 5, 6, 7, 8, 9].split(isPrime);
+  /// print(parts); // ([1], [2], [3, 4], [5, 6], [7, 8, 9])
+  /// ```
+  Iterable<List<T>> splitBefore(bool Function(T element) test) =>
+      splitBeforeIndexed((_, element) => test(element));
+
+  /// Splits the elements into chunks before some elements.
+  ///
+  /// Each element is checked using [test] for whether it should start a new chunk.
+  /// If so, the elements since the previous chunk-starting element
+  /// are emitted as a list.
+  /// Any final elements are emitted at the end.
+  ///
+  /// Example:
+  /// ```dart
+  /// var parts = [1, 0, 2, 1, 5, 7, 6, 8, 9].splitAfter(isPrime);
+  /// print(parts); // ([1, 0, 2], [1, 5], [7], [6, 8, 9])
+  /// ```
+  Iterable<List<T>> splitAfter(bool Function(T element) test) =>
+      splitAfterIndexed((_, element) => test(element));
+
+  /// Splits the elements into chunks between some elements.
+  ///
+  /// Each pair of adjacent elements are checked using [test]
+  /// for whether a chunk should end between them.
+  /// If so, the elements since the previous chunk-splitting elements
+  /// are emitted as a list.
+  /// Any final elements are emitted at the end.
+  ///
+  /// Example:
+  /// ```dart
+  /// var parts = [1, 0, 2, 1, 5, 7, 6, 8, 9].splitBetween((i, v1, v2) => v1 > v2);
+  /// print(parts); // ([1], [0, 2], [1, 5, 7], [6, 8, 9])
+  /// ```
+  Iterable<List<T>> splitBetween(bool Function(T first, T second) test) =>
+      splitBetweenIndexed((_, first, second) => test(first, second));
+
+  /// Splits the elements into chunks before some elements and indices.
+  ///
+  /// Each element and index except the first is checked using [test]
+  /// for whether it should start a new chunk.
+  /// If so, the elements since the previous chunk-starting element
+  /// are emitted as a list.
+  /// Any final elements are emitted at the end.
+  ///
+  /// Example:
+  /// ```dart
+  /// var parts = [1, 0, 2, 1, 5, 7, 6, 8, 9]
+  ///     .splitBeforeIndexed((i, v) => i < v);
+  /// print(parts); // ([1], [0, 2], [1, 5, 7], [6, 8, 9])
+  /// ```
+  Iterable<List<T>> splitBeforeIndexed(
+      bool Function(int index, T element) test) sync* {
+    var iterator = this.iterator;
+    if (!iterator.moveNext()) {
+      return;
+    }
+    var index = 1;
+    var chunk = [iterator.current];
+    while (iterator.moveNext()) {
+      var element = iterator.current;
+      if (test(index++, element)) {
+        yield chunk;
+        chunk = [];
+      }
+      chunk.add(element);
+    }
+    yield chunk;
+  }
+
+  /// Splits the elements into chunks after some elements and indices.
+  ///
+  /// Each element and index is checked using [test]
+  /// for whether it should end the current chunk.
+  /// If so, the elements since the previous chunk-ending element
+  /// are emitted as a list.
+  /// Any final elements are emitted at the end, whether the last
+  /// element should be split after or not.
+  ///
+  /// Example:
+  /// ```dart
+  /// var parts = [1, 0, 2, 1, 5, 7, 6, 8, 9].splitAfterIndexed((i, v) => i < v);
+  /// print(parts); // ([1, 0], [2, 1], [5, 7, 6], [8, 9])
+  /// ```
+  Iterable<List<T>> splitAfterIndexed(
+      bool Function(int index, T element) test) sync* {
+    var index = 0;
+    List<T>? chunk;
+    for (var element in this) {
+      (chunk ??= []).add(element);
+      if (test(index++, element)) {
+        yield chunk;
+        chunk = null;
+      }
+    }
+    if (chunk != null) yield chunk;
+  }
+
+  /// Splits the elements into chunks between some elements and indices.
+  ///
+  /// Each pair of adjacent elements and the index of the latter are
+  /// checked using [test] for whether a chunk should end between them.
+  /// If so, the elements since the previous chunk-splitting elements
+  /// are emitted as a list.
+  /// Any final elements are emitted at the end.
+  ///
+  /// Example:
+  /// ```dart
+  /// var parts = [1, 0, 2, 1, 5, 7, 6, 8, 9]
+  ///    .splitBetweenIndexed((i, v1, v2) => v1 > v2);
+  /// print(parts); // ([1], [0, 2], [1, 5, 7], [6, 8, 9])
+  /// ```
+  Iterable<List<T>> splitBetweenIndexed(
+      bool Function(int index, T first, T second) test) sync* {
+    var iterator = this.iterator;
+    if (!iterator.moveNext()) return;
+    var previous = iterator.current;
+    var chunk = <T>[previous];
+    var index = 1;
+    while (iterator.moveNext()) {
+      var element = iterator.current;
+      if (test(index++, previous, element)) {
+        yield chunk;
+        chunk = [];
+      }
+      chunk.add(element);
+      previous = element;
+    }
+    yield chunk;
+  }
+
+  /// Whether no element satisfies [test].
+  ///
+  /// Returns true if no element satisfies [test],
+  /// and false if at least one does.
+  ///
+  /// Equivalent to `iterable.every((x) => !test(x))` or
+  /// `!iterable.any(test)`.
+  bool none(bool Function(T) test) {
+    for (var element in this) {
+      if (test(element)) return false;
+    }
+    return true;
+  }
+}
+
+/// Extensions that apply to iterables with a nullable element type.
+extension IterableNullableExtension<T extends Object> on Iterable<T?> {
+  /// The non-`null` elements of this `Iterable`.
+  ///
+  /// Returns an iterable which emits all the non-`null` elements
+  /// of this iterable, in their original iteration order.
+  ///
+  /// For an `Iterable<X?>`, this method is equivalent to `.whereType<X>()`.
+  Iterable<T> whereNotNull() sync* {
+    for (var element in this) {
+      if (element != null) yield element;
+    }
+  }
+}
+
+/// Extensions that apply to iterables of numbers.
+extension IterableNumberExtension on Iterable<num> {
+  /// The sum of the elements.
+  ///
+  /// The sum is zero if the iterable is empty.
+  num get sum {
+    num result = 0;
+    for (var value in this) {
+      result += value;
+    }
+    return result;
+  }
+
+  /// The arithmetic mean of the elements of a non-empty iterable.
+  ///
+  /// The arithmetic mean is the sum of the elements
+  /// divided by the number of elements.
+  ///
+  /// The iterable must not be empty.
+  double get average {
+    var result = 0.0;
+    var count = 0;
+    for (var value in this) {
+      count += 1;
+      result += (value - result) / count;
+    }
+    if (count == 0) throw StateError('No elements');
+    return result;
+  }
+}
+
+/// Extension on iterables of integers.
+///
+/// Specialized version of some extensions of [IterableNumberExtension].
+extension IterableIntegerExtension on Iterable<int> {
+  /// The sum of the elements.
+  ///
+  /// The sum is zero if the iterable is empty.
+  int get sum {
+    var result = 0;
+    for (var value in this) {
+      result += value;
+    }
+    return result;
+  }
+
+  /// The arithmetic mean of the elements of a non-empty iterable.
+  ///
+  /// The arithmetic mean is the sum of the elements
+  /// divided by the number of elements.
+  /// This method is specialized for integers,
+  /// and may give a different result than [IterableNumberExtension.average]
+  /// for the same values, because the the number algorithm
+  /// converts all numbers to doubles.
+  ///
+  /// The iterable must not be empty.
+  double get average {
+    var average = 0;
+    var remainder = 0;
+    var count = 0;
+    for (var value in this) {
+      // Invariant: Sum of values so far = average * count + remainder.
+      // (Unless overflow has occurred).
+      count += 1;
+      var delta = value - average + remainder;
+      average += delta ~/ count;
+      remainder = delta.remainder(count);
+    }
+    if (count == 0) throw StateError('No elements');
+    return average + remainder / count;
+  }
+}
+
+/// Extension on iterables of double.
+///
+/// Specialized version of some extensions of [IterableNumberExtension].
+extension IterableDoubleExtension on Iterable<double> {
+  /// The sum of the elements.
+  ///
+  /// The sum is zero if the iterable is empty.
+  double get sum {
+    var result = 0.0;
+    for (var value in this) {
+      result += value;
+    }
+    return result;
+  }
+}
+
+/// Extensions on iterables whose elements are also iterables.
+extension IterableIterableExtension<T> on Iterable<Iterable<T>> {
+  /// The sequential elements of each iterable in this iterable.
+  ///
+  /// Iterates the elements of this iterable.
+  /// For each one, which is itself an iterable,
+  /// all the elements of that are emitted
+  /// on the returned iterable, before moving on to the next element.
+  Iterable<T> get flattened sync* {
+    for (var elements in this) {
+      yield* elements;
+    }
+  }
+}
+
+/// Extensions that apply to iterables of [Comparable] elements.
+///
+/// These operations can assume that the elements have a natural ordering,
+/// and can therefore omit, or make it optional, for the user to provide
+/// a [Comparator].
+extension IterableComparableExtension<T extends Comparable<T>> on Iterable<T> {
+  /// A minimal element of the iterable, or `null` it the iterable is empty.
+  T? get minOrNull {
+    var iterator = this.iterator;
+    if (iterator.moveNext()) {
+      var value = iterator.current;
+      while (iterator.moveNext()) {
+        var newValue = iterator.current;
+        if (value.compareTo(newValue) > 0) {
+          value = newValue;
+        }
+      }
+      return value;
+    }
+    return null;
+  }
+
+  /// A minimal element of the iterable.
+  ///
+  /// The iterable must not be empty.
+  T get min {
+    var iterator = this.iterator;
+    if (iterator.moveNext()) {
+      var value = iterator.current;
+      while (iterator.moveNext()) {
+        var newValue = iterator.current;
+        if (value.compareTo(newValue) > 0) {
+          value = newValue;
+        }
+      }
+      return value;
+    }
+    throw StateError('No element');
+  }
+
+  /// A maximal element of the iterable, or `null` if the iterable is empty.
+  T? get maxOrNull {
+    var iterator = this.iterator;
+    if (iterator.moveNext()) {
+      var value = iterator.current;
+      while (iterator.moveNext()) {
+        var newValue = iterator.current;
+        if (value.compareTo(newValue) < 0) {
+          value = newValue;
+        }
+      }
+      return value;
+    }
+    return null;
+  }
+
+  /// A maximal element of the iterable.
+  ///
+  /// The iterable must not be empty.
+  T get max {
+    var iterator = this.iterator;
+    if (iterator.moveNext()) {
+      var value = iterator.current;
+      while (iterator.moveNext()) {
+        var newValue = iterator.current;
+        if (value.compareTo(newValue) < 0) {
+          value = newValue;
+        }
+      }
+      return value;
+    }
+    throw StateError('No element');
+  }
+
+  /// Creates a sorted list of the elements of the iterable.
+  ///
+  /// If the [compare] function is not supplied, the sorting uses the
+  /// natural [Comparable] ordering of the elements.
+  List<T> sorted([Comparator<T>? compare]) => [...this]..sort(compare);
+
+  /// Whether the elements are sorted by the [compare] ordering.
+  ///
+  /// If [compare] is omitted, it defaults to comparing the
+  /// elements using their natural [Comparable] ordering.
+  bool isSorted([Comparator<T>? compare]) {
+    if (compare != null) {
+      return IterableExtension(this).isSorted(compare);
+    }
+    var iterator = this.iterator;
+    if (!iterator.moveNext()) return true;
+    var previousElement = iterator.current;
+    while (iterator.moveNext()) {
+      var element = iterator.current;
+      if (previousElement.compareTo(element) > 0) return false;
+      previousElement = element;
+    }
+    return true;
+  }
+}
+
+/// Extensions on comparator functions.
+extension ComparatorExtension<T> on Comparator<T> {
+  /// The inverse ordering of this comparator.
+  int Function(T, T) get inverse => (T a, T b) => this(b, a);
+
+  /// Makes a comparator on [R] values using this comparator.
+  ///
+  /// Compares [R] values by comparing their [keyOf] value
+  /// using this comparator.
+  Comparator<R> compareBy<R>(T Function(R) keyOf) =>
+      (R a, R b) => this(keyOf(a), keyOf(b));
+
+  /// Combine comparators sequentially.
+  ///
+  /// Creates a comparator which orders elements the same way as
+  /// this comparator, except that when two elements are considered
+  /// equal, the [tieBreaker] comparator is used instead.
+  Comparator<T> then(Comparator<T> tieBreaker) => (T a, T b) {
+        var result = this(a, b);
+        if (result == 0) result = tieBreaker(a, b);
+        return result;
+      };
+}
diff --git a/lib/src/list_extensions.dart b/lib/src/list_extensions.dart
new file mode 100644
index 0000000..c1f4af0
--- /dev/null
+++ b/lib/src/list_extensions.dart
@@ -0,0 +1,482 @@
+// Copyright (c) 2020, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// Extension methods on common collection types.
+import 'dart:collection';
+import 'dart:math';
+
+import 'algorithms.dart';
+import 'algorithms.dart' as algorithms;
+import 'equality.dart';
+import 'utils.dart';
+
+/// Various extensions on lists of arbitrary elements.
+extension ListExtensions<E> on List<E> {
+  /// Returns the index of [element] in this sorted list.
+  ///
+  /// Uses binary search to find the location of [element].
+  /// The list *must* be sorted according to [compare],
+  /// otherwise the result is unspecified
+  ///
+  /// Returns -1 if [element] does not occur in this list.
+  int binarySearch(E element, int Function(E, E) compare) =>
+      algorithms.binarySearchBy<E, E>(this, identity, compare, element);
+
+  /// Returns the index of [element] in this sorted list.
+  ///
+  /// Uses binary search to find the location of [element].
+  /// The list *must* be sorted according to [compare] on the [keyOf] of elements,
+  /// otherwise the result is unspecified.
+  ///
+  /// Returns -1 if [element] does not occur in this list.
+  ///
+  /// If [start] and [end] are supplied, only the list range from [start] to [end]
+  /// is searched, and only that range needs to be sorted.
+  int binarySearchByCompare<K>(
+          E element, K Function(E element) keyOf, int Function(K, K) compare,
+          [int start = 0, int? end]) =>
+      algorithms.binarySearchBy<E, K>(
+          this, keyOf, compare, element, start, end);
+
+  /// Returns the index of [element] in this sorted list.
+  ///
+  /// Uses binary search to find the location of [element].
+  /// The list *must* be sorted according to the natural ordering of
+  /// the [keyOf] of elements, otherwise the result is unspecified.
+  ///
+  /// Returns -1 if [element] does not occur in this list.
+  ///
+  /// If [start] and [end] are supplied, only the list range from [start] to [end]
+  /// is searched, and only that range needs to be sorted.
+  int binarySearchBy<K extends Comparable<K>>(
+          E element, K Function(E element) keyOf, [int start = 0, int? end]) =>
+      algorithms.binarySearchBy<E, K>(
+          this, keyOf, (a, b) => a.compareTo(b), element, start, end);
+
+  /// Returns the index where [element] should be in this sorted list.
+  ///
+  /// Uses binary search to find the location of [element].
+  /// The list *must* be sorted according to [compare],
+  /// otherwise the result is unspecified.
+  ///
+  /// If [element] is in the list, its index is returned,
+  /// otherwise returns the first position where adding [element]
+  /// would keep the list sorted. This may be the [length] of
+  /// the list if all elements of the list compare less than
+  /// [element].
+  int lowerBound(E element, int Function(E, E) compare) =>
+      algorithms.lowerBoundBy<E, E>(this, identity, compare, element);
+
+  /// Returns the index where [element] should be in this sorted list.
+  ///
+  /// Uses binary search to find the location of [element].
+  /// The list *must* be sorted according to [compare] of
+  /// the [keyOf] of the elements, otherwise the result is unspecified.
+  ///
+  /// If [element] is in the list, its index is returned,
+  /// otherwise returns the first position where adding [element]
+  /// would keep the list sorted. This may be the [length] of
+  /// the list if all elements of the list compare less than
+  /// [element].
+  ///
+  /// If [start] and [end] are supplied, only that range is searched,
+  /// and only that range need to be sorted.
+  int lowerBoundByCompare<K>(
+          E element, K Function(E) keyOf, int Function(K, K) compare,
+          [int start = 0, int? end]) =>
+      algorithms.lowerBoundBy(this, keyOf, compare, element, start, end);
+
+  /// Returns the index where [element] should be in this sorted list.
+  ///
+  /// Uses binary search to find the location of [element].
+  /// The list *must* be sorted according to the
+  /// natural ordering of the [keyOf] of the elements,
+  /// otherwise the result is unspecified.
+  ///
+  /// If [element] is in the list, its index is returned,
+  /// otherwise returns the first position where adding [element]
+  /// would keep the list sorted. This may be the [length] of
+  /// the list if all elements of the list compare less than
+  /// [element].
+  ///
+  /// If [start] and [end] are supplied, only that range is searched,
+  /// and only that range need to be sorted.
+  int lowerBoundBy<K extends Comparable<K>>(E element, K Function(E) keyOf,
+          [int start = 0, int? end]) =>
+      algorithms.lowerBoundBy<E, K>(
+          this, keyOf, compareComparable, element, start, end);
+
+  /// Takes an action for each element.
+  ///
+  /// Calls [action] for each element along with the index in the
+  /// iteration order.
+  void forEachIndexed(void Function(int index, E element) action) {
+    for (var index = 0; index < length; index++) {
+      action(index, this[index]);
+    }
+  }
+
+  /// Takes an action for each element as long as desired.
+  ///
+  /// Calls [action] for each element.
+  /// Stops iteration if [action] returns `false`.
+  void forEachWhile(bool Function(E element) action) {
+    for (var index = 0; index < length; index++) {
+      if (!action(this[index])) break;
+    }
+  }
+
+  /// Takes an action for each element and index as long as desired.
+  ///
+  /// Calls [action] for each element along with the index in the
+  /// iteration order.
+  /// Stops iteration if [action] returns `false`.
+  void forEachIndexedWhile(bool Function(int index, E element) action) {
+    for (var index = 0; index < length; index++) {
+      if (!action(index, this[index])) break;
+    }
+  }
+
+  /// Maps each element and its index to a new value.
+  Iterable<R> mapIndexed<R>(R Function(int index, E element) convert) sync* {
+    for (var index = 0; index < length; index++) {
+      yield convert(index, this[index]);
+    }
+  }
+
+  /// The elements whose value and index satisfies [test].
+  Iterable<E> whereIndexed(bool Function(int index, E element) test) sync* {
+    for (var index = 0; index < length; index++) {
+      var element = this[index];
+      if (test(index, element)) yield element;
+    }
+  }
+
+  /// The elements whose value and index do not satisfy [test].
+  Iterable<E> whereNotIndexed(bool Function(int index, E element) test) sync* {
+    for (var index = 0; index < length; index++) {
+      var element = this[index];
+      if (!test(index, element)) yield element;
+    }
+  }
+
+  /// Expands each element and index to a number of elements in a new iterable.
+  ///
+  /// Like [Iterable.expand] except that the callback function is supplied with
+  /// both the index and the element.
+  Iterable<R> expendIndexed<R>(
+      Iterable<R> Function(int index, E element) expand) sync* {
+    for (var index = 0; index < length; index++) {
+      yield* expand(index, this[index]);
+    }
+  }
+
+  /// Sort a range of elements by [compare].
+  void sortRange(int start, int end, int Function(E a, E b) compare) {
+    quickSortBy<E, E>(this, identity, compare, start, end);
+  }
+
+  /// Sorts elements by the [compare] of their [keyOf] property.
+  ///
+  /// Sorts elements from [start] to [end], defaulting to the entire list.
+  void sortByCompare<K>(
+      K Function(E element) keyOf, int Function(K a, K b) compare,
+      [int start = 0, int? end]) {
+    quickSortBy(this, keyOf, compare, start, end);
+  }
+
+  /// Sorts elements by the natural order of their [keyOf] property.
+  ///
+  /// Sorts elements from [start] to [end], defaulting to the entire list.
+  void sortBy<K extends Comparable<K>>(K Function(E element) keyOf,
+      [int start = 0, int? end]) {
+    quickSortBy<E, K>(this, keyOf, compareComparable, start, end);
+  }
+
+  /// Shuffle a range of elements.
+  void shuffleRange(int start, int end, [Random? random]) {
+    RangeError.checkValidRange(start, end, length);
+    shuffle(this, start, end, random);
+  }
+
+  /// Reverses the elements in a range of the list.
+  void reverseRange(int start, int end) {
+    RangeError.checkValidRange(start, end, length);
+    while (start < --end) {
+      var tmp = this[start];
+      this[start] = this[end];
+      this[end] = tmp;
+      start += 1;
+    }
+  }
+
+  /// Swaps two elements of this list.
+  void swap(int index1, int index2) {
+    RangeError.checkValidIndex(index1, this, 'index1');
+    RangeError.checkValidIndex(index2, this, 'index2');
+    var tmp = this[index1];
+    this[index1] = this[index2];
+    this[index2] = tmp;
+  }
+
+  /// A fixed length view of a range of this list.
+  ///
+  /// The view is backed by this this list, which must not
+  /// change its length while the view is being used.
+  ///
+  /// The view can be used to perform specific whole-list
+  /// actions on a part of the list.
+  /// For example, to see if a list contains more than one
+  /// "marker" element, you can do:
+  /// ```dart
+  /// someList.slice(someList.indexOf(marker) + 1).contains(marker)
+  /// ```
+  ListSlice<E> slice(int start, [int? end]) {
+    end = RangeError.checkValidRange(start, end, length);
+    var self = this;
+    if (self is ListSlice) return self.slice(start, end);
+    return ListSlice<E>(this, start, end);
+  }
+
+  /// Whether [other] has the same elements as this list.
+  ///
+  /// Returns true iff [other] has the same [length]
+  /// as this list, and the elemets of this list and [other]
+  /// at the same indices are equal according to [equality],
+  /// which defaults to using `==`.
+  bool equals(List<E> other, [Equality<E> equality = const DefaultEquality()]) {
+    if (length != other.length) return false;
+    for (var i = 0; i < length; i++) {
+      if (!equality.equals(this[i], other[i])) return false;
+    }
+    return true;
+  }
+}
+
+/// Various extensions on lists of comparable elements.
+extension ListComparableExtensions<E extends Comparable<E>> on List<E> {
+  /// Returns the index of [element] in this sorted list.
+  ///
+  /// Uses binary search to find the location of [element].
+  /// The list *must* be sorted according to [compare],
+  /// otherwise the result is unspecified.
+  /// If [compare] is omitted, it uses the natural order of the elements.
+  ///
+  /// Returns -1 if [element] does not occur in this list.
+  int binarySearch(E element, [int Function(E, E)? compare]) =>
+      algorithms.binarySearchBy<E, E>(
+          this, identity, compare ?? compareComparable, element);
+
+  /// Returns the index where [element] should be in this sorted list.
+  ///
+  /// Uses binary search to find the location of where [element] should be.
+  /// The list *must* be sorted according to [compare],
+  /// otherwise the result is unspecified.
+  /// If [compare] is omitted, it uses the natural order of the elements.
+  ///
+  /// If [element] does not occur in this list, the returned index is
+  /// the first index where inserting [element] would keep the list
+  /// sorted.
+  int lowerBound(E element, [int Function(E, E)? compare]) =>
+      algorithms.lowerBoundBy<E, E>(
+          this, identity, compare ?? compareComparable, element);
+
+  /// Sort a range of elements by [compare].
+  ///
+  /// If [compare] is omitted, the range is sorted according to the
+  /// natural ordering of the elements.
+  void sortRange(int start, int end, [int Function(E a, E b)? compare]) {
+    RangeError.checkValidRange(start, end, length);
+    algorithms.quickSortBy<E, E>(
+        this, identity, compare ?? compareComparable, start, end);
+  }
+}
+
+/// A list view of a range of another list.
+///
+/// Wraps the range of the [source] list from [start] to [end]
+/// and acts like a fixed-length list view of that range.
+/// The source list must not change length while a list slice is being used.
+class ListSlice<E> extends ListBase<E> {
+  /// Original length of [source].
+  ///
+  /// Used to detect modifications to [source] which may invalidate
+  /// the slice.
+  final int _initialSize;
+
+  /// The original list backing this slice.
+  final List<E> source;
+
+  /// The start index of the slice.
+  final int start;
+
+  @override
+  final int length;
+
+  /// Creates a slice of [source] from [start] to [end].
+  ListSlice(this.source, this.start, int end)
+      : length = end - start,
+        _initialSize = source.length {
+    RangeError.checkValidRange(start, end, source.length);
+  }
+
+  // No argument checking, for internal use.
+  ListSlice._(this._initialSize, this.source, this.start, this.length);
+
+  /// The end index of the slice.
+  int get end => start + length;
+
+  @override
+  E operator [](int index) {
+    if (source.length != _initialSize) {
+      throw ConcurrentModificationError(source);
+    }
+    RangeError.checkValidIndex(index, this, null, length);
+    return source[start + index];
+  }
+
+  @override
+  void operator []=(int index, E value) {
+    if (source.length != _initialSize) {
+      throw ConcurrentModificationError(source);
+    }
+    RangeError.checkValidIndex(index, this, null, length);
+    source[start + index] = value;
+  }
+
+  @override
+  void setRange(int start, int end, Iterable<E> iterable, [int skipCount = 0]) {
+    if (source.length != _initialSize) {
+      throw ConcurrentModificationError(source);
+    }
+    RangeError.checkValidRange(start, end, length);
+    source.setRange(start + start, start + end, iterable, skipCount);
+  }
+
+  /// A fixed length view of a range of this list.
+  ///
+  /// The view is backed by this this list, which must not
+  /// change its length while the view is being used.
+  ///
+  /// The view can be used to perform specific whole-list
+  /// actions on a part of the list.
+  /// For example, to see if a list contains more than one
+  /// "marker" element, you can do:
+  /// ```dart
+  /// someList.slice(someList.indexOf(marker) + 1).contains(marker)
+  /// ```
+  ListSlice<E> slice(int start, [int? end]) {
+    end = RangeError.checkValidRange(start, end, length);
+    return ListSlice._(_initialSize, source, start + start, end - start);
+  }
+
+  @override
+  void shuffle([Random? random]) {
+    if (source.length != _initialSize) {
+      throw ConcurrentModificationError(source);
+    }
+    algorithms.shuffle(source, start, end, random);
+  }
+
+  @override
+  void sort([int Function(E a, E b)? compare]) {
+    if (source.length != _initialSize) {
+      throw ConcurrentModificationError(source);
+    }
+    compare ??= defaultCompare;
+    quickSort(source, compare, start, start + length);
+  }
+
+  /// Sort a range of elements by [compare].
+  void sortRange(int start, int end, int Function(E a, E b) compare) {
+    if (source.length != _initialSize) {
+      throw ConcurrentModificationError(source);
+    }
+    source.sortRange(start, end, compare);
+  }
+
+  /// Shuffles a range of elements.
+  ///
+  /// If [random] is omitted, a new instance of [Random] is used.
+  void shuffleRange(int start, int end, [Random? random]) {
+    if (source.length != _initialSize) {
+      throw ConcurrentModificationError(source);
+    }
+    RangeError.checkValidRange(start, end, length);
+    algorithms.shuffle(source, this.start + start, this.start + end, random);
+  }
+
+  /// Reverses a range of elements.
+  void reverseRange(int start, int end) {
+    RangeError.checkValidRange(start, end, length);
+    source.reverseRange(this.start + start, this.start + end);
+  }
+
+  // Act like a fixed-length list.
+
+  @override
+  set length(int newLength) {
+    throw UnsupportedError('Cannot change the length of a fixed-length list');
+  }
+
+  @override
+  void add(E element) {
+    throw UnsupportedError('Cannot add to a fixed-length list');
+  }
+
+  @override
+  void insert(int index, E element) {
+    throw UnsupportedError('Cannot add to a fixed-length list');
+  }
+
+  @override
+  void insertAll(int index, Iterable<E> iterable) {
+    throw UnsupportedError('Cannot add to a fixed-length list');
+  }
+
+  @override
+  void addAll(Iterable<E> iterable) {
+    throw UnsupportedError('Cannot add to a fixed-length list');
+  }
+
+  @override
+  bool remove(Object? element) {
+    throw UnsupportedError('Cannot remove from a fixed-length list');
+  }
+
+  @override
+  void removeWhere(bool Function(E element) test) {
+    throw UnsupportedError('Cannot remove from a fixed-length list');
+  }
+
+  @override
+  void retainWhere(bool Function(E element) test) {
+    throw UnsupportedError('Cannot remove from a fixed-length list');
+  }
+
+  @override
+  void clear() {
+    throw UnsupportedError('Cannot clear a fixed-length list');
+  }
+
+  @override
+  E removeAt(int index) {
+    throw UnsupportedError('Cannot remove from a fixed-length list');
+  }
+
+  @override
+  E removeLast() {
+    throw UnsupportedError('Cannot remove from a fixed-length list');
+  }
+
+  @override
+  void removeRange(int start, int end) {
+    throw UnsupportedError('Cannot remove from a fixed-length list');
+  }
+
+  @override
+  void replaceRange(int start, int end, Iterable<E> newContents) {
+    throw UnsupportedError('Cannot remove from a fixed-length list');
+  }
+}
diff --git a/lib/src/priority_queue.dart b/lib/src/priority_queue.dart
index 6952a81..345d6f7 100644
--- a/lib/src/priority_queue.dart
+++ b/lib/src/priority_queue.dart
@@ -202,7 +202,7 @@
   /// is the case, `E` must implement [Comparable], and this is checked at
   /// runtime for every comparison.
   HeapPriorityQueue([int Function(E, E)? comparison])
-      : comparison = comparison ?? defaultCompare<E>();
+      : comparison = comparison ?? defaultCompare;
 
   E _elementAt(int index) => _queue[index] ?? (null as E);
 
diff --git a/lib/src/utils.dart b/lib/src/utils.dart
index 00ea932..64088f0 100644
--- a/lib/src/utils.dart
+++ b/lib/src/utils.dart
@@ -2,6 +2,19 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
-/// Returns a [Comparator] that asserts that its first argument is comparable.
-Comparator<T> defaultCompare<T>() =>
-    (value1, value2) => (value1 as Comparable).compareTo(value2);
+/// A [Comparator] that asserts that its first argument is comparable.
+///
+/// The function behaves just like [List.sort]'s
+/// default comparison function. It is entirely dynamic in its testing.
+///
+/// Should be used when optimistically comparing object that are assumed
+/// to be comparable.
+/// If the elements are known to be comparable, use [compareComparable].
+int defaultCompare(Object? value1, Object? value2) =>
+    (value1 as Comparable<Object?>).compareTo(value2);
+
+/// A reusable identity function at any type.
+T identity<T>(T value) => value;
+
+/// A reusable typed comparable comparator.
+int compareComparable<T extends Comparable<T>>(T a, T b) => a.compareTo(b);
diff --git a/pubspec.yaml b/pubspec.yaml
index e256638..b94841d 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -8,7 +8,7 @@
   sdk: '>=2.10.0-78 <2.11.0'
 
 dev_dependencies:
-  pedantic: ^1.0.0
+  pedantic: ^1.9.0
   test: ^1.0.0
 
 dependency_overrides:
diff --git a/test/algorithms_test.dart b/test/algorithms_test.dart
index 7ab096f..0c60463 100644
--- a/test/algorithms_test.dart
+++ b/test/algorithms_test.dart
@@ -6,6 +6,7 @@
 import 'dart:math';
 
 import 'package:collection/collection.dart';
+import 'package:collection/src/algorithms.dart';
 import 'package:test/test.dart';
 
 void main() {
@@ -154,58 +155,159 @@
     expect(lowerBound(l2, C(5), compare: compareC), equals(1));
   });
 
-  test('insertionSortRandom', () {
-    var random = Random();
-    for (var i = 0; i < 25; i++) {
-      var list = [
-        for (var j = 0; j < i; j++)
-          random.nextInt(25) // Expect some equal elements.
-      ];
-      insertionSort(list);
-      for (var j = 1; j < i; j++) {
-        expect(list[j - 1], lessThanOrEqualTo(list[j]));
+  void testSort(String name,
+      void Function(List<int> elements, [int? start, int? end]) sort) {
+    test('${name}Random', () {
+      var random = Random();
+      for (var i = 0; i < 250; i += 10) {
+        var list = [
+          for (var j = 0; j < i; j++)
+            random.nextInt(25) // Expect some equal elements.
+        ];
+        sort(list);
+        for (var j = 1; j < i; j++) {
+          expect(list[j - 1], lessThanOrEqualTo(list[j]));
+        }
       }
-    }
+    });
+
+    test('${name}SubRanges', () {
+      var l = [6, 5, 4, 3, 2, 1];
+      sort(l, 2, 4);
+      expect(l, equals([6, 5, 3, 4, 2, 1]));
+      sort(l, 1, 1);
+      expect(l, equals([6, 5, 3, 4, 2, 1]));
+      sort(l, 4, 6);
+      expect(l, equals([6, 5, 3, 4, 1, 2]));
+      sort(l, 0, 2);
+      expect(l, equals([5, 6, 3, 4, 1, 2]));
+      sort(l, 0, 6);
+      expect(l, equals([1, 2, 3, 4, 5, 6]));
+    });
+
+    test('$name insertionSortSpecialCases', () {
+      var l = [6, 6, 6, 6, 6, 6];
+      sort(l);
+      expect(l, equals([6, 6, 6, 6, 6, 6]));
+
+      l = [6, 6, 3, 3, 0, 0];
+      sort(l);
+      expect(l, equals([0, 0, 3, 3, 6, 6]));
+    });
+  }
+
+  int intId(int x) => x;
+  int intCompare(int a, int b) => a - b;
+  testSort('insertionSort', (list, [start, end]) {
+    insertionSortBy(list, intId, intCompare, start ?? 0, end ?? list.length);
   });
-
-  test('insertionSortSubRanges', () {
-    var l = [6, 5, 4, 3, 2, 1];
-    insertionSort(l, start: 2, end: 4);
-    expect(l, equals([6, 5, 3, 4, 2, 1]));
-    insertionSort(l, start: 1, end: 1);
-    expect(l, equals([6, 5, 3, 4, 2, 1]));
-    insertionSort(l, start: 4, end: 6);
-    expect(l, equals([6, 5, 3, 4, 1, 2]));
-    insertionSort(l, start: 0, end: 2);
-    expect(l, equals([5, 6, 3, 4, 1, 2]));
-    insertionSort(l, start: 0, end: 6);
-    expect(l, equals([1, 2, 3, 4, 5, 6]));
+  testSort('mergeSort compare', (list, [start, end]) {
+    mergeSort(list,
+        start: start ?? 0, end: end ?? list.length, compare: intCompare);
   });
-
-  test('insertionSortSpecialCases', () {
-    var l = [6, 6, 6, 6, 6, 6];
-    insertionSort(l);
-    expect(l, equals([6, 6, 6, 6, 6, 6]));
-
-    l = [6, 6, 3, 3, 0, 0];
-    insertionSort(l);
-    expect(l, equals([0, 0, 3, 3, 6, 6]));
+  testSort('mergeSort comparable', (list, [start, end]) {
+    mergeSort(list, start: start ?? 0, end: end ?? list.length);
   });
-
-  test('MergeSortRandom', () {
-    var random = Random();
-    for (var i = 0; i < 250; i += 1) {
-      var list = [
-        for (var j = 0; j < i; j++)
-          random.nextInt(i) // Expect some equal elements.
-      ];
+  testSort('mergeSortBy', (list, [start, end]) {
+    mergeSortBy(list, intId, intCompare, start ?? 0, end ?? list.length);
+  });
+  testSort('quickSort', (list, [start, end]) {
+    quickSort(list, intCompare, start ?? 0, end ?? list.length);
+  });
+  testSort('quickSortBy', (list, [start, end]) {
+    quickSortBy(list, intId, intCompare, start ?? 0, end ?? list.length);
+  });
+  test('MergeSortSpecialCases', () {
+    for (var size in [511, 512, 513]) {
+      // All equal.
+      var list = List<OC>.generate(size, (i) => OC(0, i));
       mergeSort(list);
-      for (var j = 1; j < i; j++) {
-        expect(list[j - 1], lessThanOrEqualTo(list[j]));
+      for (var i = 0; i < size; i++) {
+        expect(list[i].order, equals(i));
+      }
+      // All but one equal, first.
+      list[0] = OC(1, 0);
+      for (var i = 1; i < size; i++) {
+        list[i] = OC(0, i);
+      }
+      mergeSort(list);
+      for (var i = 0; i < size - 1; i++) {
+        expect(list[i].order, equals(i + 1));
+      }
+      expect(list[size - 1].order, equals(0));
+
+      // All but one equal, last.
+      for (var i = 0; i < size - 1; i++) {
+        list[i] = OC(0, i);
+      }
+      list[size - 1] = OC(-1, size - 1);
+      mergeSort(list);
+      expect(list[0].order, equals(size - 1));
+      for (var i = 1; i < size; i++) {
+        expect(list[i].order, equals(i - 1));
+      }
+
+      // Reversed.
+      for (var i = 0; i < size; i++) {
+        list[i] = OC(size - 1 - i, i);
+      }
+      mergeSort(list);
+      for (var i = 0; i < size; i++) {
+        expect(list[i].id, equals(i));
+        expect(list[i].order, equals(size - 1 - i));
       }
     }
   });
 
+  void testSortBy(
+      String name,
+      void Function<T, K>(List<T> elements, K Function(T element) keyOf,
+              int Function(K a, K b) compare,
+              [int start, int end])
+          sort) {
+    for (var n in [0, 1, 2, 10, 75, 250]) {
+      var name2 = name;
+      test('$name2: Same #$n', () {
+        var list = List<OC>.generate(n, (i) => OC(i, 0));
+        // Should succeed. Bad implementations of, e.g., quicksort can diverge.
+        sort(list, ocOrder, compareInt);
+      });
+      test('$name: Pre-sorted #$n', () {
+        var list = List<OC>.generate(n, (i) => OC(-i, i));
+        var expected = list.toList();
+        sort(list, ocOrder, compareInt);
+        // Elements have not moved.
+        expect(list, expected);
+      });
+      test('$name: Reverse-sorted #$n', () {
+        var list = List<OC>.generate(n, (i) => OC(i, -i));
+        sort(list, ocOrder, compareInt);
+        expectSorted(list, ocOrder, compareInt);
+      });
+      test('$name: Random #$n', () {
+        var random = Random();
+        var list = List<OC>.generate(n, (i) => OC(i, random.nextInt(n)));
+        sort(list, ocOrder, compareInt);
+        expectSorted(list, ocOrder, compareInt);
+      });
+      test('$name: Sublist #$n', () {
+        var random = Random();
+        var list = List<OC>.generate(n, (i) => OC(i, random.nextInt(n)));
+        var original = list.toList();
+        var start = n ~/ 4;
+        var end = start * 3;
+        sort(list, ocOrder, compareInt, start, end);
+        expectSorted(list, ocOrder, compareInt, start, end);
+        expect(list.sublist(0, start), original.sublist(0, start));
+        expect(list.sublist(end), original.sublist(end));
+      });
+    }
+  }
+
+  testSortBy('insertionSort', insertionSortBy);
+  testSortBy('mergeSort', mergeSortBy);
+  testSortBy('quickSortBy', quickSortBy);
+
   test('MergeSortPreservesOrder', () {
     var random = Random();
     // Small case where only insertion call is called,
@@ -251,48 +353,6 @@
     }
   });
 
-  test('MergeSortSpecialCases', () {
-    for (var size in [511, 512, 513]) {
-      // All equal.
-      var list = [for (var i = 0; i < size; i++) OC(0, i)];
-      mergeSort(list);
-      for (var i = 0; i < size; i++) {
-        expect(list[i].order, equals(i));
-      }
-      // All but one equal, first.
-      list[0] = OC(1, 0);
-      for (var i = 1; i < size; i++) {
-        list[i] = OC(0, i);
-      }
-      mergeSort(list);
-      for (var i = 0; i < size - 1; i++) {
-        expect(list[i].order, equals(i + 1));
-      }
-      expect(list[size - 1].order, equals(0));
-
-      // All but one equal, last.
-      for (var i = 0; i < size - 1; i++) {
-        list[i] = OC(0, i);
-      }
-      list[size - 1] = OC(-1, size - 1);
-      mergeSort(list);
-      expect(list[0].order, equals(size - 1));
-      for (var i = 1; i < size; i++) {
-        expect(list[i].order, equals(i - 1));
-      }
-
-      // Reversed.
-      for (var i = 0; i < size; i++) {
-        list[i] = OC(size - 1 - i, i);
-      }
-      mergeSort(list);
-      for (var i = 0; i < size; i++) {
-        expect(list[i].id, equals(i));
-        expect(list[i].order, equals(size - 1 - i));
-      }
-    }
-  });
-
   test('Reverse', () {
     var l = [6, 5, 4, 3, 2, 1];
     reverse(l, 2, 4);
@@ -314,13 +374,36 @@
 }
 
 int compareC(C one, C other) => one.id - other.id;
+int cId(C c) => c.id;
 
+/// Class naturally ordered by its first constructor argument.
 class OC implements Comparable<OC> {
   final int id;
   final int order;
   OC(this.id, this.order);
+
   @override
   int compareTo(OC other) => id - other.id;
+
   @override
   String toString() => 'OC[$id,$order]';
 }
+
+int ocId(OC oc) => oc.id;
+int ocOrder(OC oc) => oc.order;
+
+int compareInt(int a, int b) => a - b;
+
+/// Check that a list is sorted according to [compare] of [keyOf] of elements.
+void expectSorted<T, K>(
+    List<T> list, K Function(T element) keyOf, int Function(K a, K b) compare,
+    [int start = 0, int? end]) {
+  end ??= list.length;
+  if (start == end) return;
+  var prev = keyOf(list[start]);
+  for (var i = start + 1; i < end; i++) {
+    var next = keyOf(list[i]);
+    expect(compare(prev, next), isNonPositive);
+    prev = next;
+  }
+}
diff --git a/test/extensions_test.dart b/test/extensions_test.dart
new file mode 100644
index 0000000..3289757
--- /dev/null
+++ b/test/extensions_test.dart
@@ -0,0 +1,1659 @@
+// Copyright (c) 2020, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:math' show pow;
+
+import 'package:test/test.dart';
+
+import 'package:collection/collection.dart';
+
+void main() {
+  group('Iterable', () {
+    group('of any', () {
+      group('.whereNot', () {
+        test('empty', () {
+          expect(iterable([]).whereNot(unreachable), isEmpty);
+        });
+        test('none', () {
+          expect(iterable([1, 3, 5]).whereNot((e) => e.isOdd), isEmpty);
+        });
+        test('all', () {
+          expect(iterable([1, 3, 5]).whereNot((e) => e.isEven),
+              iterable([1, 3, 5]));
+        });
+        test('some', () {
+          expect(iterable([1, 2, 3, 4]).whereNot((e) => e.isEven),
+              iterable([1, 3]));
+        });
+      });
+      group('.sorted', () {
+        test('empty', () {
+          expect(iterable(<int>[]).sorted(unreachable), []);
+        });
+        test('singleton', () {
+          expect(iterable([1]).sorted(unreachable), [1]);
+        });
+        test('multiple', () {
+          expect(iterable([5, 2, 4, 3, 1]).sorted(cmpInt), [1, 2, 3, 4, 5]);
+        });
+      });
+      group('.sortedBy', () {
+        test('empty', () {
+          expect(iterable(<int>[]).sortedBy(unreachable), []);
+        });
+        test('singleton', () {
+          expect(iterable(<int>[1]).sortedBy(unreachable), [1]);
+        });
+        test('multiple', () {
+          expect(iterable(<int>[3, 20, 100]).sortedBy(toString), [100, 20, 3]);
+        });
+      });
+      group('.sortedByCompare', () {
+        test('empty', () {
+          expect(
+              iterable(<int>[]).sortedByCompare(unreachable, unreachable), []);
+        });
+        test('singleton', () {
+          expect(iterable(<int>[2]).sortedByCompare(unreachable, unreachable),
+              [2]);
+        });
+        test('multiple', () {
+          expect(
+              iterable(<int>[30, 2, 100])
+                  .sortedByCompare(toString, cmpParseInverse),
+              [100, 30, 2]);
+        });
+      });
+      group('isSorted', () {
+        test('empty', () {
+          expect(iterable(<int>[]).isSorted(unreachable), true);
+        });
+        test('single', () {
+          expect(iterable([1]).isSorted(unreachable), true);
+        });
+        test('same', () {
+          expect(iterable([1, 1, 1, 1]).isSorted(cmpInt), true);
+          expect(iterable([1, 0, 1, 0]).isSorted(cmpMod(2)), true);
+        });
+        test('multiple', () {
+          expect(iterable([1, 2, 3, 4]).isSorted(cmpInt), true);
+          expect(iterable([4, 3, 2, 1]).isSorted(cmpIntInverse), true);
+          expect(iterable([1, 2, 3, 0]).isSorted(cmpInt), false);
+          expect(iterable([4, 1, 2, 3]).isSorted(cmpInt), false);
+          expect(iterable([4, 3, 2, 1]).isSorted(cmpInt), false);
+        });
+      });
+      group('.isSortedBy', () {
+        test('empty', () {
+          expect(iterable(<int>[]).isSortedBy(unreachable), true);
+        });
+        test('single', () {
+          expect(iterable([1]).isSortedBy(toString), true);
+        });
+        test('same', () {
+          expect(iterable([1, 1, 1, 1]).isSortedBy(toString), true);
+        });
+        test('multiple', () {
+          expect(iterable([1, 2, 3, 4]).isSortedBy(toString), true);
+          expect(iterable([4, 3, 2, 1]).isSortedBy(toString), false);
+          expect(iterable([1000, 200, 30, 4]).isSortedBy(toString), true);
+          expect(iterable([1, 2, 3, 0]).isSortedBy(toString), false);
+          expect(iterable([4, 1, 2, 3]).isSortedBy(toString), false);
+          expect(iterable([4, 3, 2, 1]).isSortedBy(toString), false);
+        });
+      });
+      group('.isSortedByCompare', () {
+        test('empty', () {
+          expect(iterable(<int>[]).isSortedByCompare(unreachable, unreachable),
+              true);
+        });
+        test('single', () {
+          expect(iterable([1]).isSortedByCompare(toString, unreachable), true);
+        });
+        test('same', () {
+          expect(iterable([1, 1, 1, 1]).isSortedByCompare(toString, cmpParse),
+              true);
+        });
+        test('multiple', () {
+          expect(iterable([1, 2, 3, 4]).isSortedByCompare(toString, cmpParse),
+              true);
+          expect(
+              iterable([4, 3, 2, 1])
+                  .isSortedByCompare(toString, cmpParseInverse),
+              true);
+          expect(
+              iterable([1000, 200, 30, 4])
+                  .isSortedByCompare(toString, cmpString),
+              true);
+          expect(iterable([1, 2, 3, 0]).isSortedByCompare(toString, cmpParse),
+              false);
+          expect(iterable([4, 1, 2, 3]).isSortedByCompare(toString, cmpParse),
+              false);
+          expect(iterable([4, 3, 2, 1]).isSortedByCompare(toString, cmpParse),
+              false);
+        });
+      });
+      group('.forEachIndexed', () {
+        test('empty', () {
+          iterable([]).forEachIndexed(unreachable);
+        });
+        test('single', () {
+          var log = [];
+          iterable(['a']).forEachIndexed((i, s) {
+            log..add(i)..add(s);
+          });
+          expect(log, [0, 'a']);
+        });
+        test('multiple', () {
+          var log = [];
+          iterable(['a', 'b', 'c']).forEachIndexed((i, s) {
+            log..add(i)..add(s);
+          });
+          expect(log, [0, 'a', 1, 'b', 2, 'c']);
+        });
+      });
+      group('.forEachWhile', () {
+        test('empty', () {
+          iterable([]).forEachWhile(unreachable);
+        });
+        test('single true', () {
+          var log = [];
+          iterable(['a']).forEachWhile((s) {
+            log.add(s);
+            return true;
+          });
+          expect(log, ['a']);
+        });
+        test('single false', () {
+          var log = [];
+          iterable(['a']).forEachWhile((s) {
+            log.add(s);
+            return false;
+          });
+          expect(log, ['a']);
+        });
+        test('multiple one', () {
+          var log = [];
+          iterable(['a', 'b', 'c']).forEachWhile((s) {
+            log.add(s);
+            return false;
+          });
+          expect(log, ['a']);
+        });
+        test('multiple all', () {
+          var log = [];
+          iterable(['a', 'b', 'c']).forEachWhile((s) {
+            log.add(s);
+            return true;
+          });
+          expect(log, ['a', 'b', 'c']);
+        });
+        test('multiple some', () {
+          var log = [];
+          iterable(['a', 'b', 'c']).forEachWhile((s) {
+            log.add(s);
+            return s != 'b';
+          });
+          expect(log, ['a', 'b']);
+        });
+      });
+      group('.forEachIndexedWhile', () {
+        test('empty', () {
+          iterable([]).forEachIndexedWhile(unreachable);
+        });
+        test('single true', () {
+          var log = [];
+          iterable(['a']).forEachIndexedWhile((i, s) {
+            log..add(i)..add(s);
+            return true;
+          });
+          expect(log, [0, 'a']);
+        });
+        test('single false', () {
+          var log = [];
+          iterable(['a']).forEachIndexedWhile((i, s) {
+            log..add(i)..add(s);
+            return false;
+          });
+          expect(log, [0, 'a']);
+        });
+        test('multiple one', () {
+          var log = [];
+          iterable(['a', 'b', 'c']).forEachIndexedWhile((i, s) {
+            log..add(i)..add(s);
+            return false;
+          });
+          expect(log, [0, 'a']);
+        });
+        test('multiple all', () {
+          var log = [];
+          iterable(['a', 'b', 'c']).forEachIndexedWhile((i, s) {
+            log..add(i)..add(s);
+            return true;
+          });
+          expect(log, [0, 'a', 1, 'b', 2, 'c']);
+        });
+        test('multiple some', () {
+          var log = [];
+          iterable(['a', 'b', 'c']).forEachIndexedWhile((i, s) {
+            log..add(i)..add(s);
+            return s != 'b';
+          });
+          expect(log, [0, 'a', 1, 'b']);
+        });
+      });
+      group('.mapIndexed', () {
+        test('empty', () {
+          expect(iterable(<String>[]).mapIndexed(unreachable), isEmpty);
+        });
+        test('multiple', () {
+          expect(iterable(<String>['a', 'b']).mapIndexed((i, s) => [i, s]), [
+            [0, 'a'],
+            [1, 'b']
+          ]);
+        });
+      });
+      group('.whereIndexed', () {
+        test('empty', () {
+          expect(iterable(<String>[]).whereIndexed(unreachable), isEmpty);
+        });
+        test('none', () {
+          var trace = [];
+          int log(int a, int b) {
+            trace..add(a)..add(b);
+            return b;
+          }
+
+          expect(
+              iterable(<int>[1, 3, 5, 7])
+                  .whereIndexed((i, x) => log(i, x).isEven),
+              isEmpty);
+          expect(trace, [0, 1, 1, 3, 2, 5, 3, 7]);
+        });
+        test('all', () {
+          expect(iterable(<int>[1, 3, 5, 7]).whereIndexed((i, x) => x.isOdd),
+              [1, 3, 5, 7]);
+        });
+        test('some', () {
+          expect(iterable(<int>[1, 3, 5, 7]).whereIndexed((i, x) => i.isOdd),
+              [3, 7]);
+        });
+      });
+      group('.whereNotIndexed', () {
+        test('empty', () {
+          expect(iterable(<int>[]).whereNotIndexed(unreachable), isEmpty);
+        });
+        test('none', () {
+          var trace = [];
+          int log(int a, int b) {
+            trace..add(a)..add(b);
+            return b;
+          }
+
+          expect(
+              iterable(<int>[1, 3, 5, 7])
+                  .whereNotIndexed((i, x) => log(i, x).isOdd),
+              isEmpty);
+          expect(trace, [0, 1, 1, 3, 2, 5, 3, 7]);
+        });
+        test('all', () {
+          expect(
+              iterable(<int>[1, 3, 5, 7]).whereNotIndexed((i, x) => x.isEven),
+              [1, 3, 5, 7]);
+        });
+        test('some', () {
+          expect(iterable(<int>[1, 3, 5, 7]).whereNotIndexed((i, x) => i.isOdd),
+              [1, 5]);
+        });
+      });
+      group('.expandIndexed', () {
+        test('empty', () {
+          expect(iterable(<int>[]).expandIndexed(unreachable), isEmpty);
+        });
+        test('empty result', () {
+          expect(iterable(['a', 'b']).expandIndexed((i, v) => []), isEmpty);
+        });
+        test('larger result', () {
+          expect(iterable(['a', 'b']).expandIndexed((i, v) => ['$i', v]),
+              ['0', 'a', '1', 'b']);
+        });
+        test('varying result', () {
+          expect(
+              iterable(['a', 'b'])
+                  .expandIndexed((i, v) => i.isOdd ? ['$i', v] : []),
+              ['1', 'b']);
+        });
+      });
+      group('.reduceIndexed', () {
+        test('empty', () {
+          expect(() => iterable([]).reduceIndexed((i, a, b) => a),
+              throwsStateError);
+        });
+        test('single', () {
+          expect(iterable([1]).reduceIndexed(unreachable), 1);
+        });
+        test('multiple', () {
+          expect(
+              iterable([1, 4, 2])
+                  .reduceIndexed((i, p, v) => p + (pow(i + 1, v) as int)),
+              1 + 16 + 9);
+        });
+      });
+      group('.foldIndexed', () {
+        test('empty', () {
+          expect(iterable([]).foldIndexed(0, unreachable), 0);
+        });
+        test('single', () {
+          expect(
+              iterable([1]).foldIndexed('x', (i, a, b) => '$a:$i:$b'), 'x:0:1');
+        });
+        test('mulitple', () {
+          expect(iterable([1, 3, 9]).foldIndexed('x', (i, a, b) => '$a:$i:$b'),
+              'x:0:1:1:3:2:9');
+        });
+      });
+      group('.firstWhereOrNull', () {
+        test('empty', () {
+          expect(iterable([]).firstWhereOrNull(unreachable), null);
+        });
+        test('none', () {
+          expect(iterable([1, 3, 7]).firstWhereOrNull(isEven), null);
+        });
+        test('single', () {
+          expect(iterable([0, 1, 2]).firstWhereOrNull(isOdd), 1);
+        });
+        test('first of multiple', () {
+          expect(iterable([0, 1, 3]).firstWhereOrNull(isOdd), 1);
+        });
+      });
+      group('.firstWhereIndexedOrNull', () {
+        test('empty', () {
+          expect(iterable([]).firstWhereIndexedOrNull(unreachable), null);
+        });
+        test('none', () {
+          expect(
+              iterable([1, 3, 7]).firstWhereIndexedOrNull((i, x) => x.isEven),
+              null);
+          expect(iterable([1, 3, 7]).firstWhereIndexedOrNull((i, x) => i < 0),
+              null);
+        });
+        test('single', () {
+          expect(iterable([0, 3, 6]).firstWhereIndexedOrNull((i, x) => x.isOdd),
+              3);
+          expect(
+              iterable([0, 3, 6]).firstWhereIndexedOrNull((i, x) => i == 1), 3);
+        });
+        test('first of multiple', () {
+          expect(iterable([0, 3, 7]).firstWhereIndexedOrNull((i, x) => x.isOdd),
+              3);
+          expect(
+              iterable([0, 3, 7]).firstWhereIndexedOrNull((i, x) => i.isEven),
+              0);
+        });
+      });
+      group('.firstOrNull', () {
+        test('empty', () {
+          expect(iterable([]).firstOrNull, null);
+        });
+        test('single', () {
+          expect(iterable([1]).firstOrNull, 1);
+        });
+        test('first of multiple', () {
+          expect(iterable([1, 3, 5]).firstOrNull, 1);
+        });
+      });
+      group('.lastWhereOrNull', () {
+        test('empty', () {
+          expect(iterable([]).lastWhereOrNull(unreachable), null);
+        });
+        test('none', () {
+          expect(iterable([1, 3, 7]).lastWhereOrNull(isEven), null);
+        });
+        test('single', () {
+          expect(iterable([0, 1, 2]).lastWhereOrNull(isOdd), 1);
+        });
+        test('last of multiple', () {
+          expect(iterable([0, 1, 3]).lastWhereOrNull(isOdd), 3);
+        });
+      });
+      group('.lastWhereIndexedOrNull', () {
+        test('empty', () {
+          expect(iterable([]).lastWhereIndexedOrNull(unreachable), null);
+        });
+        test('none', () {
+          expect(iterable([1, 3, 7]).lastWhereIndexedOrNull((i, x) => x.isEven),
+              null);
+          expect(iterable([1, 3, 7]).lastWhereIndexedOrNull((i, x) => i < 0),
+              null);
+        });
+        test('single', () {
+          expect(
+              iterable([0, 3, 6]).lastWhereIndexedOrNull((i, x) => x.isOdd), 3);
+          expect(
+              iterable([0, 3, 6]).lastWhereIndexedOrNull((i, x) => i == 1), 3);
+        });
+        test('last of multiple', () {
+          expect(
+              iterable([0, 3, 7]).lastWhereIndexedOrNull((i, x) => x.isOdd), 7);
+          expect(iterable([0, 3, 7]).lastWhereIndexedOrNull((i, x) => i.isEven),
+              7);
+        });
+      });
+      group('.lastOrNull', () {
+        test('empty', () {
+          expect(iterable([]).lastOrNull, null);
+        });
+        test('single', () {
+          expect(iterable([1]).lastOrNull, 1);
+        });
+        test('last of multiple', () {
+          expect(iterable([1, 3, 5]).lastOrNull, 5);
+        });
+      });
+      group('.singleWhereOrNull', () {
+        test('empty', () {
+          expect(iterable([]).singleWhereOrNull(unreachable), null);
+        });
+        test('none', () {
+          expect(iterable([1, 3, 7]).singleWhereOrNull(isEven), null);
+        });
+        test('single', () {
+          expect(iterable([0, 1, 2]).singleWhereOrNull(isOdd), 1);
+        });
+        test('multiple', () {
+          expect(iterable([0, 1, 3]).singleWhereOrNull(isOdd), null);
+        });
+      });
+      group('.singleWhereIndexedOrNull', () {
+        test('empty', () {
+          expect(iterable([]).singleWhereIndexedOrNull(unreachable), null);
+        });
+        test('none', () {
+          expect(
+              iterable([1, 3, 7]).singleWhereIndexedOrNull((i, x) => x.isEven),
+              null);
+          expect(iterable([1, 3, 7]).singleWhereIndexedOrNull((i, x) => i < 0),
+              null);
+        });
+        test('single', () {
+          expect(
+              iterable([0, 3, 6]).singleWhereIndexedOrNull((i, x) => x.isOdd),
+              3);
+          expect(iterable([0, 3, 6]).singleWhereIndexedOrNull((i, x) => i == 1),
+              3);
+        });
+        test('multiple', () {
+          expect(
+              iterable([0, 3, 7]).singleWhereIndexedOrNull((i, x) => x.isOdd),
+              null);
+          expect(
+              iterable([0, 3, 7]).singleWhereIndexedOrNull((i, x) => i.isEven),
+              null);
+        });
+      });
+      group('.singleOrNull', () {
+        test('empty', () {
+          expect(iterable([]).singleOrNull, null);
+        });
+        test('single', () {
+          expect(iterable([1]).singleOrNull, 1);
+        });
+        test('multiple', () {
+          expect(iterable([1, 3, 5]).singleOrNull, null);
+        });
+      });
+      group('.groupFoldBy', () {
+        test('empty', () {
+          expect(iterable([]).groupFoldBy(unreachable, unreachable), {});
+        });
+        test('single', () {
+          expect(iterable([1]).groupFoldBy(toString, (p, v) => [p, v]), {
+            '1': [null, 1]
+          });
+        });
+        test('multiple', () {
+          expect(
+              iterable([1, 2, 3, 4, 5]).groupFoldBy<bool, String>(
+                  (x) => x.isEven, (p, v) => p == null ? '$v' : '$p:$v'),
+              {true: '2:4', false: '1:3:5'});
+        });
+      });
+      group('.groupSetsBy', () {
+        test('empty', () {
+          expect(iterable([]).groupSetsBy(unreachable), {});
+        });
+        test('multiple same', () {
+          expect(iterable([1, 1]).groupSetsBy(toString), {
+            '1': {1}
+          });
+        });
+        test('multiple', () {
+          expect(iterable([1, 2, 3, 4, 5, 1]).groupSetsBy((x) => x % 3), {
+            1: {1, 4},
+            2: {2, 5},
+            0: {3}
+          });
+        });
+      });
+      group('.groupListsBy', () {
+        test('empty', () {
+          expect(iterable([]).groupListsBy(unreachable), {});
+        });
+        test('multiple saame', () {
+          expect(iterable([1, 1]).groupListsBy(toString), {
+            '1': [1, 1]
+          });
+        });
+        test('multiple', () {
+          expect(iterable([1, 2, 3, 4, 5, 1]).groupListsBy((x) => x % 3), {
+            1: [1, 4, 1],
+            2: [2, 5],
+            0: [3]
+          });
+        });
+      });
+      group('.splitBefore', () {
+        test('empty', () {
+          expect(iterable([]).splitBefore(unreachable), []);
+        });
+        test('single', () {
+          expect(iterable([1]).splitBefore(unreachable), [
+            [1]
+          ]);
+        });
+        test('no split', () {
+          var trace = [];
+          bool log(x) {
+            trace.add(x);
+            return false;
+          }
+
+          expect(iterable([1, 2, 3]).splitBefore(log), [
+            [1, 2, 3]
+          ]);
+          expect(trace, [2, 3]);
+        });
+        test('all splits', () {
+          expect(iterable([1, 2, 3]).splitBefore((x) => true), [
+            [1],
+            [2],
+            [3]
+          ]);
+        });
+        test('some splits', () {
+          expect(iterable([1, 2, 3]).splitBefore((x) => x.isEven), [
+            [1],
+            [2, 3]
+          ]);
+        });
+      });
+      group('.splitBeforeIndexed', () {
+        test('empty', () {
+          expect(iterable([]).splitBeforeIndexed(unreachable), []);
+        });
+        test('single', () {
+          expect(iterable([1]).splitBeforeIndexed(unreachable), [
+            [1]
+          ]);
+        });
+        test('no split', () {
+          var trace = [];
+          bool log(i, x) {
+            trace..add('$i')..add(x);
+            return false;
+          }
+
+          expect(iterable([1, 2, 3]).splitBeforeIndexed(log), [
+            [1, 2, 3]
+          ]);
+          expect(trace, ['1', 2, '2', 3]);
+        });
+        test('all splits', () {
+          expect(iterable([1, 2, 3]).splitBeforeIndexed((i, x) => true), [
+            [1],
+            [2],
+            [3]
+          ]);
+        });
+        test('some splits', () {
+          expect(iterable([1, 2, 3]).splitBeforeIndexed((i, x) => x.isEven), [
+            [1],
+            [2, 3]
+          ]);
+          expect(iterable([1, 2, 3]).splitBeforeIndexed((i, x) => i.isEven), [
+            [1, 2],
+            [3]
+          ]);
+        });
+      });
+      group('.splitAfter', () {
+        test('empty', () {
+          expect(iterable([]).splitAfter(unreachable), []);
+        });
+        test('single', () {
+          expect(iterable([1]).splitAfter((x) => false), [
+            [1]
+          ]);
+          expect(iterable([1]).splitAfter((x) => true), [
+            [1]
+          ]);
+        });
+        test('no split', () {
+          var trace = [];
+          bool log(x) {
+            trace.add(x);
+            return false;
+          }
+
+          expect(iterable([1, 2, 3]).splitAfter(log), [
+            [1, 2, 3]
+          ]);
+          expect(trace, [1, 2, 3]);
+        });
+        test('all splits', () {
+          expect(iterable([1, 2, 3]).splitAfter((x) => true), [
+            [1],
+            [2],
+            [3]
+          ]);
+        });
+        test('some splits', () {
+          expect(iterable([1, 2, 3]).splitAfter((x) => x.isEven), [
+            [1, 2],
+            [3]
+          ]);
+        });
+      });
+      group('.splitAfterIndexed', () {
+        test('empty', () {
+          expect(iterable([]).splitAfterIndexed(unreachable), []);
+        });
+        test('single', () {
+          expect(iterable([1]).splitAfterIndexed((i, x) => true), [
+            [1]
+          ]);
+          expect(iterable([1]).splitAfterIndexed((i, x) => false), [
+            [1]
+          ]);
+        });
+        test('no split', () {
+          var trace = [];
+          bool log(i, x) {
+            trace..add('$i')..add(x);
+            return false;
+          }
+
+          expect(iterable([1, 2, 3]).splitAfterIndexed(log), [
+            [1, 2, 3]
+          ]);
+          expect(trace, ['0', 1, '1', 2, '2', 3]);
+        });
+        test('all splits', () {
+          expect(iterable([1, 2, 3]).splitAfterIndexed((i, x) => true), [
+            [1],
+            [2],
+            [3]
+          ]);
+        });
+        test('some splits', () {
+          expect(iterable([1, 2, 3]).splitAfterIndexed((i, x) => x.isEven), [
+            [1, 2],
+            [3]
+          ]);
+          expect(iterable([1, 2, 3]).splitAfterIndexed((i, x) => i.isEven), [
+            [1],
+            [2, 3]
+          ]);
+        });
+      });
+      group('.splitBetween', () {
+        test('empty', () {
+          expect(iterable([]).splitBetween(unreachable), []);
+        });
+        test('single', () {
+          expect(iterable([1]).splitBetween(unreachable), [
+            [1]
+          ]);
+        });
+        test('no split', () {
+          var trace = [];
+          bool log(x, y) {
+            trace.add([x, y]);
+            return false;
+          }
+
+          expect(iterable([1, 2, 3]).splitBetween(log), [
+            [1, 2, 3]
+          ]);
+          expect(trace, [
+            [1, 2],
+            [2, 3]
+          ]);
+        });
+        test('all splits', () {
+          expect(iterable([1, 2, 3]).splitBetween((x, y) => true), [
+            [1],
+            [2],
+            [3]
+          ]);
+        });
+        test('some splits', () {
+          expect(iterable([1, 2, 4]).splitBetween((x, y) => (x ^ y).isEven), [
+            [1, 2],
+            [4]
+          ]);
+        });
+      });
+      group('.splitBetweenIndexed', () {
+        test('empty', () {
+          expect(iterable([]).splitBetweenIndexed(unreachable), []);
+        });
+        test('single', () {
+          expect(iterable([1]).splitBetweenIndexed(unreachable), [
+            [1]
+          ]);
+        });
+        test('no split', () {
+          var trace = [];
+          bool log(i, x, y) {
+            trace.add([i, x, y]);
+            return false;
+          }
+
+          expect(iterable([1, 2, 3]).splitBetweenIndexed(log), [
+            [1, 2, 3]
+          ]);
+          expect(trace, [
+            [1, 1, 2],
+            [2, 2, 3]
+          ]);
+        });
+        test('all splits', () {
+          expect(iterable([1, 2, 3]).splitBetweenIndexed((i, x, y) => true), [
+            [1],
+            [2],
+            [3]
+          ]);
+        });
+        test('some splits', () {
+          expect(
+              iterable([1, 2, 4])
+                  .splitBetweenIndexed((i, x, y) => (x ^ y).isEven),
+              [
+                [1, 2],
+                [4]
+              ]);
+          expect(
+              iterable([1, 2, 4])
+                  .splitBetweenIndexed((i, x, y) => (i ^ y).isEven),
+              [
+                [1, 2],
+                [4]
+              ]);
+        });
+      });
+      group('none', () {
+        test('empty', () {
+          expect(iterable([]).none(unreachable), true);
+        });
+        test('single', () {
+          expect(iterable([1]).none(isEven), true);
+          expect(iterable([1]).none(isOdd), false);
+        });
+        test('multiple', () {
+          expect(iterable([1, 3, 5, 7, 9, 11]).none(isEven), true);
+          expect(iterable([1, 3, 5, 7, 9, 10]).none(isEven), false);
+          expect(iterable([0, 3, 5, 7, 9, 11]).none(isEven), false);
+          expect(iterable([0, 2, 4, 6, 8, 10]).none(isEven), false);
+        });
+      });
+    });
+    group('of nullable', () {
+      group('.whereNotNull', () {
+        test('empty', () {
+          expect(iterable(<int?>[]).whereNotNull(), isEmpty);
+        });
+        test('single', () {
+          expect(iterable(<int?>[null]).whereNotNull(), isEmpty);
+          expect(iterable(<int?>[1]).whereNotNull(), [1]);
+        });
+        test('multiple', () {
+          expect(iterable(<int?>[1, 3, 5]).whereNotNull(), [1, 3, 5]);
+          expect(iterable(<int?>[null, null, null]).whereNotNull(), isEmpty);
+          expect(
+              iterable(<int?>[1, null, 3, null, 5]).whereNotNull(), [1, 3, 5]);
+        });
+      });
+    });
+    group('of number', () {
+      group('.sum', () {
+        test('empty', () {
+          expect(iterable(<int>[]).sum, same(0));
+          expect(iterable(<double>[]).sum, same(0.0));
+          expect(iterable(<num>[]).sum, same(0));
+        });
+        test('single', () {
+          expect(iterable(<int>[1]).sum, same(1));
+          expect(iterable(<double>[1.2]).sum, same(1.2));
+          expect(iterable(<num>[1]).sum, same(1));
+          expect(iterable(<num>[1.2]).sum, same(1.2));
+        });
+        test('multiple', () {
+          expect(iterable(<int>[1, 2, 4]).sum, 7);
+          expect(iterable(<double>[1.2, 3.5]).sum, 4.7);
+          expect(iterable(<num>[1, 3, 5]).sum, same(9));
+          expect(iterable(<num>[1.2, 3.5]).sum, 4.7);
+          expect(iterable(<num>[1.2, 2, 3.5]).sum, 6.7);
+        });
+      });
+      group('average', () {
+        test('empty', () {
+          expect(() => iterable(<int>[]).average, throwsStateError);
+          expect(() => iterable(<double>[]).average, throwsStateError);
+          expect(() => iterable(<num>[]).average, throwsStateError);
+        });
+        test('single', () {
+          expect(iterable(<int>[4]).average, same(4.0));
+          expect(iterable(<double>[3.5]).average, 3.5);
+          expect(iterable(<num>[4]).average, same(4.0));
+          expect(iterable(<num>[3.5]).average, 3.5);
+        });
+        test('multiple', () {
+          expect(iterable(<int>[1, 3, 5]).average, same(3.0));
+          expect(iterable(<int>[1, 3, 5, 9]).average, 4.5);
+          expect(iterable(<double>[1.0, 3.0, 5.0, 9.0]).average, 4.5);
+          expect(iterable(<num>[1, 3, 5, 9]).average, 4.5);
+        });
+      });
+    });
+    group('of iterable', () {
+      group('.flattened', () {
+        var empty = iterable(<int>[]);
+        test('empty', () {
+          expect(iterable(<Iterable<int>>[]).flattened, []);
+        });
+        test('multiple empty', () {
+          expect(iterable([empty, empty, empty]).flattened, []);
+        });
+        test('single value', () {
+          expect(
+              iterable(<Iterable>[
+                iterable([1])
+              ]).flattened,
+              [1]);
+        });
+        test('multiple', () {
+          expect(
+              iterable(<Iterable>[
+                iterable([1, 2]),
+                empty,
+                iterable([3, 4])
+              ]).flattened,
+              [1, 2, 3, 4]);
+        });
+      });
+    });
+    group('of comparable', () {
+      group('.min', () {
+        test('empty', () {
+          expect(() => iterable(<String>[]).min, throwsStateError);
+        });
+        test('single', () {
+          expect(iterable(<String>['a']).min, 'a');
+        });
+        test('multiple', () {
+          expect(iterable(<String>['c', 'a', 'b']).min, 'a');
+        });
+      });
+      group('.minOrNull', () {
+        test('empty', () {
+          expect(iterable(<String>[]).minOrNull, null);
+        });
+        test('single', () {
+          expect(iterable(<String>['a']).minOrNull, 'a');
+        });
+        test('multiple', () {
+          expect(iterable(<String>['c', 'a', 'b']).minOrNull, 'a');
+        });
+      });
+      group('.max', () {
+        test('empty', () {
+          expect(() => iterable(<String>[]).max, throwsStateError);
+        });
+        test('single', () {
+          expect(iterable(<String>['a']).max, 'a');
+        });
+        test('multiple', () {
+          expect(iterable(<String>['b', 'c', 'a']).max, 'c');
+        });
+      });
+      group('.maxOrNull', () {
+        test('empty', () {
+          expect(iterable(<String>[]).maxOrNull, null);
+        });
+        test('single', () {
+          expect(iterable(<String>['a']).maxOrNull, 'a');
+        });
+        test('multiple', () {
+          expect(iterable(<String>['b', 'c', 'a']).maxOrNull, 'c');
+        });
+      });
+    });
+    group('.sorted', () {
+      test('empty', () {
+        expect(iterable(<String>[]).sorted(unreachable), []);
+        expect(iterable(<String>[]).sorted(), []);
+      });
+      test('singleton', () {
+        expect(iterable(['a']).sorted(unreachable), ['a']);
+        expect(iterable(['a']).sorted(), ['a']);
+      });
+      test('multiple', () {
+        expect(iterable(<String>['5', '2', '4', '3', '1']).sorted(cmpParse),
+            ['1', '2', '3', '4', '5']);
+        expect(
+            iterable(<String>['5', '2', '4', '3', '1']).sorted(cmpParseInverse),
+            ['5', '4', '3', '2', '1']);
+        expect(iterable(<String>['5', '2', '4', '3', '1']).sorted(),
+            ['1', '2', '3', '4', '5']);
+        // Large enough to trigger quicksort.
+        var i256 = Iterable<int>.generate(256, (i) => i ^ 0x55);
+        var sorted256 = [...i256]..sort();
+        expect(i256.sorted(cmpInt), sorted256);
+      });
+    });
+    group('.isSorted', () {
+      test('empty', () {
+        expect(iterable(<String>[]).isSorted(unreachable), true);
+        expect(iterable(<String>[]).isSorted(), true);
+      });
+      test('single', () {
+        expect(iterable(['1']).isSorted(unreachable), true);
+        expect(iterable(['1']).isSorted(), true);
+      });
+      test('same', () {
+        expect(iterable(['1', '1', '1', '1']).isSorted(cmpParse), true);
+        expect(iterable(['1', '2', '0', '3']).isSorted(cmpStringLength), true);
+        expect(iterable(['1', '1', '1', '1']).isSorted(), true);
+      });
+      test('multiple', () {
+        expect(iterable(['1', '2', '3', '4']).isSorted(cmpParse), true);
+        expect(iterable(['1', '2', '3', '4']).isSorted(), true);
+        expect(iterable(['4', '3', '2', '1']).isSorted(cmpParseInverse), true);
+        expect(iterable(['1', '2', '3', '0']).isSorted(cmpParse), false);
+        expect(iterable(['1', '2', '3', '0']).isSorted(), false);
+        expect(iterable(['4', '1', '2', '3']).isSorted(cmpParse), false);
+        expect(iterable(['4', '1', '2', '3']).isSorted(), false);
+        expect(iterable(['4', '3', '2', '1']).isSorted(cmpParse), false);
+        expect(iterable(['4', '3', '2', '1']).isSorted(), false);
+      });
+    });
+  });
+
+  group('Comparator', () {
+    test('.inverse', () {
+      var cmpStringInv = cmpString.inverse;
+      expect(cmpString('a', 'b'), isNegative);
+      expect(cmpStringInv('a', 'b'), isPositive);
+      expect(cmpString('aa', 'a'), isPositive);
+      expect(cmpStringInv('aa', 'a'), isNegative);
+      expect(cmpString('a', 'a'), isZero);
+      expect(cmpStringInv('a', 'a'), isZero);
+    });
+    test('.compareBy', () {
+      var cmpByLength = cmpInt.compareBy((String s) => s.length);
+      expect(cmpByLength('a', 'b'), 0);
+      expect(cmpByLength('aa', 'b'), isPositive);
+      expect(cmpByLength('b', 'aa'), isNegative);
+      var cmpByInverseLength = cmpIntInverse.compareBy((String s) => s.length);
+      expect(cmpByInverseLength('a', 'b'), 0);
+      expect(cmpByInverseLength('aa', 'b'), isNegative);
+      expect(cmpByInverseLength('b', 'aa'), isPositive);
+    });
+
+    test('.then', () {
+      var cmpLengthFirst = cmpStringLength.then(cmpString);
+      var strings = ['a', 'aa', 'ba', 'ab', 'b', 'aaa'];
+      strings.sort(cmpString);
+      expect(strings, ['a', 'aa', 'aaa', 'ab', 'b', 'ba']);
+      strings.sort(cmpLengthFirst);
+      expect(strings, ['a', 'b', 'aa', 'ab', 'ba', 'aaa']);
+
+      int cmpFirstLetter(String s1, String s2) =>
+          s1.runes.first - s2.runes.first;
+      var cmpLetterLength = cmpFirstLetter.then(cmpStringLength);
+      var cmpLengthLetter = cmpStringLength.then(cmpFirstLetter);
+      strings = ['a', 'ab', 'b', 'ba', 'aaa'];
+      strings.sort(cmpLetterLength);
+      expect(strings, ['a', 'ab', 'aaa', 'b', 'ba']);
+      strings.sort(cmpLengthLetter);
+      expect(strings, ['a', 'b', 'ab', 'ba', 'aaa']);
+    });
+  });
+
+  group('List', () {
+    group('of any', () {
+      group('.binarySearch', () {
+        test('empty', () {
+          expect(<int>[].binarySearch(1, unreachable), -1);
+        });
+        test('single', () {
+          expect([0].binarySearch(1, cmpInt), -1);
+          expect([1].binarySearch(1, cmpInt), 0);
+          expect([2].binarySearch(1, cmpInt), -1);
+        });
+        test('multiple', () {
+          expect([1, 2, 3, 4, 5, 6].binarySearch(3, cmpInt), 2);
+          expect([6, 5, 4, 3, 2, 1].binarySearch(3, cmpIntInverse), 3);
+        });
+      });
+      group('.binarySearchByCompare', () {
+        test('empty', () {
+          expect(<int>[].binarySearchByCompare(1, toString, cmpParse), -1);
+        });
+        test('single', () {
+          expect([0].binarySearchByCompare(1, toString, cmpParse), -1);
+          expect([1].binarySearchByCompare(1, toString, cmpParse), 0);
+          expect([2].binarySearchByCompare(1, toString, cmpParse), -1);
+        });
+        test('multiple', () {
+          expect(
+              [1, 2, 3, 4, 5, 6].binarySearchByCompare(3, toString, cmpParse),
+              2);
+          expect(
+              [6, 5, 4, 3, 2, 1]
+                  .binarySearchByCompare(3, toString, cmpParseInverse),
+              3);
+        });
+      });
+      group('.binarySearchBy', () {
+        test('empty', () {
+          expect(<int>[].binarySearchBy(1, toString), -1);
+        });
+        test('single', () {
+          expect([0].binarySearchBy(1, toString), -1);
+          expect([1].binarySearchBy(1, toString), 0);
+          expect([2].binarySearchBy(1, toString), -1);
+        });
+        test('multiple', () {
+          expect([1, 2, 3, 4, 5, 6].binarySearchBy(3, toString), 2);
+        });
+      });
+
+      group('.lowerBound', () {
+        test('empty', () {
+          expect(<int>[].lowerBound(1, unreachable), 0);
+        });
+        test('single', () {
+          expect([0].lowerBound(1, cmpInt), 1);
+          expect([1].lowerBound(1, cmpInt), 0);
+          expect([2].lowerBound(1, cmpInt), 0);
+        });
+        test('multiple', () {
+          expect([1, 2, 3, 4, 5, 6].lowerBound(3, cmpInt), 2);
+          expect([6, 5, 4, 3, 2, 1].lowerBound(3, cmpIntInverse), 3);
+          expect([1, 2, 4, 5, 6].lowerBound(3, cmpInt), 2);
+          expect([6, 5, 4, 2, 1].lowerBound(3, cmpIntInverse), 3);
+        });
+      });
+      group('.lowerBoundByCompare', () {
+        test('empty', () {
+          expect(<int>[].lowerBoundByCompare(1, toString, cmpParse), 0);
+        });
+        test('single', () {
+          expect([0].lowerBoundByCompare(1, toString, cmpParse), 1);
+          expect([1].lowerBoundByCompare(1, toString, cmpParse), 0);
+          expect([2].lowerBoundByCompare(1, toString, cmpParse), 0);
+        });
+        test('multiple', () {
+          expect(
+              [1, 2, 3, 4, 5, 6].lowerBoundByCompare(3, toString, cmpParse), 2);
+          expect(
+              [6, 5, 4, 3, 2, 1]
+                  .lowerBoundByCompare(3, toString, cmpParseInverse),
+              3);
+          expect([1, 2, 4, 5, 6].lowerBoundByCompare(3, toString, cmpParse), 2);
+          expect(
+              [6, 5, 4, 2, 1].lowerBoundByCompare(3, toString, cmpParseInverse),
+              3);
+        });
+      });
+      group('.lowerBoundBy', () {
+        test('empty', () {
+          expect(<int>[].lowerBoundBy(1, toString), 0);
+        });
+        test('single', () {
+          expect([0].lowerBoundBy(1, toString), 1);
+          expect([1].lowerBoundBy(1, toString), 0);
+          expect([2].lowerBoundBy(1, toString), 0);
+        });
+        test('multiple', () {
+          expect([1, 2, 3, 4, 5, 6].lowerBoundBy(3, toString), 2);
+          expect([1, 2, 4, 5, 6].lowerBoundBy(3, toString), 2);
+        });
+      });
+      group('sortRange', () {
+        test('errors', () {
+          expect(() => [1].sortRange(-1, 1, cmpInt), throwsArgumentError);
+          expect(() => [1].sortRange(0, 2, cmpInt), throwsArgumentError);
+          expect(() => [1].sortRange(1, 0, cmpInt), throwsArgumentError);
+        });
+        test('empty range', () {
+          <int>[].sortRange(0, 0, unreachable);
+          var list = [3, 2, 1];
+          list.sortRange(0, 0, unreachable);
+          list.sortRange(3, 3, unreachable);
+          expect(list, [3, 2, 1]);
+        });
+        test('single', () {
+          [1].sortRange(0, 1, unreachable);
+          var list = [3, 2, 1];
+          list.sortRange(0, 1, unreachable);
+          list.sortRange(1, 2, unreachable);
+          list.sortRange(2, 3, unreachable);
+        });
+        test('multiple', () {
+          var list = [9, 8, 7, 6, 5, 4, 3, 2, 1];
+          list.sortRange(2, 5, cmpInt);
+          expect(list, [9, 8, 5, 6, 7, 4, 3, 2, 1]);
+          list.sortRange(4, 8, cmpInt);
+          expect(list, [9, 8, 5, 6, 2, 3, 4, 7, 1]);
+          list.sortRange(3, 6, cmpIntInverse);
+          expect(list, [9, 8, 5, 6, 3, 2, 4, 7, 1]);
+        });
+      });
+      group('.sortBy', () {
+        test('empty', () {
+          expect(<int>[]..sortBy(unreachable), []);
+        });
+        test('singleton', () {
+          expect([1]..sortBy(unreachable), [1]);
+        });
+        test('multiple', () {
+          expect([3, 20, 100]..sortBy(toString), [100, 20, 3]);
+        });
+        group('range', () {
+          test('errors', () {
+            expect(() => [1].sortBy(toString, -1, 1), throwsArgumentError);
+            expect(() => [1].sortBy(toString, 0, 2), throwsArgumentError);
+            expect(() => [1].sortBy(toString, 1, 0), throwsArgumentError);
+          });
+          test('empty', () {
+            expect([5, 7, 4, 2, 3]..sortBy(unreachable, 2, 2), [5, 7, 4, 2, 3]);
+          });
+          test('singleton', () {
+            expect([5, 7, 4, 2, 3]..sortBy(unreachable, 2, 3), [5, 7, 4, 2, 3]);
+          });
+          test('multiple', () {
+            expect(
+                [5, 7, 40, 2, 3]..sortBy((a) => '$a', 1, 4), [5, 2, 40, 7, 3]);
+          });
+        });
+      });
+      group('.sortByCompare', () {
+        test('empty', () {
+          expect(<int>[]..sortByCompare(unreachable, unreachable), []);
+        });
+        test('singleton', () {
+          expect([2]..sortByCompare(unreachable, unreachable), [2]);
+        });
+        test('multiple', () {
+          expect([30, 2, 100]..sortByCompare(toString, cmpParseInverse),
+              [100, 30, 2]);
+        });
+        group('range', () {
+          test('errors', () {
+            expect(() => [1].sortByCompare(toString, cmpParse, -1, 1),
+                throwsArgumentError);
+            expect(() => [1].sortByCompare(toString, cmpParse, 0, 2),
+                throwsArgumentError);
+            expect(() => [1].sortByCompare(toString, cmpParse, 1, 0),
+                throwsArgumentError);
+          });
+          test('empty', () {
+            expect(
+                [3, 5, 7, 3, 1]..sortByCompare(unreachable, unreachable, 2, 2),
+                [3, 5, 7, 3, 1]);
+          });
+          test('singleton', () {
+            expect(
+                [3, 5, 7, 3, 1]..sortByCompare(unreachable, unreachable, 2, 3),
+                [3, 5, 7, 3, 1]);
+          });
+          test('multiple', () {
+            expect(
+                [3, 5, 7, 30, 1]
+                  ..sortByCompare(toString, cmpParseInverse, 1, 4),
+                [3, 30, 7, 5, 1]);
+          });
+        });
+      });
+      group('.shuffleRange', () {
+        test('errors', () {
+          expect(() => [1].shuffleRange(-1, 1), throwsArgumentError);
+          expect(() => [1].shuffleRange(0, 2), throwsArgumentError);
+          expect(() => [1].shuffleRange(1, 0), throwsArgumentError);
+        });
+        test('empty range', () {
+          expect(<int>[]..shuffleRange(0, 0), []);
+          expect([1, 2, 3, 4]..shuffleRange(0, 0), [1, 2, 3, 4]);
+          expect([1, 2, 3, 4]..shuffleRange(4, 4), [1, 2, 3, 4]);
+        });
+        test('singleton range', () {
+          expect([1, 2, 3, 4]..shuffleRange(0, 1), [1, 2, 3, 4]);
+          expect([1, 2, 3, 4]..shuffleRange(3, 4), [1, 2, 3, 4]);
+        });
+        test('multiple', () {
+          var list = [1, 2, 3, 4, 5];
+          do {
+            list.shuffleRange(0, 3);
+            expect(list.getRange(3, 5), [4, 5]);
+            expect(list.getRange(0, 3), unorderedEquals([1, 2, 3]));
+          } while (ListEquality().equals(list.sublist(0, 3), [1, 2, 3]));
+          // Won't terminate if shuffle *never* moves a value.
+        });
+      });
+      group('.reverseRange', () {
+        test('errors', () {
+          expect(() => [1].reverseRange(-1, 1), throwsArgumentError);
+          expect(() => [1].reverseRange(0, 2), throwsArgumentError);
+          expect(() => [1].reverseRange(1, 0), throwsArgumentError);
+        });
+        test('empty range', () {
+          expect(<int>[]..reverseRange(0, 0), []);
+          expect([1, 2, 3, 4]..reverseRange(0, 0), [1, 2, 3, 4]);
+          expect([1, 2, 3, 4]..reverseRange(4, 4), [1, 2, 3, 4]);
+        });
+        test('singleton range', () {
+          expect([1, 2, 3, 4]..reverseRange(0, 1), [1, 2, 3, 4]);
+          expect([1, 2, 3, 4]..reverseRange(3, 4), [1, 2, 3, 4]);
+        });
+        test('multiple', () {
+          var list = [1, 2, 3, 4, 5];
+          list.reverseRange(0, 3);
+          expect(list, [3, 2, 1, 4, 5]);
+          list.reverseRange(3, 5);
+          expect(list, [3, 2, 1, 5, 4]);
+          list.reverseRange(0, 5);
+          expect(list, [4, 5, 1, 2, 3]);
+        });
+      });
+      group('.swap', () {
+        test('errors', () {
+          expect(() => [1].swap(0, 1), throwsArgumentError);
+          expect(() => [1].swap(1, 1), throwsArgumentError);
+          expect(() => [1].swap(1, 0), throwsArgumentError);
+          expect(() => [1].swap(-1, 0), throwsArgumentError);
+        });
+        test('self swap', () {
+          expect([1]..swap(0, 0), [1]);
+          expect([1, 2, 3]..swap(1, 1), [1, 2, 3]);
+        });
+        test('actual swap', () {
+          expect([1, 2, 3]..swap(0, 2), [3, 2, 1]);
+          expect([1, 2, 3]..swap(2, 0), [3, 2, 1]);
+          expect([1, 2, 3]..swap(2, 1), [1, 3, 2]);
+          expect([1, 2, 3]..swap(1, 2), [1, 3, 2]);
+          expect([1, 2, 3]..swap(0, 1), [2, 1, 3]);
+          expect([1, 2, 3]..swap(1, 0), [2, 1, 3]);
+        });
+      });
+      group('.slice', () {
+        test('errors', () {
+          expect(() => [1].slice(-1, 1), throwsArgumentError);
+          expect(() => [1].slice(0, 2), throwsArgumentError);
+          expect(() => [1].slice(1, 0), throwsArgumentError);
+          var l = <int>[1];
+          var slice = l.slice(0, 1);
+          l.removeLast();
+          expect(() => slice.first, throwsConcurrentModificationError);
+        });
+        test('empty', () {
+          expect([].slice(0, 0), isEmpty);
+        });
+        test('modify', () {
+          var list = [1, 2, 3, 4, 5, 6, 7, 8, 9];
+          var slice = list.slice(2, 6);
+          expect(slice, [3, 4, 5, 6]);
+          slice.sort(cmpIntInverse);
+          expect(slice, [6, 5, 4, 3]);
+          expect(list, [1, 2, 6, 5, 4, 3, 7, 8, 9]);
+        });
+      });
+      group('equals', () {
+        test('empty', () {
+          expect(<Object>[].equals(<int>[]), true);
+        });
+        test('non-empty', () {
+          expect([1, 2.5, 'a'].equals([1.0, 2.5, 'a']), true);
+          expect([1, 2.5, 'a'].equals([1.0, 2.5, 'b']), false);
+          expect(
+              [
+                [1]
+              ].equals([
+                [1]
+              ]),
+              false);
+          expect(
+              [
+                [1]
+              ].equals([
+                [1]
+              ], const ListEquality()),
+              true);
+        });
+      });
+      group('.forEachIndexed', () {
+        test('empty', () {
+          [].forEachIndexed(unreachable);
+        });
+        test('single', () {
+          var log = [];
+          ['a'].forEachIndexed((i, s) {
+            log..add(i)..add(s);
+          });
+          expect(log, [0, 'a']);
+        });
+        test('multiple', () {
+          var log = [];
+          ['a', 'b', 'c'].forEachIndexed((i, s) {
+            log..add(i)..add(s);
+          });
+          expect(log, [0, 'a', 1, 'b', 2, 'c']);
+        });
+      });
+      group('.forEachWhile', () {
+        test('empty', () {
+          [].forEachWhile(unreachable);
+        });
+        test('single true', () {
+          var log = [];
+          ['a'].forEachWhile((s) {
+            log.add(s);
+            return true;
+          });
+          expect(log, ['a']);
+        });
+        test('single false', () {
+          var log = [];
+          ['a'].forEachWhile((s) {
+            log.add(s);
+            return false;
+          });
+          expect(log, ['a']);
+        });
+        test('multiple one', () {
+          var log = [];
+          ['a', 'b', 'c'].forEachWhile((s) {
+            log.add(s);
+            return false;
+          });
+          expect(log, ['a']);
+        });
+        test('multiple all', () {
+          var log = [];
+          ['a', 'b', 'c'].forEachWhile((s) {
+            log.add(s);
+            return true;
+          });
+          expect(log, ['a', 'b', 'c']);
+        });
+        test('multiple some', () {
+          var log = [];
+          ['a', 'b', 'c'].forEachWhile((s) {
+            log.add(s);
+            return s != 'b';
+          });
+          expect(log, ['a', 'b']);
+        });
+      });
+      group('.forEachIndexedWhile', () {
+        test('empty', () {
+          [].forEachIndexedWhile(unreachable);
+        });
+        test('single true', () {
+          var log = [];
+          ['a'].forEachIndexedWhile((i, s) {
+            log..add(i)..add(s);
+            return true;
+          });
+          expect(log, [0, 'a']);
+        });
+        test('single false', () {
+          var log = [];
+          ['a'].forEachIndexedWhile((i, s) {
+            log..add(i)..add(s);
+            return false;
+          });
+          expect(log, [0, 'a']);
+        });
+        test('multiple one', () {
+          var log = [];
+          ['a', 'b', 'c'].forEachIndexedWhile((i, s) {
+            log..add(i)..add(s);
+            return false;
+          });
+          expect(log, [0, 'a']);
+        });
+        test('multiple all', () {
+          var log = [];
+          ['a', 'b', 'c'].forEachIndexedWhile((i, s) {
+            log..add(i)..add(s);
+            return true;
+          });
+          expect(log, [0, 'a', 1, 'b', 2, 'c']);
+        });
+        test('multiple some', () {
+          var log = [];
+          ['a', 'b', 'c'].forEachIndexedWhile((i, s) {
+            log..add(i)..add(s);
+            return s != 'b';
+          });
+          expect(log, [0, 'a', 1, 'b']);
+        });
+      });
+      group('.mapIndexed', () {
+        test('empty', () {
+          expect(<String>[].mapIndexed(unreachable), isEmpty);
+        });
+        test('multiple', () {
+          expect(<String>['a', 'b'].mapIndexed((i, s) => [i, s]), [
+            [0, 'a'],
+            [1, 'b']
+          ]);
+        });
+      });
+      group('.whereIndexed', () {
+        test('empty', () {
+          expect(<String>[].whereIndexed(unreachable), isEmpty);
+        });
+        test('none', () {
+          var trace = [];
+          int log(int a, int b) {
+            trace..add(a)..add(b);
+            return b;
+          }
+
+          expect(<int>[1, 3, 5, 7].whereIndexed((i, x) => log(i, x).isEven),
+              isEmpty);
+          expect(trace, [0, 1, 1, 3, 2, 5, 3, 7]);
+        });
+        test('all', () {
+          expect(
+              <int>[1, 3, 5, 7].whereIndexed((i, x) => x.isOdd), [1, 3, 5, 7]);
+        });
+        test('some', () {
+          expect(<int>[1, 3, 5, 7].whereIndexed((i, x) => i.isOdd), [3, 7]);
+        });
+      });
+      group('.whereNotIndexed', () {
+        test('empty', () {
+          expect(<int>[].whereNotIndexed(unreachable), isEmpty);
+        });
+        test('none', () {
+          var trace = [];
+          int log(int a, int b) {
+            trace..add(a)..add(b);
+            return b;
+          }
+
+          expect(<int>[1, 3, 5, 7].whereNotIndexed((i, x) => log(i, x).isOdd),
+              isEmpty);
+          expect(trace, [0, 1, 1, 3, 2, 5, 3, 7]);
+        });
+        test('all', () {
+          expect(<int>[1, 3, 5, 7].whereNotIndexed((i, x) => x.isEven),
+              [1, 3, 5, 7]);
+        });
+        test('some', () {
+          expect(<int>[1, 3, 5, 7].whereNotIndexed((i, x) => i.isOdd), [1, 5]);
+        });
+      });
+      group('.expandIndexed', () {
+        test('empty', () {
+          expect(<int>[].expandIndexed(unreachable), isEmpty);
+        });
+        test('empty result', () {
+          expect(['a', 'b'].expandIndexed((i, v) => []), isEmpty);
+        });
+        test('larger result', () {
+          expect(['a', 'b'].expandIndexed((i, v) => ['$i', v]),
+              ['0', 'a', '1', 'b']);
+        });
+        test('varying result', () {
+          expect(['a', 'b'].expandIndexed((i, v) => i.isOdd ? ['$i', v] : []),
+              ['1', 'b']);
+        });
+      });
+    });
+    group('on comparable', () {
+      group('.binarySearch', () {
+        test('empty', () {
+          expect(<String>[].binarySearch('1', unreachable), -1);
+          expect(<String>[].binarySearch('1'), -1);
+        });
+        test('single', () {
+          expect(['0'].binarySearch('1', cmpString), -1);
+          expect(['1'].binarySearch('1', cmpString), 0);
+          expect(['2'].binarySearch('1', cmpString), -1);
+          expect(
+              ['0'].binarySearch(
+                '1',
+              ),
+              -1);
+          expect(
+              ['1'].binarySearch(
+                '1',
+              ),
+              0);
+          expect(
+              ['2'].binarySearch(
+                '1',
+              ),
+              -1);
+        });
+        test('multiple', () {
+          expect(
+              ['1', '2', '3', '4', '5', '6'].binarySearch('3', cmpString), 2);
+          expect(['1', '2', '3', '4', '5', '6'].binarySearch('3'), 2);
+          expect(
+              ['6', '5', '4', '3', '2', '1'].binarySearch('3', cmpParseInverse),
+              3);
+        });
+      });
+    });
+    group('.lowerBound', () {
+      test('empty', () {
+        expect(<String>[].lowerBound('1', unreachable), 0);
+      });
+      test('single', () {
+        expect(['0'].lowerBound('1', cmpString), 1);
+        expect(['1'].lowerBound('1', cmpString), 0);
+        expect(['2'].lowerBound('1', cmpString), 0);
+        expect(['0'].lowerBound('1'), 1);
+        expect(['1'].lowerBound('1'), 0);
+        expect(['2'].lowerBound('1'), 0);
+      });
+      test('multiple', () {
+        expect(['1', '2', '3', '4', '5', '6'].lowerBound('3', cmpParse), 2);
+        expect(['1', '2', '3', '4', '5', '6'].lowerBound('3'), 2);
+        expect(
+            ['6', '5', '4', '3', '2', '1'].lowerBound('3', cmpParseInverse), 3);
+        expect(['1', '2', '4', '5', '6'].lowerBound('3', cmpParse), 2);
+        expect(['1', '2', '4', '5', '6'].lowerBound('3'), 2);
+        expect(['6', '5', '4', '2', '1'].lowerBound('3', cmpParseInverse), 3);
+      });
+    });
+    group('sortRange', () {
+      test('errors', () {
+        expect(() => [1].sortRange(-1, 1, cmpInt), throwsArgumentError);
+        expect(() => [1].sortRange(0, 2, cmpInt), throwsArgumentError);
+        expect(() => [1].sortRange(1, 0, cmpInt), throwsArgumentError);
+      });
+      test('empty range', () {
+        <int>[].sortRange(0, 0, unreachable);
+        var list = [3, 2, 1];
+        list.sortRange(0, 0, unreachable);
+        list.sortRange(3, 3, unreachable);
+        expect(list, [3, 2, 1]);
+      });
+      test('single', () {
+        [1].sortRange(0, 1, unreachable);
+        var list = [3, 2, 1];
+        list.sortRange(0, 1, unreachable);
+        list.sortRange(1, 2, unreachable);
+        list.sortRange(2, 3, unreachable);
+      });
+      test('multiple', () {
+        var list = [9, 8, 7, 6, 5, 4, 3, 2, 1];
+        list.sortRange(2, 5, cmpInt);
+        expect(list, [9, 8, 5, 6, 7, 4, 3, 2, 1]);
+        list.sortRange(4, 8, cmpInt);
+        expect(list, [9, 8, 5, 6, 2, 3, 4, 7, 1]);
+        list.sortRange(3, 6, cmpIntInverse);
+        expect(list, [9, 8, 5, 6, 3, 2, 4, 7, 1]);
+      });
+    });
+  });
+}
+
+/// Creates a plain iterable not implementing any other class.
+Iterable<T> iterable<T>(Iterable<T> values) sync* {
+  yield* values;
+}
+
+Never unreachable([_, __, ___]) => fail('Unreachable');
+
+String toString(Object? o) => '$o';
+
+/// Compares values equal if they have the same remainder mod [mod].
+int Function(int, int) cmpMod(int mod) => (a, b) => a ~/ mod - b ~/ mod;
+
+/// Compares strings lexically.
+int cmpString(String a, String b) => a.compareTo(b);
+
+/// Compares strings inverse lexically.
+int cmpStringInverse(String a, String b) => b.compareTo(a);
+
+/// Compares strings by length.
+int cmpStringLength(String a, String b) => a.length - b.length;
+
+/// Compares strings by their integer numeral content.
+int cmpParse(String s1, String s2) => cmpInt(int.parse(s1), int.parse(s2));
+
+/// Compares strings inversely by their integer numeral content.
+int cmpParseInverse(String s1, String s2) =>
+    cmpIntInverse(int.parse(s1), int.parse(s2));
+
+/// Compares integers by size.
+int cmpInt(int a, int b) => a - b;
+
+/// Compares integers by inverse size.
+int cmpIntInverse(int a, int b) => b - a;
+
+/// Tests an integer for being even.
+bool isEven(int x) => x.isEven;
+
+/// Tests an integer for being odd.
+bool isOdd(int x) => x.isOdd;