merge null_safety branch into master (#28)

* Migrate package to Null Safety.

- Update CHANGELOG.md.
- Add unsound entry point for test, so it can be run.
- Update SDK dependency to 2.9.0-1
- Updates travis config to run nnbd tests/analysis 
- Adds dependency overrides so tests can be ran in null safety mode
- Updates package version to expected pre-release version
- Updates sdk constraints to restrict to dev sdks with the allow list

Co-authored-by @lrhn.
diff --git a/.travis.yml b/.travis.yml
index 8f0ecf6..afa42c9 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,14 +1,38 @@
 language: dart
 dart:
 - dev
-- stable
-# Only building master means that we don't run two builds for each pull request.
-dart_task:
-- test: --platform vm,chrome
-- dartanalyzer
-- dartfmt
+
+jobs:
+  include:
+    - stage: analyze_and_format
+      name: "Analyze lib/ (no experiment flag)"
+      dart: dev
+      os: linux
+      script: dartanalyzer --fatal-warnings --fatal-infos lib/
+    - stage: analyze_and_format
+      name: "Analyze (with experiment flag)"
+      dart: dev
+      os: linux
+      script: dartanalyzer --enable-experiment=non-nullable --fatal-warnings --fatal-infos .
+    - stage: analyze_and_format
+      name: "Format"
+      dart: dev
+      os: linux
+      script: dartfmt -n --set-exit-if-changed .
+    - stage: test
+      name: "Vm Tests"
+      dart: dev
+      os: linux
+      script: pub run --enable-experiment=non-nullable test -p vm
+    - stage: test
+      name: "Web Tests"
+      dart: dev
+      os: linux
+      script: pub run --enable-experiment=non-nullable test -p chrome
+
 branches:
-  only: [master]
+  only: [master, null_safety]
+
 cache:
   directories:
     - $HOME/.pub-cache
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b8a7dce..458b8c8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
 # Changelog
 
+## 1.1.0-nullsafety
+
+* Make package null safe.
+
 ## 1.0.0
 
 * Core APIs deemed stable; package version set to 1.0.0.
diff --git a/analysis_options.yaml b/analysis_options.yaml
index 3bad72b..d34d1f0 100644
--- a/analysis_options.yaml
+++ b/analysis_options.yaml
@@ -5,3 +5,5 @@
     annotate_overrides: ignore
     prefer_single_quotes: ignore
     use_function_type_syntax_for_parameters: ignore
+  enable-experiment:
+    - non-nullable
diff --git a/lib/src/characters.dart b/lib/src/characters.dart
index 676716b..70b40eb 100644
--- a/lib/src/characters.dart
+++ b/lib/src/characters.dart
@@ -47,14 +47,14 @@
   /// as well as controlling the iteration in more detail.
   CharacterRange get iteratorAtEnd;
 
-  /// Whether [Character] is an element of this sequence of
-  /// characters.
+  /// Whether [other] is an element of this sequence of
+  /// others.
   ///
-  /// Returns false if [Character] is not a string containing
+  /// Returns false if [other] is not a string containing
   /// a single character,
   /// because then it is not a single element of this [Iterable]
   /// of characters.
-  bool contains(Object Character);
+  bool contains(Object? other);
 
   /// Whether this sequence of characters contains [other]
   /// as a subsequence.
@@ -79,13 +79,14 @@
   /// Returns a [CharacterRange] containing the first occurrence of
   /// [characters] in this string.
   /// Returns `null` if there is no such occurrence.
-  CharacterRange /*?*/ findFirst(Characters characters);
+  CharacterRange? findFirst(Characters characters);
 
   /// Finds the last occurrence of [characters].
   ///
   /// Returns a [CharacterRange] containing the last occurrence of
-  /// [characters]. Returns `null` if there is no such occurrence,
-  CharacterRange /*?*/ findLast(Characters characters);
+  /// [characters].
+  /// Returns `null` if there is no such occurrence,
+  CharacterRange? findLast(Characters characters);
 
   /// Eagerly selects a subset of the characters.
   ///
@@ -132,7 +133,7 @@
   /// of characters.
   Characters skipWhile(bool Function(String) test);
 
-  /// Eagerly selects a leading sequnce of characters.
+  /// Eagerly selects a leading sequence of characters.
   ///
   /// Checks each character, from first to last, against [test],
   /// until one is found whwere [test] returns `false`.
@@ -143,7 +144,7 @@
   /// is returned.
   Characters takeWhile(bool Function(String) test);
 
-  /// Eagerly selects a leading sequnce of characters.
+  /// Eagerly selects a leading sequence of characters.
   ///
   /// Checks each character, from last to first, against [test],
   /// until one is found whwere [test] returns `false`.
@@ -679,7 +680,7 @@
   ///
   /// Returns `null` if there are no occurrences of [pattern]
   /// in the current range.
-  CharacterRange /*?*/ replaceAll(Characters pattern, Characters replacement);
+  CharacterRange? replaceAll(Characters pattern, Characters replacement);
 
   /// Splits the current range of characters at each occurrence of [pattern].
   ///
@@ -737,7 +738,7 @@
   ///
   /// Returns `null` if there are no occurrences of [pattern]
   /// in the current range.
-  CharacterRange /*?*/ replaceFirst(Characters pattern, Characters replacement);
+  CharacterRange? replaceFirst(Characters pattern, Characters replacement);
 
   /// Whether the current range starts with [characters].
   ///
diff --git a/lib/src/characters_impl.dart b/lib/src/characters_impl.dart
index 4f474c8..dde34d5 100644
--- a/lib/src/characters_impl.dart
+++ b/lib/src/characters_impl.dart
@@ -69,7 +69,7 @@
 
   @override
   Iterable<T> whereType<T>() {
-    Iterable<Object> self = this;
+    Iterable<Object?> self = this;
     if (self is Iterable<T>) {
       return self.map<T>((x) => x);
     }
@@ -83,7 +83,7 @@
   }
 
   @override
-  String lastWhere(bool test(String element), {String orElse()}) {
+  String lastWhere(bool test(String element), {String orElse()?}) {
     int cursor = string.length;
     var brk = BackBreaks(string, cursor, 0, stateEoTNoBreak);
     int next = 0;
@@ -114,7 +114,7 @@
   }
 
   @override
-  bool contains(Object other) {
+  bool contains(Object? other) {
     if (other is String) {
       if (other.isEmpty) return false;
       int next = Breaks(other, 0, other.length, stateSoTNoBreak).nextBreak();
@@ -375,14 +375,14 @@
   String toString() => string;
 
   @override
-  CharacterRange findFirst(Characters characters) {
+  CharacterRange? findFirst(Characters characters) {
     var range = _rangeAll;
     if (range.collapseToFirst(characters)) return range;
     return null;
   }
 
   @override
-  CharacterRange findLast(Characters characters) {
+  CharacterRange? findLast(Characters characters) {
     var range = _rangeAll;
     if (range.collapseToLast(characters)) return range;
     return null;
@@ -408,7 +408,7 @@
 
   /// The [current] value is created lazily and cached to avoid repeated
   /// or unnecessary string allocation.
-  String _currentCache;
+  String? _currentCache;
 
   StringCharacterRange(String string) : this._(string, 0, 0);
   StringCharacterRange._(this._string, this._start, this._end);
@@ -778,8 +778,7 @@
   }
 
   @override
-  CharacterRange /*?*/ replaceFirst(
-      Characters pattern, Characters replacement) {
+  CharacterRange? replaceFirst(Characters pattern, Characters replacement) {
     String patternString = pattern.string;
     String replacementString = replacement.string;
     String replaced;
@@ -799,7 +798,7 @@
   }
 
   @override
-  CharacterRange /*?*/ replaceAll(Characters pattern, Characters replacement) {
+  CharacterRange? replaceAll(Characters pattern, Characters replacement) {
     var patternString = pattern.string;
     var replacementString = replacement.string;
     if (patternString.isEmpty) {
@@ -811,7 +810,7 @@
     if (_start == _end) return null;
     int start = 0;
     int cursor = _start;
-    StringBuffer buffer;
+    StringBuffer? buffer;
     while ((cursor = _indexOf(_string, patternString, cursor, _end)) >= 0) {
       (buffer ??= StringBuffer())
         ..write(_string.substring(start, cursor))
diff --git a/pubspec.yaml b/pubspec.yaml
index 8863486..c0b1dc4 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,10 +1,96 @@
 name: characters
-version: 1.0.0
+version: 1.1.0-nullsafety
 description: String replacement with operations that are Unicode/grapheme cluster aware.
 homepage: https://www.github.com/dart-lang/characters
 
 environment:
-  sdk: ">=2.6.0 <3.0.0"
+  # This must remain a tight constraint (only allow dev versions) until nnbd is
+  # stable.
+  sdk: '>=2.9.0-18.0 <2.9.0'
+
 dev_dependencies:
   test: "^1.6.0"
   pedantic: ^1.9.0
+
+dependency_overrides:
+  async:
+    git:
+      url: git://github.com/dart-lang/async.git
+      ref: null_safety
+  boolean_selector:
+    git:
+      url: git://github.com/dart-lang/boolean_selector.git
+      ref: null_safety
+  charcode:
+    git:
+      url: git://github.com/dart-lang/charcode.git
+      ref: null_safety
+  collection:
+    git: git://github.com/dart-lang/collection.git
+  js:
+    git:
+      url: git://github.com/dart-lang/sdk.git
+      path: pkg/js
+  matcher:
+    git:
+      url: git://github.com/dart-lang/matcher.git
+      ref: null_safety
+  meta:
+    git:
+      url: git://github.com/dart-lang/sdk.git
+      path: pkg/meta
+  path:
+    git:
+      url: git://github.com/dart-lang/path.git
+      ref: null_safety
+  pedantic:
+    git:
+      url: git://github.com/dart-lang/pedantic.git
+      ref: null_safety
+  pool:
+    git:
+      url: git://github.com/dart-lang/pool.git
+      ref: null_safety
+  source_maps:
+    git:
+      url: git://github.com/dart-lang/source_maps.git
+      ref: null_safety
+  source_map_stack_trace:
+    git:
+      url: git://github.com/dart-lang/source_map_stack_trace.git
+      ref: null_safety
+  source_span:
+    git:
+      url: git://github.com/dart-lang/source_span.git
+      ref: null_safety
+  stack_trace:
+    git:
+      url: git://github.com/dart-lang/stack_trace.git
+      ref: null_safety
+  stream_channel:
+    git:
+      url: git://github.com/dart-lang/stream_channel.git
+      ref: null_safety
+  string_scanner:
+    git:
+      url: git://github.com/dart-lang/string_scanner.git
+      ref: null_safety
+  term_glyph:
+    git:
+      url: git://github.com/dart-lang/term_glyph.git
+      ref: null_safety
+  test_api:
+    git:
+      url: git://github.com/dart-lang/test.git
+      ref: null_safety
+      path: pkgs/test_api
+  test_core:
+    git:
+      url: git://github.com/dart-lang/test.git
+      ref: null_safety
+      path: pkgs/test_core
+  test:
+    git:
+      url: git://github.com/dart-lang/test.git
+      ref: null_safety
+      path: pkgs/test
diff --git a/test/characters_test.dart b/test/characters_test.dart
index f34e6ec..d7f057b 100644
--- a/test/characters_test.dart
+++ b/test/characters_test.dart
@@ -1,668 +1,13 @@
-// Copyright (c) 2019, 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";
-
-import "package:test/test.dart";
-
-import "package:characters/characters.dart";
-
-import "src/unicode_tests.dart";
-import "src/unicode_grapheme_tests.dart";
-import "src/various_tests.dart";
-
-Random random;
-
-void main([List<String> args]) {
-  // Ensure random seed is part of every test failure message,
-  // and that it can be reapplied for testing.
-  var seed = (args != null && args.isNotEmpty)
-      ? int.parse(args[0])
-      : Random().nextInt(0x3FFFFFFF);
-  random = Random(seed);
-  group("[Random Seed: $seed]", tests);
-  group("characters", () {
-    test("operations", () {
-      var flag = "\u{1F1E9}\u{1F1F0}"; // Regional Indicators "DK".
-      var string = "Hi $flag!";
-      expect(string.length, 8);
-      var cs = gc(string);
-      expect(cs.length, 5);
-      expect(cs.toList(), ["H", "i", " ", flag, "!"]);
-      expect(cs.skip(2).toString(), " $flag!");
-      expect(cs.skipLast(2).toString(), "Hi ");
-      expect(cs.take(2).toString(), "Hi");
-      expect(cs.takeLast(2).toString(), "$flag!");
-
-      expect(cs.contains("\u{1F1E9}"), false);
-      expect(cs.contains(flag), true);
-      expect(cs.contains("$flag!"), false);
-      expect(cs.containsAll(gc("$flag!")), true);
-
-      expect(cs.takeWhile((x) => x != " ").toString(), "Hi");
-      expect(cs.takeLastWhile((x) => x != " ").toString(), "$flag!");
-      expect(cs.skipWhile((x) => x != " ").toString(), " $flag!");
-      expect(cs.skipLastWhile((x) => x != " ").toString(), "Hi ");
-
-      expect(cs.findFirst(gc("")).moveBack(), false);
-      expect(cs.findFirst(gc(flag)).current, flag);
-      expect(cs.findLast(gc(flag)).current, flag);
-      expect(cs.iterator.moveNext(), true);
-      expect(cs.iterator.moveBack(), false);
-      expect((cs.iterator..moveNext()).current, "H");
-      expect(cs.iteratorAtEnd.moveNext(), false);
-      expect(cs.iteratorAtEnd.moveBack(), true);
-      expect((cs.iteratorAtEnd..moveBack()).current, "!");
-    });
-
-    testParts(gc("a"), gc("b"), gc("c"), gc("d"), gc("e"));
-
-    // Composite pictogram example, from https://en.wikipedia.org/wiki/Zero-width_joiner.
-    var flag = "\u{1f3f3}"; // U+1F3F3, Flag, waving. Category Pictogram.
-    var white = "\ufe0f"; // U+FE0F, Variant selector 16. Category Extend.
-    var zwj = "\u200d"; // U+200D, ZWJ
-    var rainbow = "\u{1f308}"; // U+1F308, Rainbow. Category Pictogram
-
-    testParts(gc("$flag$white$zwj$rainbow"), gc("$flag$white"), gc("$rainbow"),
-        gc("$flag$zwj$rainbow"), gc("!"));
-  });
-}
-
-void tests() {
-  test("empty", () {
-    expectGC(gc(""), []);
-  });
-  group("gc-ASCII", () {
-    for (var text in [
-      "",
-      "A",
-      "123456abcdefab",
-    ]) {
-      test('"$text"', () {
-        expectGC(gc(text), charsOf(text));
-      });
-    }
-    test("CR+NL", () {
-      expectGC(gc("a\r\nb"), ["a", "\r\n", "b"]);
-      expectGC(gc("a\n\rb"), ["a", "\n", "\r", "b"]);
-    });
-  });
-  group("Non-ASCII single-code point", () {
-    for (var text in [
-      "à la mode",
-      "rødgrød-æble-ål",
-    ]) {
-      test('"$text"', () {
-        expectGC(gc(text), charsOf(text));
-      });
-    }
-  });
-  group("Combining marks", () {
-    var text = "a\u0300 la mode";
-    test('"$text"', () {
-      expectGC(gc(text), ["a\u0300", " ", "l", "a", " ", "m", "o", "d", "e"]);
-    });
-    var text2 = "æble-a\u030Al";
-    test('"$text2"', () {
-      expectGC(gc(text2), ["æ", "b", "l", "e", "-", "a\u030A", "l"]);
-    });
-  });
-
-  group("Regional Indicators", () {
-    test('"πŸ‡¦πŸ‡©πŸ‡°πŸ‡ΎπŸ‡ͺπŸ‡Έ"', () {
-      // Andorra, Cayman Islands, Spain.
-      expectGC(gc("πŸ‡¦πŸ‡©πŸ‡°πŸ‡ΎπŸ‡ͺπŸ‡Έ"), ["πŸ‡¦πŸ‡©", "πŸ‡°πŸ‡Ύ", "πŸ‡ͺπŸ‡Έ"]);
-    });
-    test('"XπŸ‡¦πŸ‡©πŸ‡°πŸ‡ΎπŸ‡ͺπŸ‡Έ"', () {
-      // Other, Andorra, Cayman Islands, Spain.
-      expectGC(gc("XπŸ‡¦πŸ‡©πŸ‡°πŸ‡ΎπŸ‡ͺπŸ‡Έ"), ["X", "πŸ‡¦πŸ‡©", "πŸ‡°πŸ‡Ύ", "πŸ‡ͺπŸ‡Έ"]);
-    });
-    test('"πŸ‡©πŸ‡°πŸ‡ΎπŸ‡ͺπŸ‡Έ"', () {
-      // Denmark, Yemen, unmatched S.
-      expectGC(gc("πŸ‡©πŸ‡°πŸ‡ΎπŸ‡ͺπŸ‡Έ"), ["πŸ‡©πŸ‡°", "πŸ‡ΎπŸ‡ͺ", "πŸ‡Έ"]);
-    });
-    test('"XπŸ‡©πŸ‡°πŸ‡ΎπŸ‡ͺπŸ‡Έ"', () {
-      // Other, Denmark, Yemen, unmatched S.
-      expectGC(gc("XπŸ‡©πŸ‡°πŸ‡ΎπŸ‡ͺπŸ‡Έ"), ["X", "πŸ‡©πŸ‡°", "πŸ‡ΎπŸ‡ͺ", "πŸ‡Έ"]);
-    });
-  });
-
-  group("Hangul", () {
-    // Individual characters found on Wikipedia. Not expected to make sense.
-    test('"읍쌍된밟"', () {
-      expectGC(gc("읍쌍된밟"), ["읍", "쌍", "된", "밟"]);
-    });
-  });
-
-  group("Unicode test", () {
-    for (var gcs in splitTests) {
-      test("[${testDescription(gcs)}]", () {
-        expectGC(gc(gcs.join()), gcs);
-      });
-    }
-  });
-
-  group("Emoji test", () {
-    for (var gcs in emojis) {
-      test("[${testDescription(gcs)}]", () {
-        expectGC(gc(gcs.join()), gcs);
-      });
-    }
-  });
-
-  group("Zalgo test", () {
-    for (var gcs in zalgo) {
-      test("[${testDescription(gcs)}]", () {
-        expectGC(gc(gcs.join()), gcs);
-      });
-    }
-  });
-}
-
-// Converts text with no multi-code-point grapheme clusters into
-// list of grapheme clusters.
-List<String> charsOf(String text) =>
-    text.runes.map((r) => String.fromCharCode(r)).toList();
-
-void expectGC(Characters actual, List<String> expected) {
-  var text = expected.join();
-
-  // Iterable operations.
-  expect(actual.string, text);
-  expect(actual.toString(), text);
-  expect(actual.toList(), expected);
-  expect(actual.length, expected.length);
-  if (expected.isNotEmpty) {
-    expect(actual.first, expected.first);
-    expect(actual.last, expected.last);
-  } else {
-    expect(() => actual.first, throwsStateError);
-    expect(() => actual.last, throwsStateError);
-  }
-  if (expected.length == 1) {
-    expect(actual.single, expected.single);
-  } else {
-    expect(() => actual.single, throwsStateError);
-  }
-  expect(actual.isEmpty, expected.isEmpty);
-  expect(actual.isNotEmpty, expected.isNotEmpty);
-  expect(actual.contains(""), false);
-  for (var char in expected) {
-    expect(actual.contains(char), true);
-  }
-  for (int i = 1; i < expected.length; i++) {
-    expect(actual.contains(expected[i - 1] + expected[i]), false);
-  }
-  expect(actual.skip(1).toList(), expected.skip(1).toList());
-  expect(actual.take(1).toList(), expected.take(1).toList());
-  expect(actual.skip(1).toString(), expected.skip(1).join());
-  expect(actual.take(1).toString(), expected.take(1).join());
-
-  if (expected.isNotEmpty) {
-    expect(actual.skipLast(1).toList(),
-        expected.take(expected.length - 1).toList());
-    expect(actual.takeLast(1).toList(),
-        expected.skip(expected.length - 1).toList());
-    expect(actual.skipLast(1).toString(),
-        expected.take(expected.length - 1).join());
-    expect(actual.takeLast(1).toString(),
-        expected.skip(expected.length - 1).join());
-  }
-  bool isEven(String s) => s.length.isEven;
-
-  expect(
-      actual.skipWhile(isEven).toList(), expected.skipWhile(isEven).toList());
-  expect(
-      actual.takeWhile(isEven).toList(), expected.takeWhile(isEven).toList());
-  expect(
-      actual.skipWhile(isEven).toString(), expected.skipWhile(isEven).join());
-  expect(
-      actual.takeWhile(isEven).toString(), expected.takeWhile(isEven).join());
-
-  expect(actual.skipLastWhile(isEven).toString(),
-      expected.toList().reversed.skipWhile(isEven).toList().reversed.join());
-  expect(actual.takeLastWhile(isEven).toString(),
-      expected.toList().reversed.takeWhile(isEven).toList().reversed.join());
-
-  expect(actual.where(isEven).toString(), expected.where(isEven).join());
-
-  expect((actual + actual).toString(), actual.string + actual.string);
-
-  // Iteration.
-  var it = actual.iterator;
-  expect(it.isEmpty, true);
-  for (var i = 0; i < expected.length; i++) {
-    expect(it.moveNext(), true);
-    expect(it.current, expected[i]);
-
-    expect(actual.elementAt(i), expected[i]);
-    expect(actual.skip(i).first, expected[i]);
-  }
-  expect(it.moveNext(), false);
-  for (var i = expected.length - 1; i >= 0; i--) {
-    expect(it.moveBack(), true);
-    expect(it.current, expected[i]);
-  }
-  expect(it.moveBack(), false);
-  expect(it.isEmpty, true);
-
-  // GraphemeClusters operations.
-  expect(actual.toUpperCase().string, text.toUpperCase());
-  expect(actual.toLowerCase().string, text.toLowerCase());
-
-  expect(actual.string, text);
-
-  expect(actual.containsAll(gc("")), true);
-  expect(actual.containsAll(actual), true);
-  if (expected.isNotEmpty) {
-    int steps = min(5, expected.length);
-    for (int s = 0; s <= steps; s++) {
-      int i = expected.length * s ~/ steps;
-      expect(actual.startsWith(gc(expected.sublist(0, i).join())), true);
-      expect(actual.endsWith(gc(expected.sublist(i).join())), true);
-      for (int t = s + 1; t <= steps; t++) {
-        int j = expected.length * t ~/ steps;
-        var slice = expected.sublist(i, j).join();
-        var gcs = gc(slice);
-        expect(actual.containsAll(gcs), true);
-      }
-    }
-  }
-
-  {
-    // Random walk back and forth.
-    var it = actual.iterator;
-    int pos = -1;
-    if (random.nextBool()) {
-      pos = expected.length;
-      it = actual.iteratorAtEnd;
-    }
-    int steps = 5 + random.nextInt(expected.length * 2 + 1);
-    bool lastMove = false;
-    while (true) {
-      bool back = false;
-      if (pos < 0) {
-        expect(lastMove, false);
-        expect(it.isEmpty, true);
-      } else if (pos >= expected.length) {
-        expect(lastMove, false);
-        expect(it.isEmpty, true);
-        back = true;
-      } else {
-        expect(lastMove, true);
-        expect(it.current, expected[pos]);
-        back = random.nextBool();
-      }
-      if (--steps < 0) break;
-      if (back) {
-        lastMove = it.moveBack();
-        pos -= 1;
-      } else {
-        lastMove = it.moveNext();
-        pos += 1;
-      }
-    }
-  }
-}
-
-Characters gc(String string) => Characters(string);
-
-void testParts(
-    Characters a, Characters b, Characters c, Characters d, Characters e) {
-  var cs = gc("$a$b$c$d$e");
-  test("$cs", () {
-    var it = cs.iterator;
-    expect(it.isEmpty, true);
-    expect(it.isNotEmpty, false);
-    expect(it.current, "");
-
-    // moveNext().
-    expect(it.moveNext(), true);
-    expect(it.isEmpty, false);
-    expect(it.current, "$a");
-    expect(it.moveNext(), true);
-    expect(it.isEmpty, false);
-    expect(it.current, "$b");
-    expect(it.moveNext(), true);
-    expect(it.isEmpty, false);
-    expect(it.current, "$c");
-    expect(it.moveNext(), true);
-    expect(it.isEmpty, false);
-    expect(it.current, "$d");
-    expect(it.moveNext(), true);
-    expect(it.isEmpty, false);
-    expect(it.current, "$e");
-    expect(it.moveNext(), false);
-    expect(it.isEmpty, true);
-    expect(it.current, "");
-
-    // moveBack().
-    expect(it.moveBack(), true);
-    expect(it.isEmpty, false);
-    expect(it.current, "$e");
-    expect(it.moveBack(), true);
-    expect(it.isEmpty, false);
-    expect(it.current, "$d");
-    expect(it.moveBack(), true);
-    expect(it.isEmpty, false);
-    expect(it.current, "$c");
-    expect(it.moveBack(), true);
-    expect(it.isEmpty, false);
-    expect(it.current, "$b");
-    expect(it.moveBack(), true);
-    expect(it.isEmpty, false);
-    expect(it.current, "$a");
-    expect(it.moveBack(), false);
-    expect(it.isEmpty, true);
-    expect(it.current, "");
-
-    // moveNext(int).
-    expect(it.moveTo(c), true);
-    expect(it.current, "$c");
-    expect(it.moveTo(b), false);
-    expect(it.moveTo(c), false);
-    expect(it.current, "$c");
-    expect(it.moveTo(d), true);
-    expect(it.current, "$d");
-
-    // moveBack(c).
-    expect(it.moveBackTo(c), true);
-    expect(it.current, "$c");
-    expect(it.moveBackTo(d), false);
-    expect(it.moveBackTo(c), false);
-    expect(it.moveBackTo(a), true);
-    expect(it.current, "$a");
-
-    // moveNext(n)
-    expect(it.moveBack(), false);
-
-    expect(it.moveNext(2), true);
-    expect(it.current, "$a$b");
-    expect(it.moveNext(4), false);
-    expect(it.current, "$c$d$e");
-    expect(it.moveNext(0), true);
-    expect(it.current, "");
-    expect(it.moveNext(1), false);
-    expect(it.current, "");
-
-    // moveBack(n).
-    expect(it.moveBack(2), true);
-    expect(it.current, "$d$e");
-    expect(it.moveBack(1), true);
-    expect(it.current, "$c");
-    expect(it.moveBack(3), false);
-    expect(it.current, "$a$b");
-    expect(it.moveBack(), false);
-
-    // moveFirst.
-    it.expandAll();
-    expect(it.current, "$a$b$c$d$e");
-    expect(it.collapseToFirst(b), true);
-    expect(it.current, "$b");
-    it.expandAll();
-    expect(it.current, "$b$c$d$e");
-    expect(it.collapseToFirst(a), false);
-    expect(it.current, "$b$c$d$e");
-
-    // moveBackTo
-    it.expandBackAll();
-    expect(it.current, "$a$b$c$d$e");
-    expect(it.collapseToLast(c), true);
-    expect(it.current, "$c");
-
-    // includeNext/includePrevious
-    expect(it.expandTo(e), true);
-    expect(it.current, "$c$d$e");
-    expect(it.expandTo(e), false);
-    expect(it.expandBackTo(b), true);
-    expect(it.current, "$b$c$d$e");
-    expect(it.expandBackTo(b), false);
-    expect(it.current, "$b$c$d$e");
-    expect(it.collapseToFirst(c), true);
-    expect(it.current, "$c");
-
-    // includeUntilNext/expandBackUntil
-    expect(it.expandBackUntil(a), true);
-    expect(it.current, "$b$c");
-    expect(it.expandBackUntil(a), true);
-    expect(it.current, "$b$c");
-    expect(it.expandUntil(e), true);
-    expect(it.current, "$b$c$d");
-    expect(it.expandUntil(e), true);
-    expect(it.current, "$b$c$d");
-
-    // dropFirst/dropLast
-    expect(it.dropFirst(), true);
-    expect(it.current, "$c$d");
-    expect(it.dropLast(), true);
-    expect(it.current, "$c");
-    it.expandBackAll();
-    it.expandAll();
-    expect(it.current, "$a$b$c$d$e");
-    expect(it.dropTo(b), true);
-    expect(it.current, "$c$d$e");
-    expect(it.dropBackTo(d), true);
-    expect(it.current, "$c");
-
-    it.expandBackAll();
-    it.expandAll();
-    expect(it.current, "$a$b$c$d$e");
-
-    expect(it.dropUntil(b), true);
-    expect(it.current, "$b$c$d$e");
-    expect(it.dropBackUntil(d), true);
-    expect(it.current, "$b$c$d");
-
-    it.dropWhile((x) => x == b.string);
-    expect(it.current, "$c$d");
-    it.expandBackAll();
-    expect(it.current, "$a$b$c$d");
-    it.dropBackWhile((x) => x != b.string);
-    expect(it.current, "$a$b");
-    it.dropBackWhile((x) => false);
-    expect(it.current, "$a$b");
-
-    // include..While
-    it.expandWhile((x) => false);
-    expect(it.current, "$a$b");
-    it.expandWhile((x) => x != e.string);
-    expect(it.current, "$a$b$c$d");
-    expect(it.collapseToFirst(c), true);
-    expect(it.current, "$c");
-    it.expandBackWhile((x) => false);
-    expect(it.current, "$c");
-    it.expandBackWhile((x) => x != a.string);
-    expect(it.current, "$b$c");
-
-    var cs2 = cs.replaceAll(c, gc(""));
-    var cs3 = cs.replaceFirst(c, gc(""));
-    var cs4 = cs.findFirst(c).replaceRange(gc("")).source;
-    var cse = gc("$a$b$d$e");
-    expect(cs2, cse);
-    expect(cs3, cse);
-    expect(cs4, cse);
-    var cs5 = cs4.replaceAll(a, c);
-    expect(cs5, gc("$c$b$d$e"));
-    var cs6 = cs5.replaceAll(gc(""), a);
-    expect(cs6, gc("$a$c$a$b$a$d$a$e$a"));
-    var cs7 = cs6.replaceFirst(b, a);
-    expect(cs7, gc("$a$c$a$a$a$d$a$e$a"));
-    var cs8 = cs7.replaceFirst(e, a);
-    expect(cs8, gc("$a$c$a$a$a$d$a$a$a"));
-    var cs9 = cs8.replaceAll(a + a, b);
-    expect(cs9, gc("$a$c$b$a$d$b$a"));
-    it = cs9.iterator;
-    it.moveTo(b + a);
-    expect("$b$a", it.current);
-    it.expandTo(b + a);
-    expect("$b$a$d$b$a", it.current);
-    var cs10 = it.replaceAll(b + a, e + e);
-    expect(cs10.currentCharacters, e + e + d + e + e);
-    expect(cs10.source, gc("$a$c$e$e$d$e$e"));
-    var cs11 = it.replaceRange(e);
-    expect(cs11.currentCharacters, e);
-    expect(cs11.source, gc("$a$c$e"));
-
-    var cs12 = gc("$a$b$a");
-    expect(cs12.split(b), [a, a]);
-    expect(cs12.split(a), [gc(""), b, gc("")]);
-    expect(cs12.split(a, 2), [gc(""), gc("$b$a")]);
-
-    expect(cs12.split(gc("")), [a, b, a]);
-    expect(cs12.split(gc(""), 2), [a, gc("$b$a")]);
-
-    expect(gc("").split(gc("")), [gc("")]);
-
-    var cs13 = gc("$b$a$b$a$b$a");
-    expect(cs13.split(b), [gc(""), a, a, a]);
-    expect(cs13.split(b, 1), [cs13]);
-    expect(cs13.split(b, 2), [gc(""), gc("$a$b$a$b$a")]);
-    expect(cs13.split(b, 3), [gc(""), a, gc("$a$b$a")]);
-    expect(cs13.split(b, 4), [gc(""), a, a, a]);
-    expect(cs13.split(b, 5), [gc(""), a, a, a]);
-    expect(cs13.split(b, 9999), [gc(""), a, a, a]);
-    expect(cs13.split(b, 0), [gc(""), a, a, a]);
-    expect(cs13.split(b, -1), [gc(""), a, a, a]);
-    expect(cs13.split(b, -9999), [gc(""), a, a, a]);
-
-    it = cs13.iterator..expandAll();
-    expect(it.current, "$b$a$b$a$b$a");
-    it.dropFirst();
-    it.dropLast();
-    expect(it.current, "$a$b$a$b");
-    expect(it.split(a).map((range) => range.current), ["", "$b", "$b"]);
-    expect(it.split(a, 2).map((range) => range.current), ["", "$b$a$b"]);
-    // Each split is after an *a*.
-    bool first = true;
-    for (var range in it.split(a)) {
-      if (range.isEmpty) {
-        // First range is empty.
-        expect(first, true);
-        first = false;
-        continue;
-      }
-      // Later ranges are "b" that come after "a".
-      expect(range.current, "$b");
-      range.moveBack();
-      expect(range.current, "$a");
-    }
-
-    expect(it.split(gc("")).map((range) => range.current),
-        ["$a", "$b", "$a", "$b"]);
-
-    expect(gc("").iterator.split(gc("")).map((range) => range.current), [""]);
-
-    expect(cs.startsWith(gc("")), true);
-    expect(cs.startsWith(a), true);
-    expect(cs.startsWith(a + b), true);
-    expect(cs.startsWith(gc("$a$b$c")), true);
-    expect(cs.startsWith(gc("$a$b$c$d")), true);
-    expect(cs.startsWith(gc("$a$b$c$d$e")), true);
-    expect(cs.startsWith(b), false);
-    expect(cs.startsWith(c), false);
-    expect(cs.startsWith(d), false);
-    expect(cs.startsWith(e), false);
-
-    expect(cs.endsWith(gc("")), true);
-    expect(cs.endsWith(e), true);
-    expect(cs.endsWith(d + e), true);
-    expect(cs.endsWith(gc("$c$d$e")), true);
-    expect(cs.endsWith(gc("$b$c$d$e")), true);
-    expect(cs.endsWith(gc("$a$b$c$d$e")), true);
-    expect(cs.endsWith(d), false);
-    expect(cs.endsWith(c), false);
-    expect(cs.endsWith(b), false);
-    expect(cs.endsWith(a), false);
-
-    it = cs.findFirst(b + c);
-    expect(it.startsWith(gc("")), true);
-    expect(it.startsWith(b), true);
-    expect(it.startsWith(b + c), true);
-    expect(it.startsWith(a + b + c), false);
-    expect(it.startsWith(b + c + d), false);
-    expect(it.startsWith(a), false);
-
-    expect(it.endsWith(gc("")), true);
-    expect(it.endsWith(c), true);
-    expect(it.endsWith(b + c), true);
-    expect(it.endsWith(a + b + c), false);
-    expect(it.endsWith(b + c + d), false);
-    expect(it.endsWith(d), false);
-
-    it.collapseToFirst(c);
-    expect(it.isPrecededBy(gc("")), true);
-    expect(it.isPrecededBy(b), true);
-    expect(it.isPrecededBy(a + b), true);
-    expect(it.isPrecededBy(a + b + c), false);
-    expect(it.isPrecededBy(a), false);
-
-    expect(it.isFollowedBy(gc("")), true);
-    expect(it.isFollowedBy(d), true);
-    expect(it.isFollowedBy(d + e), true);
-    expect(it.isFollowedBy(c + d + e), false);
-    expect(it.isFollowedBy(e), false);
-  });
-  test("replace methods", () {
-    // Unicode grapheme breaking character classes,
-    // represented by their first value.
-
-    var pattern = gc("\t"); // A non-combining entry to be replaced.
-    var non = gc("");
-
-    var c = otr + cr + pattern + lf + pic + pattern + zwj + pic + otr;
-    var r = c.replaceAll(pattern, non);
-    expect(r, otr + cr + lf + pic + zwj + pic + otr);
-    var ci = c.iterator..moveNextAll();
-    var ri = ci.replaceAll(pattern, non);
-    expect(ri.currentCharacters, otr + cr + lf + pic + zwj + pic + otr);
-    ci.dropFirst();
-    ci.dropLast();
-    expect(ci.currentCharacters, cr + pattern + lf + pic + pattern + zwj + pic);
-    expect(ci.currentCharacters.length, 7);
-    ri = ci.replaceAll(pattern, non);
-    expect(ri.currentCharacters, cr + lf + pic + zwj + pic);
-    expect(ri.currentCharacters.length, 2);
-    ci.dropFirst();
-    ci.dropLast(5);
-    expect(ci.currentCharacters, pattern);
-    ri = ci.replaceAll(pattern, non);
-    expect(ri.currentCharacters, cr + lf);
-    ci.moveNext(2);
-    ci.moveNext(1);
-    expect(ci.currentCharacters, pattern);
-    ri = ci.replaceAll(pattern, non);
-    expect(ri.currentCharacters, pic + zwj + pic);
-
-    c = otr + pic + ext + pattern + pic + ext + otr;
-    expect(c.length, 5);
-    ci = c.iterator..moveTo(pattern);
-    expect(ci.currentCharacters, pattern);
-    ri = ci.replaceAll(pattern, zwj);
-    expect(ri.currentCharacters, pic + ext + zwj + pic + ext);
-
-    c = reg + pattern + reg + reg;
-    ci = c.iterator..moveTo(pattern);
-    ri = ci.replaceRange(non);
-    expect(ri.currentCharacters, reg + reg);
-    expect(ri.moveNext(), true);
-    expect(ri.currentCharacters, reg);
-  });
-}
-
-/// Sample characters from each breaking algorithm category.
-final Characters ctl = gc("\x00"); // Control, NUL.
-final Characters cr = gc("\r"); // Carriage Return, CR.
-final Characters lf = gc("\n"); // Newline, NL.
-final Characters otr = gc(" "); // Other, Space.
-final Characters ext = gc("\u0300"); // Extend, Combining Grave Accent.
-final Characters spc = gc("\u0903"); // Spacing Mark, Devanagari Sign Visarga.
-final Characters pre = gc("\u0600"); // Prepend, Arabic Number Sign.
-final Characters zwj = gc("\u200d"); // Zero-Width Joiner.
-final Characters pic = gc("\u00a9"); // Extended Pictographic, Copyright.
-final Characters reg = gc("\u{1f1e6}"); // Regional Identifier "a".
-final Characters hanl = gc("\u1100"); // Hangul L, Choseong Kiyeok.
-final Characters hanv = gc("\u1160"); // Hangul V, Jungseong Filler.
-final Characters hant = gc("\u11a8"); // Hangul T, Jongseong Kiyeok.
-final Characters hanlv = gc("\uac00"); // Hangul LV, Syllable Ga.
-final Characters hanlvt = gc("\uac01"); // Hangul LVT, Syllable Gag.
+// 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.

+

+// Unsound entry point. Use until all dependencies are sound.

+// Then move `characters_test.dart` back out from `sound_tests/`.

+

+// @dart=2.8

+import 'sound_tests/characters_test.dart' as sound;

+

+void main() {

+  sound.main();

+}

diff --git a/test/sound_tests/characters_test.dart b/test/sound_tests/characters_test.dart
new file mode 100644
index 0000000..5fe41fe
--- /dev/null
+++ b/test/sound_tests/characters_test.dart
@@ -0,0 +1,668 @@
+// Copyright (c) 2019, 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";
+
+import "package:test/test.dart";
+
+import "package:characters/characters.dart";
+
+import "../src/unicode_tests.dart";
+import "../src/unicode_grapheme_tests.dart";
+import "../src/various_tests.dart";
+
+late Random random;
+
+void main([List<String>? args]) {
+  // Ensure random seed is part of every test failure message,
+  // and that it can be reapplied for testing.
+  var seed = (args != null && args.isNotEmpty)
+      ? int.parse(args[0])
+      : Random().nextInt(0x3FFFFFFF);
+  random = Random(seed);
+  group("[Random Seed: $seed]", tests);
+  group("characters", () {
+    test("operations", () {
+      var flag = "\u{1F1E9}\u{1F1F0}"; // Regional Indicators "DK".
+      var string = "Hi $flag!";
+      expect(string.length, 8);
+      var cs = gc(string);
+      expect(cs.length, 5);
+      expect(cs.toList(), ["H", "i", " ", flag, "!"]);
+      expect(cs.skip(2).toString(), " $flag!");
+      expect(cs.skipLast(2).toString(), "Hi ");
+      expect(cs.take(2).toString(), "Hi");
+      expect(cs.takeLast(2).toString(), "$flag!");
+
+      expect(cs.contains("\u{1F1E9}"), false);
+      expect(cs.contains(flag), true);
+      expect(cs.contains("$flag!"), false);
+      expect(cs.containsAll(gc("$flag!")), true);
+
+      expect(cs.takeWhile((x) => x != " ").toString(), "Hi");
+      expect(cs.takeLastWhile((x) => x != " ").toString(), "$flag!");
+      expect(cs.skipWhile((x) => x != " ").toString(), " $flag!");
+      expect(cs.skipLastWhile((x) => x != " ").toString(), "Hi ");
+
+      expect(cs.findFirst(gc(""))!.moveBack(), false);
+      expect(cs.findFirst(gc(flag))!.current, flag);
+      expect(cs.findLast(gc(flag))!.current, flag);
+      expect(cs.iterator.moveNext(), true);
+      expect(cs.iterator.moveBack(), false);
+      expect((cs.iterator..moveNext()).current, "H");
+      expect(cs.iteratorAtEnd.moveNext(), false);
+      expect(cs.iteratorAtEnd.moveBack(), true);
+      expect((cs.iteratorAtEnd..moveBack()).current, "!");
+    });
+
+    testParts(gc("a"), gc("b"), gc("c"), gc("d"), gc("e"));
+
+    // Composite pictogram example, from https://en.wikipedia.org/wiki/Zero-width_joiner.
+    var flag = "\u{1f3f3}"; // U+1F3F3, Flag, waving. Category Pictogram.
+    var white = "\ufe0f"; // U+FE0F, Variant selector 16. Category Extend.
+    var zwj = "\u200d"; // U+200D, ZWJ
+    var rainbow = "\u{1f308}"; // U+1F308, Rainbow. Category Pictogram
+
+    testParts(gc("$flag$white$zwj$rainbow"), gc("$flag$white"), gc("$rainbow"),
+        gc("$flag$zwj$rainbow"), gc("!"));
+  });
+}
+
+void tests() {
+  test("empty", () {
+    expectGC(gc(""), []);
+  });
+  group("gc-ASCII", () {
+    for (var text in [
+      "",
+      "A",
+      "123456abcdefab",
+    ]) {
+      test('"$text"', () {
+        expectGC(gc(text), charsOf(text));
+      });
+    }
+    test("CR+NL", () {
+      expectGC(gc("a\r\nb"), ["a", "\r\n", "b"]);
+      expectGC(gc("a\n\rb"), ["a", "\n", "\r", "b"]);
+    });
+  });
+  group("Non-ASCII single-code point", () {
+    for (var text in [
+      "à la mode",
+      "rødgrød-æble-ål",
+    ]) {
+      test('"$text"', () {
+        expectGC(gc(text), charsOf(text));
+      });
+    }
+  });
+  group("Combining marks", () {
+    var text = "a\u0300 la mode";
+    test('"$text"', () {
+      expectGC(gc(text), ["a\u0300", " ", "l", "a", " ", "m", "o", "d", "e"]);
+    });
+    var text2 = "æble-a\u030Al";
+    test('"$text2"', () {
+      expectGC(gc(text2), ["æ", "b", "l", "e", "-", "a\u030A", "l"]);
+    });
+  });
+
+  group("Regional Indicators", () {
+    test('"πŸ‡¦πŸ‡©πŸ‡°πŸ‡ΎπŸ‡ͺπŸ‡Έ"', () {
+      // Andorra, Cayman Islands, Spain.
+      expectGC(gc("πŸ‡¦πŸ‡©πŸ‡°πŸ‡ΎπŸ‡ͺπŸ‡Έ"), ["πŸ‡¦πŸ‡©", "πŸ‡°πŸ‡Ύ", "πŸ‡ͺπŸ‡Έ"]);
+    });
+    test('"XπŸ‡¦πŸ‡©πŸ‡°πŸ‡ΎπŸ‡ͺπŸ‡Έ"', () {
+      // Other, Andorra, Cayman Islands, Spain.
+      expectGC(gc("XπŸ‡¦πŸ‡©πŸ‡°πŸ‡ΎπŸ‡ͺπŸ‡Έ"), ["X", "πŸ‡¦πŸ‡©", "πŸ‡°πŸ‡Ύ", "πŸ‡ͺπŸ‡Έ"]);
+    });
+    test('"πŸ‡©πŸ‡°πŸ‡ΎπŸ‡ͺπŸ‡Έ"', () {
+      // Denmark, Yemen, unmatched S.
+      expectGC(gc("πŸ‡©πŸ‡°πŸ‡ΎπŸ‡ͺπŸ‡Έ"), ["πŸ‡©πŸ‡°", "πŸ‡ΎπŸ‡ͺ", "πŸ‡Έ"]);
+    });
+    test('"XπŸ‡©πŸ‡°πŸ‡ΎπŸ‡ͺπŸ‡Έ"', () {
+      // Other, Denmark, Yemen, unmatched S.
+      expectGC(gc("XπŸ‡©πŸ‡°πŸ‡ΎπŸ‡ͺπŸ‡Έ"), ["X", "πŸ‡©πŸ‡°", "πŸ‡ΎπŸ‡ͺ", "πŸ‡Έ"]);
+    });
+  });
+
+  group("Hangul", () {
+    // Individual characters found on Wikipedia. Not expected to make sense.
+    test('"읍쌍된밟"', () {
+      expectGC(gc("읍쌍된밟"), ["읍", "쌍", "된", "밟"]);
+    });
+  });
+
+  group("Unicode test", () {
+    for (var gcs in splitTests) {
+      test("[${testDescription(gcs)}]", () {
+        expectGC(gc(gcs.join()), gcs);
+      });
+    }
+  });
+
+  group("Emoji test", () {
+    for (var gcs in emojis) {
+      test("[${testDescription(gcs)}]", () {
+        expectGC(gc(gcs.join()), gcs);
+      });
+    }
+  });
+
+  group("Zalgo test", () {
+    for (var gcs in zalgo) {
+      test("[${testDescription(gcs)}]", () {
+        expectGC(gc(gcs.join()), gcs);
+      });
+    }
+  });
+}
+
+// Converts text with no multi-code-point grapheme clusters into
+// list of grapheme clusters.
+List<String> charsOf(String text) =>
+    text.runes.map((r) => String.fromCharCode(r)).toList();
+
+void expectGC(Characters actual, List<String> expected) {
+  var text = expected.join();
+
+  // Iterable operations.
+  expect(actual.string, text);
+  expect(actual.toString(), text);
+  expect(actual.toList(), expected);
+  expect(actual.length, expected.length);
+  if (expected.isNotEmpty) {
+    expect(actual.first, expected.first);
+    expect(actual.last, expected.last);
+  } else {
+    expect(() => actual.first, throwsStateError);
+    expect(() => actual.last, throwsStateError);
+  }
+  if (expected.length == 1) {
+    expect(actual.single, expected.single);
+  } else {
+    expect(() => actual.single, throwsStateError);
+  }
+  expect(actual.isEmpty, expected.isEmpty);
+  expect(actual.isNotEmpty, expected.isNotEmpty);
+  expect(actual.contains(""), false);
+  for (var char in expected) {
+    expect(actual.contains(char), true);
+  }
+  for (int i = 1; i < expected.length; i++) {
+    expect(actual.contains(expected[i - 1] + expected[i]), false);
+  }
+  expect(actual.skip(1).toList(), expected.skip(1).toList());
+  expect(actual.take(1).toList(), expected.take(1).toList());
+  expect(actual.skip(1).toString(), expected.skip(1).join());
+  expect(actual.take(1).toString(), expected.take(1).join());
+
+  if (expected.isNotEmpty) {
+    expect(actual.skipLast(1).toList(),
+        expected.take(expected.length - 1).toList());
+    expect(actual.takeLast(1).toList(),
+        expected.skip(expected.length - 1).toList());
+    expect(actual.skipLast(1).toString(),
+        expected.take(expected.length - 1).join());
+    expect(actual.takeLast(1).toString(),
+        expected.skip(expected.length - 1).join());
+  }
+  bool isEven(String s) => s.length.isEven;
+
+  expect(
+      actual.skipWhile(isEven).toList(), expected.skipWhile(isEven).toList());
+  expect(
+      actual.takeWhile(isEven).toList(), expected.takeWhile(isEven).toList());
+  expect(
+      actual.skipWhile(isEven).toString(), expected.skipWhile(isEven).join());
+  expect(
+      actual.takeWhile(isEven).toString(), expected.takeWhile(isEven).join());
+
+  expect(actual.skipLastWhile(isEven).toString(),
+      expected.toList().reversed.skipWhile(isEven).toList().reversed.join());
+  expect(actual.takeLastWhile(isEven).toString(),
+      expected.toList().reversed.takeWhile(isEven).toList().reversed.join());
+
+  expect(actual.where(isEven).toString(), expected.where(isEven).join());
+
+  expect((actual + actual).toString(), actual.string + actual.string);
+
+  // Iteration.
+  var it = actual.iterator;
+  expect(it.isEmpty, true);
+  for (var i = 0; i < expected.length; i++) {
+    expect(it.moveNext(), true);
+    expect(it.current, expected[i]);
+
+    expect(actual.elementAt(i), expected[i]);
+    expect(actual.skip(i).first, expected[i]);
+  }
+  expect(it.moveNext(), false);
+  for (var i = expected.length - 1; i >= 0; i--) {
+    expect(it.moveBack(), true);
+    expect(it.current, expected[i]);
+  }
+  expect(it.moveBack(), false);
+  expect(it.isEmpty, true);
+
+  // GraphemeClusters operations.
+  expect(actual.toUpperCase().string, text.toUpperCase());
+  expect(actual.toLowerCase().string, text.toLowerCase());
+
+  expect(actual.string, text);
+
+  expect(actual.containsAll(gc("")), true);
+  expect(actual.containsAll(actual), true);
+  if (expected.isNotEmpty) {
+    int steps = min(5, expected.length);
+    for (int s = 0; s <= steps; s++) {
+      int i = expected.length * s ~/ steps;
+      expect(actual.startsWith(gc(expected.sublist(0, i).join())), true);
+      expect(actual.endsWith(gc(expected.sublist(i).join())), true);
+      for (int t = s + 1; t <= steps; t++) {
+        int j = expected.length * t ~/ steps;
+        var slice = expected.sublist(i, j).join();
+        var gcs = gc(slice);
+        expect(actual.containsAll(gcs), true);
+      }
+    }
+  }
+
+  {
+    // Random walk back and forth.
+    var it = actual.iterator;
+    int pos = -1;
+    if (random.nextBool()) {
+      pos = expected.length;
+      it = actual.iteratorAtEnd;
+    }
+    int steps = 5 + random.nextInt(expected.length * 2 + 1);
+    bool lastMove = false;
+    while (true) {
+      bool back = false;
+      if (pos < 0) {
+        expect(lastMove, false);
+        expect(it.isEmpty, true);
+      } else if (pos >= expected.length) {
+        expect(lastMove, false);
+        expect(it.isEmpty, true);
+        back = true;
+      } else {
+        expect(lastMove, true);
+        expect(it.current, expected[pos]);
+        back = random.nextBool();
+      }
+      if (--steps < 0) break;
+      if (back) {
+        lastMove = it.moveBack();
+        pos -= 1;
+      } else {
+        lastMove = it.moveNext();
+        pos += 1;
+      }
+    }
+  }
+}
+
+Characters gc(String string) => Characters(string);
+
+void testParts(
+    Characters a, Characters b, Characters c, Characters d, Characters e) {
+  var cs = gc("$a$b$c$d$e");
+  test("$cs", () {
+    var it = cs.iterator;
+    expect(it.isEmpty, true);
+    expect(it.isNotEmpty, false);
+    expect(it.current, "");
+
+    // moveNext().
+    expect(it.moveNext(), true);
+    expect(it.isEmpty, false);
+    expect(it.current, "$a");
+    expect(it.moveNext(), true);
+    expect(it.isEmpty, false);
+    expect(it.current, "$b");
+    expect(it.moveNext(), true);
+    expect(it.isEmpty, false);
+    expect(it.current, "$c");
+    expect(it.moveNext(), true);
+    expect(it.isEmpty, false);
+    expect(it.current, "$d");
+    expect(it.moveNext(), true);
+    expect(it.isEmpty, false);
+    expect(it.current, "$e");
+    expect(it.moveNext(), false);
+    expect(it.isEmpty, true);
+    expect(it.current, "");
+
+    // moveBack().
+    expect(it.moveBack(), true);
+    expect(it.isEmpty, false);
+    expect(it.current, "$e");
+    expect(it.moveBack(), true);
+    expect(it.isEmpty, false);
+    expect(it.current, "$d");
+    expect(it.moveBack(), true);
+    expect(it.isEmpty, false);
+    expect(it.current, "$c");
+    expect(it.moveBack(), true);
+    expect(it.isEmpty, false);
+    expect(it.current, "$b");
+    expect(it.moveBack(), true);
+    expect(it.isEmpty, false);
+    expect(it.current, "$a");
+    expect(it.moveBack(), false);
+    expect(it.isEmpty, true);
+    expect(it.current, "");
+
+    // moveNext(int).
+    expect(it.moveTo(c), true);
+    expect(it.current, "$c");
+    expect(it.moveTo(b), false);
+    expect(it.moveTo(c), false);
+    expect(it.current, "$c");
+    expect(it.moveTo(d), true);
+    expect(it.current, "$d");
+
+    // moveBack(c).
+    expect(it.moveBackTo(c), true);
+    expect(it.current, "$c");
+    expect(it.moveBackTo(d), false);
+    expect(it.moveBackTo(c), false);
+    expect(it.moveBackTo(a), true);
+    expect(it.current, "$a");
+
+    // moveNext(n)
+    expect(it.moveBack(), false);
+
+    expect(it.moveNext(2), true);
+    expect(it.current, "$a$b");
+    expect(it.moveNext(4), false);
+    expect(it.current, "$c$d$e");
+    expect(it.moveNext(0), true);
+    expect(it.current, "");
+    expect(it.moveNext(1), false);
+    expect(it.current, "");
+
+    // moveBack(n).
+    expect(it.moveBack(2), true);
+    expect(it.current, "$d$e");
+    expect(it.moveBack(1), true);
+    expect(it.current, "$c");
+    expect(it.moveBack(3), false);
+    expect(it.current, "$a$b");
+    expect(it.moveBack(), false);
+
+    // moveFirst.
+    it.expandAll();
+    expect(it.current, "$a$b$c$d$e");
+    expect(it.collapseToFirst(b), true);
+    expect(it.current, "$b");
+    it.expandAll();
+    expect(it.current, "$b$c$d$e");
+    expect(it.collapseToFirst(a), false);
+    expect(it.current, "$b$c$d$e");
+
+    // moveBackTo
+    it.expandBackAll();
+    expect(it.current, "$a$b$c$d$e");
+    expect(it.collapseToLast(c), true);
+    expect(it.current, "$c");
+
+    // includeNext/includePrevious
+    expect(it.expandTo(e), true);
+    expect(it.current, "$c$d$e");
+    expect(it.expandTo(e), false);
+    expect(it.expandBackTo(b), true);
+    expect(it.current, "$b$c$d$e");
+    expect(it.expandBackTo(b), false);
+    expect(it.current, "$b$c$d$e");
+    expect(it.collapseToFirst(c), true);
+    expect(it.current, "$c");
+
+    // includeUntilNext/expandBackUntil
+    expect(it.expandBackUntil(a), true);
+    expect(it.current, "$b$c");
+    expect(it.expandBackUntil(a), true);
+    expect(it.current, "$b$c");
+    expect(it.expandUntil(e), true);
+    expect(it.current, "$b$c$d");
+    expect(it.expandUntil(e), true);
+    expect(it.current, "$b$c$d");
+
+    // dropFirst/dropLast
+    expect(it.dropFirst(), true);
+    expect(it.current, "$c$d");
+    expect(it.dropLast(), true);
+    expect(it.current, "$c");
+    it.expandBackAll();
+    it.expandAll();
+    expect(it.current, "$a$b$c$d$e");
+    expect(it.dropTo(b), true);
+    expect(it.current, "$c$d$e");
+    expect(it.dropBackTo(d), true);
+    expect(it.current, "$c");
+
+    it.expandBackAll();
+    it.expandAll();
+    expect(it.current, "$a$b$c$d$e");
+
+    expect(it.dropUntil(b), true);
+    expect(it.current, "$b$c$d$e");
+    expect(it.dropBackUntil(d), true);
+    expect(it.current, "$b$c$d");
+
+    it.dropWhile((x) => x == b.string);
+    expect(it.current, "$c$d");
+    it.expandBackAll();
+    expect(it.current, "$a$b$c$d");
+    it.dropBackWhile((x) => x != b.string);
+    expect(it.current, "$a$b");
+    it.dropBackWhile((x) => false);
+    expect(it.current, "$a$b");
+
+    // include..While
+    it.expandWhile((x) => false);
+    expect(it.current, "$a$b");
+    it.expandWhile((x) => x != e.string);
+    expect(it.current, "$a$b$c$d");
+    expect(it.collapseToFirst(c), true);
+    expect(it.current, "$c");
+    it.expandBackWhile((x) => false);
+    expect(it.current, "$c");
+    it.expandBackWhile((x) => x != a.string);
+    expect(it.current, "$b$c");
+
+    var cs2 = cs.replaceAll(c, gc(""));
+    var cs3 = cs.replaceFirst(c, gc(""));
+    var cs4 = cs.findFirst(c)!.replaceRange(gc("")).source;
+    var cse = gc("$a$b$d$e");
+    expect(cs2, cse);
+    expect(cs3, cse);
+    expect(cs4, cse);
+    var cs5 = cs4.replaceAll(a, c);
+    expect(cs5, gc("$c$b$d$e"));
+    var cs6 = cs5.replaceAll(gc(""), a);
+    expect(cs6, gc("$a$c$a$b$a$d$a$e$a"));
+    var cs7 = cs6.replaceFirst(b, a);
+    expect(cs7, gc("$a$c$a$a$a$d$a$e$a"));
+    var cs8 = cs7.replaceFirst(e, a);
+    expect(cs8, gc("$a$c$a$a$a$d$a$a$a"));
+    var cs9 = cs8.replaceAll(a + a, b);
+    expect(cs9, gc("$a$c$b$a$d$b$a"));
+    it = cs9.iterator;
+    it.moveTo(b + a);
+    expect("$b$a", it.current);
+    it.expandTo(b + a);
+    expect("$b$a$d$b$a", it.current);
+    var cs10 = it.replaceAll(b + a, e + e)!;
+    expect(cs10.currentCharacters, e + e + d + e + e);
+    expect(cs10.source, gc("$a$c$e$e$d$e$e"));
+    var cs11 = it.replaceRange(e);
+    expect(cs11.currentCharacters, e);
+    expect(cs11.source, gc("$a$c$e"));
+
+    var cs12 = gc("$a$b$a");
+    expect(cs12.split(b), [a, a]);
+    expect(cs12.split(a), [gc(""), b, gc("")]);
+    expect(cs12.split(a, 2), [gc(""), gc("$b$a")]);
+
+    expect(cs12.split(gc("")), [a, b, a]);
+    expect(cs12.split(gc(""), 2), [a, gc("$b$a")]);
+
+    expect(gc("").split(gc("")), [gc("")]);
+
+    var cs13 = gc("$b$a$b$a$b$a");
+    expect(cs13.split(b), [gc(""), a, a, a]);
+    expect(cs13.split(b, 1), [cs13]);
+    expect(cs13.split(b, 2), [gc(""), gc("$a$b$a$b$a")]);
+    expect(cs13.split(b, 3), [gc(""), a, gc("$a$b$a")]);
+    expect(cs13.split(b, 4), [gc(""), a, a, a]);
+    expect(cs13.split(b, 5), [gc(""), a, a, a]);
+    expect(cs13.split(b, 9999), [gc(""), a, a, a]);
+    expect(cs13.split(b, 0), [gc(""), a, a, a]);
+    expect(cs13.split(b, -1), [gc(""), a, a, a]);
+    expect(cs13.split(b, -9999), [gc(""), a, a, a]);
+
+    it = cs13.iterator..expandAll();
+    expect(it.current, "$b$a$b$a$b$a");
+    it.dropFirst();
+    it.dropLast();
+    expect(it.current, "$a$b$a$b");
+    expect(it.split(a).map((range) => range.current), ["", "$b", "$b"]);
+    expect(it.split(a, 2).map((range) => range.current), ["", "$b$a$b"]);
+    // Each split is after an *a*.
+    bool first = true;
+    for (var range in it.split(a)) {
+      if (range.isEmpty) {
+        // First range is empty.
+        expect(first, true);
+        first = false;
+        continue;
+      }
+      // Later ranges are "b" that come after "a".
+      expect(range.current, "$b");
+      range.moveBack();
+      expect(range.current, "$a");
+    }
+
+    expect(it.split(gc("")).map((range) => range.current),
+        ["$a", "$b", "$a", "$b"]);
+
+    expect(gc("").iterator.split(gc("")).map((range) => range.current), [""]);
+
+    expect(cs.startsWith(gc("")), true);
+    expect(cs.startsWith(a), true);
+    expect(cs.startsWith(a + b), true);
+    expect(cs.startsWith(gc("$a$b$c")), true);
+    expect(cs.startsWith(gc("$a$b$c$d")), true);
+    expect(cs.startsWith(gc("$a$b$c$d$e")), true);
+    expect(cs.startsWith(b), false);
+    expect(cs.startsWith(c), false);
+    expect(cs.startsWith(d), false);
+    expect(cs.startsWith(e), false);
+
+    expect(cs.endsWith(gc("")), true);
+    expect(cs.endsWith(e), true);
+    expect(cs.endsWith(d + e), true);
+    expect(cs.endsWith(gc("$c$d$e")), true);
+    expect(cs.endsWith(gc("$b$c$d$e")), true);
+    expect(cs.endsWith(gc("$a$b$c$d$e")), true);
+    expect(cs.endsWith(d), false);
+    expect(cs.endsWith(c), false);
+    expect(cs.endsWith(b), false);
+    expect(cs.endsWith(a), false);
+
+    it = cs.findFirst(b + c)!;
+    expect(it.startsWith(gc("")), true);
+    expect(it.startsWith(b), true);
+    expect(it.startsWith(b + c), true);
+    expect(it.startsWith(a + b + c), false);
+    expect(it.startsWith(b + c + d), false);
+    expect(it.startsWith(a), false);
+
+    expect(it.endsWith(gc("")), true);
+    expect(it.endsWith(c), true);
+    expect(it.endsWith(b + c), true);
+    expect(it.endsWith(a + b + c), false);
+    expect(it.endsWith(b + c + d), false);
+    expect(it.endsWith(d), false);
+
+    it.collapseToFirst(c);
+    expect(it.isPrecededBy(gc("")), true);
+    expect(it.isPrecededBy(b), true);
+    expect(it.isPrecededBy(a + b), true);
+    expect(it.isPrecededBy(a + b + c), false);
+    expect(it.isPrecededBy(a), false);
+
+    expect(it.isFollowedBy(gc("")), true);
+    expect(it.isFollowedBy(d), true);
+    expect(it.isFollowedBy(d + e), true);
+    expect(it.isFollowedBy(c + d + e), false);
+    expect(it.isFollowedBy(e), false);
+  });
+  test("replace methods", () {
+    // Unicode grapheme breaking character classes,
+    // represented by their first value.
+
+    var pattern = gc("\t"); // A non-combining entry to be replaced.
+    var non = gc("");
+
+    var c = otr + cr + pattern + lf + pic + pattern + zwj + pic + otr;
+    var r = c.replaceAll(pattern, non);
+    expect(r, otr + cr + lf + pic + zwj + pic + otr);
+    var ci = c.iterator..moveNextAll();
+    var ri = ci.replaceAll(pattern, non)!;
+    expect(ri.currentCharacters, otr + cr + lf + pic + zwj + pic + otr);
+    ci.dropFirst();
+    ci.dropLast();
+    expect(ci.currentCharacters, cr + pattern + lf + pic + pattern + zwj + pic);
+    expect(ci.currentCharacters.length, 7);
+    ri = ci.replaceAll(pattern, non)!;
+    expect(ri.currentCharacters, cr + lf + pic + zwj + pic);
+    expect(ri.currentCharacters.length, 2);
+    ci.dropFirst();
+    ci.dropLast(5);
+    expect(ci.currentCharacters, pattern);
+    ri = ci.replaceAll(pattern, non)!;
+    expect(ri.currentCharacters, cr + lf);
+    ci.moveNext(2);
+    ci.moveNext(1);
+    expect(ci.currentCharacters, pattern);
+    ri = ci.replaceAll(pattern, non)!;
+    expect(ri.currentCharacters, pic + zwj + pic);
+
+    c = otr + pic + ext + pattern + pic + ext + otr;
+    expect(c.length, 5);
+    ci = c.iterator..moveTo(pattern);
+    expect(ci.currentCharacters, pattern);
+    ri = ci.replaceAll(pattern, zwj)!;
+    expect(ri.currentCharacters, pic + ext + zwj + pic + ext);
+
+    c = reg + pattern + reg + reg;
+    ci = c.iterator..moveTo(pattern);
+    ri = ci.replaceRange(non);
+    expect(ri.currentCharacters, reg + reg);
+    expect(ri.moveNext(), true);
+    expect(ri.currentCharacters, reg);
+  });
+}
+
+/// Sample characters from each breaking algorithm category.
+final Characters ctl = gc("\x00"); // Control, NUL.
+final Characters cr = gc("\r"); // Carriage Return, CR.
+final Characters lf = gc("\n"); // Newline, NL.
+final Characters otr = gc(" "); // Other, Space.
+final Characters ext = gc("\u0300"); // Extend, Combining Grave Accent.
+final Characters spc = gc("\u0903"); // Spacing Mark, Devanagari Sign Visarga.
+final Characters pre = gc("\u0600"); // Prepend, Arabic Number Sign.
+final Characters zwj = gc("\u200d"); // Zero-Width Joiner.
+final Characters pic = gc("\u00a9"); // Extended Pictographic, Copyright.
+final Characters reg = gc("\u{1f1e6}"); // Regional Identifier "a".
+final Characters hanl = gc("\u1100"); // Hangul L, Choseong Kiyeok.
+final Characters hanv = gc("\u1160"); // Hangul V, Jungseong Filler.
+final Characters hant = gc("\u11a8"); // Hangul T, Jongseong Kiyeok.
+final Characters hanlv = gc("\uac00"); // Hangul LV, Syllable Ga.
+final Characters hanlvt = gc("\uac01"); // Hangul LVT, Syllable Gag.
diff --git a/test/src/unicode_tests.dart b/test/src/unicode_tests.dart
index 740c966..23bc1ca 100644
--- a/test/src/unicode_tests.dart
+++ b/test/src/unicode_tests.dart
@@ -16,7 +16,7 @@
       " ÷";
 }
 
-var categoryName = List<String>(16)
+var categoryName = List<String>.filled(16, "")
   ..[categoryOther] = "Other"
   ..[categoryCR] = "CR"
   ..[categoryLF] = "LF"