Add some extra functionality. (#34)

Add some extra functionality

Also do some clean-up.

Fixes #21, #22 and #24:

* https://github.com/dart-lang/characters/issues/24
* https://github.com/dart-lang/characters/issues/22
* https://github.com/dart-lang/characters/issues/21

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 989359e..31cad3a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,8 +1,15 @@
+## 1.1.0-nullsafety.3
+
+* Added `stringBeforeLength` and `stringAfterLength` to `CharacterRange`.
+* Added `CharacterRange.at` constructor.
+* Added `getRange(start, end)` and `characterAt(pos)` to `Characters`
+  as alternative to `.take(end).skip(start)` and `getRange(pos, pos + 1)`.
+* Change some positional parameter names from `other` to `characters`.
+
 ## 1.1.0-nullsafety.2
 
 * Update for the 2.10 dev sdk.
 
-
 ## 1.1.0-nullsafety.1
 
 * Allow the <=2.9.10 stable sdks.
diff --git a/analysis_options.yaml b/analysis_options.yaml
index d34d1f0..dd48aa5 100644
--- a/analysis_options.yaml
+++ b/analysis_options.yaml
@@ -1,8 +1,6 @@
-include: package:pedantic/analysis_options.yaml
+include: package:pedantic/analysis_options.1.9.0.yaml
 analyzer:
   errors:
-    omit_local_variable_types: ignore
-    annotate_overrides: ignore
     prefer_single_quotes: ignore
     use_function_type_syntax_for_parameters: ignore
   enable-experiment:
diff --git a/benchmark/benchmark.dart b/benchmark/benchmark.dart
index c6ab05e..7862564 100644
--- a/benchmark/benchmark.dart
+++ b/benchmark/benchmark.dart
@@ -9,8 +9,8 @@
 import "../test/src/text_samples.dart";
 
 double bench(int Function() action, int ms) {
-  int elapsed = 0;
-  int count = 0;
+  var elapsed = 0;
+  var count = 0;
   var stopwatch = Stopwatch()..start();
   do {
     count += action();
@@ -20,7 +20,7 @@
 }
 
 int iterateIndicesOnly() {
-  int graphemeClusters = 0;
+  var graphemeClusters = 0;
   var char = Characters(hangul).iterator;
   while (char.moveNext()) {
     graphemeClusters++;
@@ -33,7 +33,7 @@
 }
 
 int iterateStrings() {
-  int codeUnits = 0;
+  var codeUnits = 0;
   var char = Characters(hangul).iterator;
   while (char.moveNext()) {
     codeUnits += char.current.length;
@@ -58,7 +58,7 @@
 }
 
 int replaceStrings() {
-  int count = 0;
+  var count = 0;
   {
     const language = "한글";
     assert(language.length == 6);
@@ -85,7 +85,7 @@
 }
 
 void main(List<String> args) {
-  int count = 1;
+  var count = 1;
   if (args.isNotEmpty) count = int.tryParse(args[0]) ?? 1;
 
   // Warmup.
@@ -94,7 +94,7 @@
   bench(reverseStrings, 250);
   bench(replaceStrings, 250);
 
-  for (int i = 0; i < count; i++) {
+  for (var i = 0; i < count; i++) {
     var performance = bench(iterateIndicesOnly, 2000);
     print("Index Iteration: $performance gc/ms");
     performance = bench(iterateStrings, 2000);
diff --git a/example/main.dart b/example/main.dart
index 467a2f4..46ba94b 100644
--- a/example/main.dart
+++ b/example/main.dart
@@ -3,7 +3,7 @@
 // Small API examples. For full API docs see:
 // https://pub.dev/documentation/characters/latest/characters/characters-library.html
 void main() {
-  String hi = 'Hi 🇩🇰';
+  var hi = 'Hi 🇩🇰';
   print('String is "$hi"\n');
 
   // Length.
@@ -19,7 +19,6 @@
   print('Skipping last character: "${hi.characters.skipLast(1)}"\n');
 
   // Replace characters.
-  Characters newHi =
-      hi.characters.replaceAll('🇩🇰'.characters, '🇺🇸'.characters);
+  var newHi = hi.characters.replaceAll('🇩🇰'.characters, '🇺🇸'.characters);
   print('Change flag: "$newHi"');
 }
diff --git a/lib/src/characters.dart b/lib/src/characters.dart
index 70b40eb..365beac 100644
--- a/lib/src/characters.dart
+++ b/lib/src/characters.dart
@@ -35,6 +35,7 @@
   /// Allows iterating the characters of [string] as a plain iterator,
   /// using [CharacterRange.moveNext],
   /// as well as controlling the iteration in more detail.
+  @override
   CharacterRange get iterator;
 
   /// Iterator over the characters of this string.
@@ -54,6 +55,7 @@
   /// a single character,
   /// because then it is not a single element of this [Iterable]
   /// of characters.
+  @override
   bool contains(Object? other);
 
   /// Whether this sequence of characters contains [other]
@@ -92,6 +94,7 @@
   ///
   /// Tests each character against [test], and returns the
   /// characters of the concatenation of those character strings.
+  @override
   Characters where(bool Function(String) test);
 
   /// Eagerly selects all but the first [count] characters.
@@ -99,6 +102,7 @@
   /// If [count] is greater than [length], the count of character
   /// available, then the empty sequence of characters
   /// is returned.
+  @override
   Characters skip(int count);
 
   /// Eagerly selects the first [count] characters.
@@ -106,8 +110,41 @@
   /// If [count] is greater than [length], the count of character
   /// available, then the entire sequence of characters
   /// is returned.
+  @override
   Characters take(int count);
 
+  /// Eagerly selects the range of characters from [start] to [end].
+  ///
+  /// The [start] value must be non-negative,
+  /// and [end], if supplied, must be greater than or equal to [start].
+  ///
+  /// A `characters.getRange(start, end)` is equivalent to
+  /// ```dart
+  ///  (end != null ? characters.take(end) : characters).skip(start)
+  /// ```
+  /// if [end] is omitted, the call is equivalent to `characters.skip(start)`.
+  ///
+  /// If [start] is greater than or equal to [length]
+  /// the returned characters is empty.
+  /// Otherwise, if [end] is greater than [length], or omitted,
+  /// [end] is equivalent to [length].
+  Characters getRange(int start, [int? end]);
+
+  /// Returns the single-character sequence of the [position]th character.
+  ///
+  /// The [position] must be non-negative and less than [length].
+  ///
+  /// This operation must iterate characters up to [position] to find
+  /// the result, just like [elementAt].
+  /// It is not an efficient way to iterate over the individual characters
+  /// of a `Characters`.
+  ///
+  /// An call to `chars.characterAt(n)`
+  /// is equivalent to `chars.elementAt(n).characters`,
+  /// or to `chars.getRange(n, n + 1)`
+  /// (except that [getRange] allows `n` to be larger than [length]).
+  Characters characterAt(int position);
+
   /// Eagerly selects all but the last [count] characters.
   ///
   /// If [count] is greater than [length], the count of character
@@ -131,6 +168,7 @@
   ///
   /// If no characters test `false`, the result is an empty sequence
   /// of characters.
+  @override
   Characters skipWhile(bool Function(String) test);
 
   /// Eagerly selects a leading sequence of characters.
@@ -142,6 +180,7 @@
   ///
   /// If no characters test `false`, the entire sequence of character
   /// is returned.
+  @override
   Characters takeWhile(bool Function(String) test);
 
   /// Eagerly selects a leading sequence of characters.
@@ -236,12 +275,15 @@
   Characters toUpperCase();
 
   /// The hash code of [string].
+  @override
   int get hashCode;
 
   /// Whether [other] to another [Characters] with the same [string].
+  @override
   bool operator ==(Object other);
 
   /// The [string] content of these characters.
+  @override
   String toString();
 }
 
@@ -281,6 +323,28 @@
   /// of [string].
   factory CharacterRange(String string) = StringCharacterRange;
 
+  /// Creates a new character iterator on [string].
+  ///
+  /// The iterator starts with a current range containing the
+  /// substring from [startIndex] to [endIndex] of [string].
+  /// If [startIndex] is not a character boundary,
+  /// the range starts at the beginning of the character
+  /// that [startIndex] is pointing into.
+  /// If [endIndex] is not a character boundary,
+  /// the range end at then end of the character
+  /// that [endIndex] is pointing into.
+  /// If [endIndex] is not provided,
+  /// it defaults to the same index as [startIndex].
+  ///
+  /// So, if no [endIndex] is provided,
+  /// and [startIndex] is at a character boundary,
+  /// the resulting iterator's current range is empty.
+  /// Otherwise, the range will contain the characters
+  /// of the substring from [startIndex] to [endIndex],
+  /// extended so that it contains entire characters of the original [string].
+  factory CharacterRange.at(String string, int startIndex, [int? endIndex]) =
+      StringCharacterRange.at;
+
   /// The character sequence that this range is a sub-sequence of.
   Characters get source;
 
@@ -302,9 +366,15 @@
   /// The string of the characters before the current range.
   String get stringBefore;
 
+  /// The length, in code units, of [stringBefore].
+  int get stringBeforeLength;
+
   /// The string of the characters after the current range.
   String get stringAfter;
 
+  /// The length, in code units, of [stringAfter].
+  int get stringAfterLength;
+
   /// Creates a copy of this [CharacterRange].
   ///
   /// The copy is in the exact same state as this iterator.
@@ -336,6 +406,7 @@
   ///
   /// Returns `true` if there were [count] following characters
   /// and `false` if not.
+  @override
   bool moveNext([int count = 1]);
 
   /// Moves the range to be everything after the current range.
diff --git a/lib/src/characters_impl.dart b/lib/src/characters_impl.dart
index dde34d5..c5fddee 100644
--- a/lib/src/characters_impl.dart
+++ b/lib/src/characters_impl.dart
@@ -15,6 +15,7 @@
   // Try to avoid allocating more empty grapheme clusters.
   static const StringCharacters _empty = StringCharacters("");
 
+  @override
   final String string;
 
   const StringCharacters(this.string);
@@ -44,7 +45,7 @@
   @override
   String get single {
     if (string.isEmpty) throw StateError("No element");
-    int firstEnd =
+    var firstEnd =
         Breaks(string, 0, string.length, stateSoTNoBreak).nextBreak();
     if (firstEnd == string.length) return string;
     throw StateError("Too many elements");
@@ -60,7 +61,7 @@
   int get length {
     if (string.isEmpty) return 0;
     var brk = Breaks(string, 0, string.length, stateSoTNoBreak);
-    int length = 0;
+    var length = 0;
     while (brk.nextBreak() >= 0) {
       length++;
     }
@@ -83,27 +84,28 @@
   }
 
   @override
-  String lastWhere(bool test(String element), {String orElse()?}) {
-    int cursor = string.length;
+  String lastWhere(bool Function(String element) test,
+      {String Function()? orElse}) {
+    var cursor = string.length;
     var brk = BackBreaks(string, cursor, 0, stateEoTNoBreak);
-    int next = 0;
+    var next = 0;
     while ((next = brk.nextBreak()) >= 0) {
-      String current = string.substring(next, cursor);
+      var current = string.substring(next, cursor);
       if (test(current)) return current;
       cursor = next;
     }
     if (orElse != null) return orElse();
-    throw StateError("no element");
+    throw StateError("No element");
   }
 
   @override
   String elementAt(int index) {
     RangeError.checkNotNegative(index, "index");
-    int count = 0;
+    var count = 0;
     if (string.isNotEmpty) {
       var breaks = Breaks(string, 0, string.length, stateSoTNoBreak);
-      int start = 0;
-      int end = 0;
+      var start = 0;
+      var end = 0;
       while ((end = breaks.nextBreak()) >= 0) {
         if (count == index) return string.substring(start, end);
         count++;
@@ -117,7 +119,7 @@
   bool contains(Object? other) {
     if (other is String) {
       if (other.isEmpty) return false;
-      int next = Breaks(other, 0, other.length, stateSoTNoBreak).nextBreak();
+      var next = Breaks(other, 0, other.length, stateSoTNoBreak).nextBreak();
       if (next != other.length) return false;
       // [other] is single grapheme cluster.
       return _indexOf(string, other, 0, string.length) >= 0;
@@ -126,21 +128,21 @@
   }
 
   @override
-  bool startsWith(Characters other) {
-    int length = string.length;
-    String otherString = other.string;
+  bool startsWith(Characters characters) {
+    var length = string.length;
+    var otherString = characters.string;
     if (otherString.isEmpty) return true;
     return string.startsWith(otherString) &&
         isGraphemeClusterBoundary(string, 0, length, otherString.length);
   }
 
   @override
-  bool endsWith(Characters other) {
-    int length = string.length;
-    String otherString = other.string;
+  bool endsWith(Characters characters) {
+    var length = string.length;
+    var otherString = characters.string;
     if (otherString.isEmpty) return true;
-    int otherLength = otherString.length;
-    int start = string.length - otherLength;
+    var otherLength = otherString.length;
+    var start = string.length - otherLength;
     return start >= 0 &&
         string.startsWith(otherString, start) &&
         isGraphemeClusterBoundary(string, 0, length, start);
@@ -186,60 +188,90 @@
   }
 
   @override
-  bool containsAll(Characters other) =>
-      _indexOf(string, other.string, 0, string.length) >= 0;
+  bool containsAll(Characters characters) =>
+      _indexOf(string, characters.string, 0, string.length) >= 0;
+
+  /// Returns the break position of the [count]'th break.
+  ///
+  /// Starts from the index [cursor] in [string].
+  /// Use [breaks], which is assumed to be at [cursor],
+  /// if available.
+  ///
+  /// Returns `string.length` if there are less than [count]
+  /// characters left.
+  int _skipIndices(int count, int cursor, Breaks? breaks) {
+    if (count == 0 || cursor == string.length) return cursor;
+    breaks ??= Breaks(string, cursor, string.length, stateSoTNoBreak);
+    do {
+      var nextBreak = breaks.nextBreak();
+      if (nextBreak < 0) break;
+      cursor = nextBreak;
+    } while (--count > 0);
+    return cursor;
+  }
 
   @override
   Characters skip(int count) {
     RangeError.checkNotNegative(count, "count");
-    if (count == 0) return this;
-    if (string.isNotEmpty) {
-      int stringLength = string.length;
-      var breaks = Breaks(string, 0, stringLength, stateSoTNoBreak);
-      int startIndex = 0;
-      while (count > 0) {
-        int index = breaks.nextBreak();
-        if (index >= 0) {
-          count--;
-          startIndex = index;
-        } else {
-          return _empty;
-        }
-      }
-      if (startIndex == stringLength) return _empty;
-      return StringCharacters(string.substring(startIndex));
-    }
-    return this;
+    return _skip(count);
+  }
+
+  Characters _skip(int count) {
+    var start = _skipIndices(count, 0, null);
+    if (start == string.length) return _empty;
+    return StringCharacters(string.substring(start));
   }
 
   @override
   Characters take(int count) {
     RangeError.checkNotNegative(count, "count");
-    if (count == 0) return _empty;
-    if (string.isNotEmpty) {
-      var breaks = Breaks(string, 0, string.length, stateSoTNoBreak);
-      int endIndex = 0;
-      while (count > 0) {
-        int index = breaks.nextBreak();
-        if (index >= 0) {
-          endIndex = index;
-          count--;
-        } else {
-          return this;
-        }
-      }
-      return StringCharacters(string.substring(0, endIndex));
+    return _take(count);
+  }
+
+  Characters _take(int count) {
+    var end = _skipIndices(count, 0, null);
+    if (end == string.length) return this;
+    return StringCharacters(string.substring(0, end));
+  }
+
+  @override
+  Characters getRange(int start, [int? end]) {
+    RangeError.checkNotNegative(start, "start");
+    if (end == null) return _skip(start);
+    if (end < start) throw RangeError.range(end, start, null, "end");
+    if (end == start) return _empty;
+    if (start == 0) return _take(end);
+    if (string.isEmpty) return this;
+    var breaks = Breaks(string, 0, string.length, stateSoTNoBreak);
+    var startIndex = _skipIndices(start, 0, breaks);
+    if (startIndex == string.length) return _empty;
+    var endIndex = _skipIndices(end - start, start, breaks);
+    return StringCharacters(string.substring(startIndex, endIndex));
+  }
+
+  @override
+  Characters characterAt(int position) {
+    var breaks = Breaks(string, 0, string.length, stateSoTNoBreak);
+    var start = 0;
+
+    while (position > 0) {
+      position--;
+      start = breaks.nextBreak();
+      if (start < 0) throw StateError("No element");
     }
-    return this;
+    var end = breaks.nextBreak();
+    if (end < 0) throw StateError("No element");
+    if (start == 0 && end == string.length) return this;
+    return StringCharacters(string.substring(start, end));
   }
 
   @override
   Characters skipWhile(bool Function(String) test) {
     if (string.isNotEmpty) {
-      int stringLength = string.length;
+      var stringLength = string.length;
       var breaks = Breaks(string, 0, stringLength, stateSoTNoBreak);
-      int index = 0;
-      int startIndex = 0;
+      var index = 0;
+      var startIndex = 0;
       while ((index = breaks.nextBreak()) >= 0) {
         if (!test(string.substring(startIndex, index))) {
           if (startIndex == 0) return this;
@@ -256,8 +288,8 @@
   Characters takeWhile(bool Function(String) test) {
     if (string.isNotEmpty) {
       var breaks = Breaks(string, 0, string.length, stateSoTNoBreak);
-      int index = 0;
-      int endIndex = 0;
+      var index = 0;
+      var endIndex = 0;
       while ((index = breaks.nextBreak()) >= 0) {
         if (!test(string.substring(endIndex, index))) {
           if (endIndex == 0) return _empty;
@@ -277,8 +309,8 @@
   }
 
   @override
-  Characters operator +(Characters other) =>
-      StringCharacters(string + other.string);
+  Characters operator +(Characters characters) =>
+      StringCharacters(string + characters.string);
 
   @override
   Characters skipLast(int count) {
@@ -286,9 +318,9 @@
     if (count == 0) return this;
     if (string.isNotEmpty) {
       var breaks = BackBreaks(string, string.length, 0, stateEoTNoBreak);
-      int endIndex = string.length;
+      var endIndex = string.length;
       while (count > 0) {
-        int index = breaks.nextBreak();
+        var index = breaks.nextBreak();
         if (index >= 0) {
           endIndex = index;
           count--;
@@ -305,8 +337,8 @@
   Characters skipLastWhile(bool Function(String) test) {
     if (string.isNotEmpty) {
       var breaks = BackBreaks(string, string.length, 0, stateEoTNoBreak);
-      int index = 0;
-      int end = string.length;
+      var index = 0;
+      var end = string.length;
       while ((index = breaks.nextBreak()) >= 0) {
         if (!test(string.substring(index, end))) {
           if (end == string.length) return this;
@@ -324,9 +356,9 @@
     if (count == 0) return _empty;
     if (string.isNotEmpty) {
       var breaks = BackBreaks(string, string.length, 0, stateEoTNoBreak);
-      int startIndex = string.length;
+      var startIndex = string.length;
       while (count > 0) {
-        int index = breaks.nextBreak();
+        var index = breaks.nextBreak();
         if (index >= 0) {
           startIndex = index;
           count--;
@@ -345,8 +377,8 @@
   Characters takeLastWhile(bool Function(String) test) {
     if (string.isNotEmpty) {
       var breaks = BackBreaks(string, string.length, 0, stateEoTNoBreak);
-      int index = 0;
-      int start = string.length;
+      var index = 0;
+      var start = string.length;
       while ((index = breaks.nextBreak()) >= 0) {
         if (!test(string.substring(index, start))) {
           if (start == string.length) return _empty;
@@ -411,6 +443,14 @@
   String? _currentCache;
 
   StringCharacterRange(String string) : this._(string, 0, 0);
+
+  factory StringCharacterRange.at(String string, int startIndex,
+      [int? endIndex]) {
+    RangeError.checkValidRange(
+        startIndex, endIndex, string.length, "startIndex", "endIndex");
+    return _expandRange(string, startIndex, endIndex ?? startIndex);
+  }
+
   StringCharacterRange._(this._string, this._start, this._end);
 
   /// Changes the current range.
@@ -447,15 +487,15 @@
   bool _advanceEnd(int count, int newStart) {
     if (count > 0) {
       var state = stateSoTNoBreak;
-      int index = _end;
+      var index = _end;
       while (index < _string.length) {
-        int char = _string.codeUnitAt(index);
-        int category = categoryControl;
-        int nextIndex = index + 1;
+        var char = _string.codeUnitAt(index);
+        var category = categoryControl;
+        var nextIndex = index + 1;
         if (char & 0xFC00 != 0xD800) {
           category = low(char);
         } else if (nextIndex < _string.length) {
-          int nextChar = _string.codeUnitAt(nextIndex);
+          var nextChar = _string.codeUnitAt(nextIndex);
           if (nextChar & 0xFC00 == 0xDC00) {
             nextIndex += 1;
             category = high(char, nextChar);
@@ -479,7 +519,7 @@
   }
 
   bool _moveNextPattern(String patternString, int start, int end) {
-    int offset = _indexOf(_string, patternString, start, end);
+    var offset = _indexOf(_string, patternString, start, end);
     if (offset >= 0) {
       _move(offset, offset + patternString.length);
       return true;
@@ -493,9 +533,9 @@
   bool _retractStart(int count, int newEnd) {
     RangeError.checkNotNegative(count, "count");
     var breaks = _backBreaksFromStart();
-    int start = _start;
+    var start = _start;
     while (count > 0) {
-      int nextBreak = breaks.nextBreak();
+      var nextBreak = breaks.nextBreak();
       if (nextBreak >= 0) {
         start = nextBreak;
       } else {
@@ -508,7 +548,7 @@
   }
 
   bool _movePreviousPattern(String patternString, int start, int end) {
-    int offset = _lastIndexOf(_string, patternString, start, end);
+    var offset = _lastIndexOf(_string, patternString, start, end);
     if (offset >= 0) {
       _move(offset, offset + patternString.length);
       return true;
@@ -543,7 +583,7 @@
     if (_start == _end) return count == 0;
     var breaks = Breaks(_string, _start, _end, stateSoTNoBreak);
     while (count > 0) {
-      int nextBreak = breaks.nextBreak();
+      var nextBreak = breaks.nextBreak();
       if (nextBreak >= 0) {
         _start = nextBreak;
         _currentCache = null;
@@ -584,8 +624,8 @@
   void dropWhile(bool Function(String) test) {
     if (_start == _end) return;
     var breaks = Breaks(_string, _start, _end, stateSoTNoBreak);
-    int cursor = _start;
-    int next = 0;
+    var cursor = _start;
+    var next = 0;
     while ((next = breaks.nextBreak()) >= 0) {
       if (!test(_string.substring(cursor, next))) {
         break;
@@ -600,7 +640,7 @@
     RangeError.checkNotNegative(count, "count");
     var breaks = BackBreaks(_string, _end, _start, stateEoTNoBreak);
     while (count > 0) {
-      int nextBreak = breaks.nextBreak();
+      var nextBreak = breaks.nextBreak();
       if (nextBreak >= 0) {
         _end = nextBreak;
         _currentCache = null;
@@ -641,8 +681,8 @@
   void dropBackWhile(bool Function(String) test) {
     if (_start == _end) return;
     var breaks = BackBreaks(_string, _end, _start, stateEoTNoBreak);
-    int cursor = _end;
-    int next = 0;
+    var cursor = _end;
+    var next = 0;
     while ((next = breaks.nextBreak()) >= 0) {
       if (!test(_string.substring(next, cursor))) {
         break;
@@ -657,8 +697,8 @@
 
   @override
   bool expandTo(Characters target) {
-    String targetString = target.string;
-    int index = _indexOf(_string, targetString, _end, _string.length);
+    var targetString = target.string;
+    var index = _indexOf(_string, targetString, _end, _string.length);
     if (index >= 0) {
       _move(_start, index + targetString.length);
       return true;
@@ -669,8 +709,8 @@
   @override
   void expandWhile(bool Function(String character) test) {
     var breaks = _breaksFromEnd();
-    int cursor = _end;
-    int next = 0;
+    var cursor = _end;
+    var next = 0;
     while ((next = breaks.nextBreak()) >= 0) {
       if (!test(_string.substring(cursor, next))) {
         break;
@@ -691,7 +731,7 @@
   @override
   bool expandBackTo(Characters target) {
     var targetString = target.string;
-    int index = _lastIndexOf(_string, targetString, 0, _start);
+    var index = _lastIndexOf(_string, targetString, 0, _start);
     if (index >= 0) {
       _move(index, _end);
       return true;
@@ -702,8 +742,8 @@
   @override
   void expandBackWhile(bool Function(String character) test) {
     var breaks = _backBreaksFromStart();
-    int cursor = _start;
-    int next = 0;
+    var cursor = _start;
+    var next = 0;
     while ((next = breaks.nextBreak()) >= 0) {
       if (!test(_string.substring(next, cursor))) {
         _move(cursor, _end);
@@ -768,7 +808,7 @@
   }
 
   bool _advanceEndUntil(String targetString, int newStart) {
-    int index = _indexOf(_string, targetString, _end, _string.length);
+    var index = _indexOf(_string, targetString, _end, _string.length);
     if (index >= 0) {
       _move(newStart, index);
       return true;
@@ -779,13 +819,13 @@
 
   @override
   CharacterRange? replaceFirst(Characters pattern, Characters replacement) {
-    String patternString = pattern.string;
-    String replacementString = replacement.string;
+    var patternString = pattern.string;
+    var replacementString = replacement.string;
     String replaced;
     if (patternString.isEmpty) {
       replaced = _string.replaceRange(_start, _start, replacementString);
     } else {
-      int index = _indexOf(_string, patternString, _start, _end);
+      var index = _indexOf(_string, patternString, _start, _end);
       if (index >= 0) {
         replaced = _string.replaceRange(
             index, index + patternString.length, replacementString);
@@ -793,7 +833,7 @@
         return null;
       }
     }
-    int newEnd = replaced.length - _string.length + _end;
+    var newEnd = replaced.length - _string.length + _end;
     return _expandRange(replaced, _start, newEnd);
   }
 
@@ -804,12 +844,12 @@
     if (patternString.isEmpty) {
       var replaced = _explodeReplace(
           _string, _start, _end, replacementString, replacementString);
-      int newEnd = replaced.length - (_string.length - _end);
+      var newEnd = replaced.length - (_string.length - _end);
       return _expandRange(replaced, _start, newEnd);
     }
     if (_start == _end) return null;
-    int start = 0;
-    int cursor = _start;
+    var start = 0;
+    var cursor = _start;
     StringBuffer? buffer;
     while ((cursor = _indexOf(_string, patternString, cursor, _end)) >= 0) {
       (buffer ??= StringBuffer())
@@ -820,23 +860,28 @@
     }
     if (buffer == null) return null;
     buffer.write(_string.substring(start));
-    String replaced = buffer.toString();
-    int newEnd = replaced.length - (_string.length - _end);
+    var replaced = buffer.toString();
+    var newEnd = replaced.length - (_string.length - _end);
     return _expandRange(replaced, _start, newEnd);
   }
 
   @override
   CharacterRange replaceRange(Characters replacement) {
-    String replacementString = replacement.string;
-    String resultString = _string.replaceRange(_start, _end, replacementString);
+    var replacementString = replacement.string;
+    var resultString = _string.replaceRange(_start, _end, replacementString);
     return _expandRange(
         resultString, _start, _start + replacementString.length);
   }
 
-  // Expands a range if its start or end are not grapheme cluster boundaries.
-  StringCharacterRange _expandRange(String string, int start, int end) {
+  /// Expands a range if its start or end are not grapheme cluster boundaries.
+  ///
+  /// Low-level function which does not validate its input. Assume that
+  /// 0 <= [start] <= [end] <= `string.length`.
+  static StringCharacterRange _expandRange(String string, int start, int end) {
     start = previousBreak(string, 0, string.length, start);
-    end = nextBreak(string, 0, string.length, end);
+    if (end != start) {
+      end = nextBreak(string, 0, string.length, end);
+    }
     return StringCharacterRange._(string, start, end);
   }
 
@@ -864,16 +909,16 @@
   }
 
   bool _endsWith(int start, int end, String string) {
-    int length = string.length;
-    int stringStart = end - length;
+    var length = string.length;
+    var stringStart = end - length;
     return stringStart >= start &&
         _string.startsWith(string, stringStart) &&
         isGraphemeClusterBoundary(_string, start, end, stringStart);
   }
 
   bool _startsWith(int start, int end, String string) {
-    int length = string.length;
-    int stringEnd = start + length;
+    var length = string.length;
+    var stringEnd = start + length;
     return stringEnd <= end &&
         _string.startsWith(string, start) &&
         isGraphemeClusterBoundary(_string, start, end, stringEnd);
@@ -882,7 +927,7 @@
   @override
   bool moveBackTo(Characters target) {
     var targetString = target.string;
-    int index = _lastIndexOf(_string, targetString, 0, _start);
+    var index = _lastIndexOf(_string, targetString, 0, _start);
     if (index >= 0) {
       _move(index, index + targetString.length);
       return true;
@@ -893,7 +938,7 @@
   @override
   bool moveTo(Characters target) {
     var targetString = target.string;
-    int index = _indexOf(_string, targetString, _end, _string.length);
+    var index = _indexOf(_string, targetString, _end, _string.length);
     if (index >= 0) {
       _move(index, index + targetString.length);
       return true;
@@ -925,9 +970,15 @@
   String get stringAfter => _string.substring(_end);
 
   @override
+  int get stringAfterLength => _string.length - _end;
+
+  @override
   String get stringBefore => _string.substring(0, _start);
 
   @override
+  int get stringBeforeLength => _start;
+
+  @override
   Iterable<CharacterRange> split(Characters pattern, [int maxParts = 0]) sync* {
     if (maxParts == 1 || _start == _end) {
       yield this;
@@ -968,8 +1019,8 @@
   }
   var buffer = StringBuffer(string.substring(0, start));
   var breaks = Breaks(string, start, end, stateSoTNoBreak);
-  int index = 0;
-  String replacement = outerReplacement;
+  var index = 0;
+  var replacement = outerReplacement;
   while ((index = breaks.nextBreak()) >= 0) {
     buffer..write(replacement)..write(string.substring(start, index));
     start = index;
@@ -984,16 +1035,16 @@
 /// Both [start] and [end] are grapheme cluster boundaries in the
 /// [source] string.
 int _indexOf(String source, String pattern, int start, int end) {
-  int patternLength = pattern.length;
+  var patternLength = pattern.length;
   if (patternLength == 0) return start;
   // Any start position after realEnd won't fit the pattern before end.
-  int realEnd = end - patternLength;
+  var realEnd = end - patternLength;
   if (realEnd < start) return -1;
   // Use indexOf if what we can overshoot is
   // less than twice as much as what we have left to search.
-  int rest = source.length - realEnd;
+  var rest = source.length - realEnd;
   if (rest <= (realEnd - start) * 2) {
-    int index = 0;
+    var index = 0;
     while (start < realEnd && (index = source.indexOf(pattern, start)) >= 0) {
       if (index > realEnd) return -1;
       if (isGraphemeClusterBoundary(source, start, end, index) &&
@@ -1010,9 +1061,9 @@
 
 int _gcIndexOf(String source, String pattern, int start, int end) {
   var breaks = Breaks(source, start, end, stateSoT);
-  int index = 0;
+  var index = 0;
   while ((index = breaks.nextBreak()) >= 0) {
-    int endIndex = index + pattern.length;
+    var endIndex = index + pattern.length;
     if (endIndex > end) break;
     if (source.startsWith(pattern, index) &&
         isGraphemeClusterBoundary(source, start, end, endIndex)) {
@@ -1026,15 +1077,15 @@
 /// Both [start] and [end] are grapheme cluster boundaries in the
 /// [source] string.
 int _lastIndexOf(String source, String pattern, int start, int end) {
-  int patternLength = pattern.length;
+  var patternLength = pattern.length;
   if (patternLength == 0) return end;
   // Start of pattern must be in range [start .. end - patternLength].
-  int realEnd = end - patternLength;
+  var realEnd = end - patternLength;
   if (realEnd < start) return -1;
   // If the range from 0 to start is no more than double the range from
   // start to end, use lastIndexOf.
   if (realEnd * 2 > start) {
-    int index = 0;
+    var index = 0;
     while (realEnd >= start &&
         (index = source.lastIndexOf(pattern, realEnd)) >= 0) {
       if (index < start) return -1;
@@ -1052,9 +1103,9 @@
 
 int _gcLastIndexOf(String source, String pattern, int start, int end) {
   var breaks = BackBreaks(source, end, start, stateEoT);
-  int index = 0;
+  var index = 0;
   while ((index = breaks.nextBreak()) >= 0) {
-    int startIndex = index - pattern.length;
+    var startIndex = index - pattern.length;
     if (startIndex < start) break;
     if (source.startsWith(pattern, startIndex) &&
         isGraphemeClusterBoundary(source, start, end, startIndex)) {
diff --git a/lib/src/grapheme_clusters/breaks.dart b/lib/src/grapheme_clusters/breaks.dart
index 08d1d84..71ed02e 100644
--- a/lib/src/grapheme_clusters/breaks.dart
+++ b/lib/src/grapheme_clusters/breaks.dart
@@ -45,8 +45,8 @@
   /// which means that [cursor] has reached [end].
   int nextBreak() {
     while (cursor < end) {
-      int breakAt = cursor;
-      int char = base.codeUnitAt(cursor++);
+      var breakAt = cursor;
+      var char = base.codeUnitAt(cursor++);
       if (char & 0xFC00 != 0xD800) {
         state = move(state, low(char));
         if (state & stateNoBreak == 0) {
@@ -55,9 +55,9 @@
         continue;
       }
       // The category of an unpaired lead surrogate is Control.
-      int category = categoryControl;
+      var category = categoryControl;
       if (cursor < end) {
-        int nextChar = base.codeUnitAt(cursor);
+        var nextChar = base.codeUnitAt(cursor);
         if (nextChar & 0xFC00 == 0xDC00) {
           category = high(char, nextChar);
           cursor++;
@@ -112,8 +112,8 @@
   /// which means that [cursor] has reached [start].
   int nextBreak() {
     while (cursor > start) {
-      int breakAt = cursor;
-      int char = base.codeUnitAt(--cursor);
+      var breakAt = cursor;
+      var char = base.codeUnitAt(--cursor);
       if (char & 0xFC00 != 0xDC00) {
         state = moveBack(state, low(char));
         if (state >= stateLookaheadMin) state = _lookAhead(state);
@@ -123,9 +123,9 @@
         continue;
       }
       // The category of an unpaired tail surrogate is Control.
-      int category = categoryControl;
+      var category = categoryControl;
       if (cursor >= start) {
-        int prevChar = base.codeUnitAt(cursor - 1);
+        var prevChar = base.codeUnitAt(cursor - 1);
         if (prevChar & 0xFC00 == 0xD800) {
           category = high(prevChar, char);
           cursor -= 1;
@@ -158,7 +158,7 @@
     return lookAheadRegional(base, start, cursor);
   }
   if (state == stateZWJPictographicLookahead) {
-    int prevPic = lookAheadPictorgraphicExtend(base, start, cursor);
+    var prevPic = lookAheadPictorgraphicExtend(base, start, cursor);
     if (prevPic >= 0) return stateZWJPictographic | stateNoBreak;
     return stateExtend; // State for break before seeing ZWJ.
   }
@@ -179,14 +179,14 @@
   // Has just seen second regional indicator.
   // Figure out if there are an odd or even number of preceding RIs.
   // ALL REGIONAL INDICATORS ARE NON-BMP CHARACTERS.
-  int count = 0;
-  int index = cursor;
+  var count = 0;
+  var index = cursor;
   while (index - 2 >= start) {
-    int tail = base.codeUnitAt(index - 1);
+    var tail = base.codeUnitAt(index - 1);
     if (tail & 0xFC00 != 0xDC00) break;
-    int lead = base.codeUnitAt(index - 2);
+    var lead = base.codeUnitAt(index - 2);
     if (lead & 0xFC00 != 0xD800) break;
-    int category = high(lead, tail);
+    var category = high(lead, tail);
     if (category != categoryRegionalIndicator) break;
     index -= 2;
     count ^= 1;
@@ -208,11 +208,11 @@
 int lookAheadPictorgraphicExtend(String base, int start, int cursor) {
   // Has just seen ZWJ+Pictographic. Check if preceeding is Pic Ext*.
   // (If so, just move cursor back to the Pic).
-  int index = cursor;
+  var index = cursor;
   while (index > start) {
-    int char = base.codeUnitAt(--index);
-    int prevChar = 0;
-    int category = categoryControl;
+    var char = base.codeUnitAt(--index);
+    var prevChar = 0;
+    var category = categoryControl;
     if (char & 0xFC00 != 0xDC00) {
       category = low(char);
     } else if (index > start &&
@@ -247,16 +247,16 @@
   // surrogates.
   if (start < index && index < end) {
     // Something on both sides of index.
-    int char = text.codeUnitAt(index);
-    int prevChar = text.codeUnitAt(index - 1);
-    int catAfter = categoryControl;
+    var char = text.codeUnitAt(index);
+    var prevChar = text.codeUnitAt(index - 1);
+    var catAfter = categoryControl;
     if (char & 0xF800 != 0xD800) {
       catAfter = low(char);
     } else if (char & 0xFC00 == 0xD800) {
       // Lead surrogate. Combine with following tail surrogate,
       // otherwise it's a control and always a boundary.
       if (index + 1 >= end) return true;
-      int nextChar = text.codeUnitAt(index + 1);
+      var nextChar = text.codeUnitAt(index + 1);
       if (nextChar & 0xFC00 != 0xDC00) return true;
       catAfter = high(char, nextChar);
     } else {
@@ -264,7 +264,7 @@
       // before or is always a bundary.
       return prevChar & 0xFC00 != 0xD800;
     }
-    int catBefore = categoryControl;
+    var catBefore = categoryControl;
     if (prevChar & 0xFC00 != 0xDC00) {
       catBefore = low(prevChar);
       index -= 1;
@@ -272,7 +272,7 @@
       // If no prior lead surrogate, it's a control and always a boundary.
       index -= 2;
       if (start <= index) {
-        int prevPrevChar = text.codeUnitAt(index);
+        var prevPrevChar = text.codeUnitAt(index);
         if (prevPrevChar & 0xFC00 != 0xD800) {
           return true;
         }
@@ -303,21 +303,21 @@
   assert(index <= end);
   assert(end <= text.length);
   if (index == start || index == end) return index;
-  int indexBefore = index;
-  int nextChar = text.codeUnitAt(index);
-  int category = categoryControl;
+  var indexBefore = index;
+  var nextChar = text.codeUnitAt(index);
+  var category = categoryControl;
   if (nextChar & 0xF800 != 0xD800) {
     category = low(nextChar);
   } else if (nextChar & 0xFC00 == 0xD800) {
-    int indexAfter = index + 1;
+    var indexAfter = index + 1;
     if (indexAfter < end) {
-      int secondChar = text.codeUnitAt(indexAfter);
+      var secondChar = text.codeUnitAt(indexAfter);
       if (secondChar & 0xFC00 == 0xDC00) {
         category = high(nextChar, secondChar);
       }
     }
   } else {
-    int prevChar = text.codeUnitAt(index - 1);
+    var prevChar = text.codeUnitAt(index - 1);
     if (prevChar & 0xFC00 == 0xD800) {
       category = high(prevChar, nextChar);
       indexBefore -= 1;
@@ -337,21 +337,21 @@
   assert(index <= end);
   assert(end <= text.length);
   if (index == start || index == end) return index;
-  int indexBefore = index - 1;
-  int prevChar = text.codeUnitAt(indexBefore);
-  int prevCategory = categoryControl;
+  var indexBefore = index - 1;
+  var prevChar = text.codeUnitAt(indexBefore);
+  var prevCategory = categoryControl;
   if (prevChar & 0xF800 != 0xD800) {
     prevCategory = low(prevChar);
   } else if (prevChar & 0xFC00 == 0xD800) {
-    int nextChar = text.codeUnitAt(index);
+    var nextChar = text.codeUnitAt(index);
     if (nextChar & 0xFC00 == 0xDC00) {
       index += 1;
       if (index == end) return end;
       prevCategory = high(prevChar, nextChar);
     }
   } else if (indexBefore > start) {
-    int secondCharIndex = indexBefore - 1;
-    int secondChar = text.codeUnitAt(secondCharIndex);
+    var secondCharIndex = indexBefore - 1;
+    var secondChar = text.codeUnitAt(secondCharIndex);
     if (secondChar & 0xFC00 == 0xD800) {
       indexBefore = secondCharIndex;
       prevCategory = high(secondChar, prevChar);
@@ -361,14 +361,14 @@
   // the previous character are the [^RI] (RI RI)* RI x RI and
   // Pic Ext* ZWJ x Pic breaks. In all other cases, all the necessary
   // information is in the last seen category.
-  int state = stateOther;
+  var state = stateOther;
   if (prevCategory == categoryRegionalIndicator) {
-    int prevState = lookAheadRegional(text, start, indexBefore);
+    var prevState = lookAheadRegional(text, start, indexBefore);
     if (prevState != stateRegionalOdd) {
       state = stateRegionalSingle;
     }
   } else if (prevCategory == categoryZWJ || prevCategory == categoryExtend) {
-    int prevPic = lookAheadPictorgraphicExtend(text, start, indexBefore);
+    var prevPic = lookAheadPictorgraphicExtend(text, start, indexBefore);
     if (prevPic >= 0) {
       state = prevCategory == categoryZWJ
           ? statePictographicZWJ
diff --git a/lib/src/grapheme_clusters/table.dart b/lib/src/grapheme_clusters/table.dart
index b42a66c..c9d3821 100644
--- a/lib/src/grapheme_clusters/table.dart
+++ b/lib/src/grapheme_clusters/table.dart
@@ -317,18 +317,18 @@
     '\u102b\u102b\u102b\u102b\u102b\u102b\u102b\u102b\u102b\u102b\u102b\u102b'
     '\u102b\u102b\u102b\u102b\u102b\u102b\u102b\u102b\u102b\u102b';
 int low(int codeUnit) {
-  int chunkStart = _start.codeUnitAt(codeUnit >> 6);
-  int index = chunkStart + (codeUnit & 63);
-  int bit = index & 1;
-  int pair = _data.codeUnitAt(index >> 1);
+  var chunkStart = _start.codeUnitAt(codeUnit >> 6);
+  var index = chunkStart + (codeUnit & 63);
+  var bit = index & 1;
+  var pair = _data.codeUnitAt(index >> 1);
   return (pair >> 4) & -bit | (pair & 0xF) & (bit - 1);
 }
 
 int high(int lead, int tail) {
-  int chunkStart = _start.codeUnitAt(1024 + (0x3ff & lead));
-  int index = chunkStart + (0x3ff & tail);
-  int bit = index & 1;
-  int pair = _data.codeUnitAt(index >> 1);
+  var chunkStart = _start.codeUnitAt(1024 + (0x3ff & lead));
+  var index = chunkStart + (0x3ff & tail);
+  var bit = index & 1;
+  var pair = _data.codeUnitAt(index >> 1);
   return (pair >> 4) & -bit | (pair & 0xF) & (bit - 1);
 }
 
diff --git a/pubspec.yaml b/pubspec.yaml
index b560a25..e4fbb25 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,11 +1,11 @@
 name: characters
-version: 1.1.0-nullsafety.2
+version: 1.1.0-nullsafety.3
 description: String replacement with operations that are Unicode/grapheme cluster aware.
 homepage: https://www.github.com/dart-lang/characters
 
 environment:
   # This must remain a tight constraint until nnbd is stable
-  sdk: '>=2.10.0-0 <2.10.0'
+  sdk: '>=2.10.0-110 <2.10.0'
 
 dev_dependencies:
   test: "^1.6.0"
diff --git a/test/sound_tests/characters_test.dart b/test/sound_tests/characters_test.dart
index 5fe41fe..2834a46 100644
--- a/test/sound_tests/characters_test.dart
+++ b/test/sound_tests/characters_test.dart
@@ -34,6 +34,9 @@
       expect(cs.skipLast(2).toString(), "Hi ");
       expect(cs.take(2).toString(), "Hi");
       expect(cs.takeLast(2).toString(), "$flag!");
+      expect(cs.getRange(1, 4).toString(), "i $flag");
+      expect(cs.characterAt(1).toString(), "i");
+      expect(cs.characterAt(3).toString(), flag);
 
       expect(cs.contains("\u{1F1E9}"), false);
       expect(cs.contains(flag), true);
@@ -67,6 +70,73 @@
     testParts(gc("$flag$white$zwj$rainbow"), gc("$flag$white"), gc("$rainbow"),
         gc("$flag$zwj$rainbow"), gc("!"));
   });
+
+  group("CharacterRange", () {
+    test("new", () {
+      var range = CharacterRange("abc");
+      expect(range.isEmpty, true);
+      expect(range.moveNext(), true);
+      expect(range.current, "a");
+    });
+    group("new.at", () {
+      test("simple", () {
+        var range = CharacterRange.at("abc", 0);
+        expect(range.isEmpty, true);
+        expect(range.moveNext(), true);
+        expect(range.current, "a");
+
+        range = CharacterRange.at("abc", 1);
+        expect(range.isEmpty, true);
+        expect(range.moveNext(), true);
+        expect(range.current, "b");
+
+        range = CharacterRange.at("abc", 1, 2);
+        expect(range.isEmpty, false);
+        expect(range.current, "b");
+        expect(range.moveNext(), true);
+
+        range = CharacterRange.at("abc", 0, 3);
+        expect(range.isEmpty, false);
+        expect(range.current, "abc");
+        expect(range.moveNext(), false);
+      });
+      test("complicated", () {
+        // 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
+
+        var rbflag = "$flag$white$zwj$rainbow";
+        var string = "-$rbflag-";
+        var range = CharacterRange.at(string, 1);
+        expect(range.isEmpty, true);
+        expect(range.moveNext(), true);
+        expect(range.current, rbflag);
+
+        range = range = CharacterRange.at(string, 2);
+        expect(range.isEmpty, false);
+        expect(range.current, rbflag);
+
+        range = range = CharacterRange.at(string, 0, 2);
+        expect(range.isEmpty, false);
+        expect(range.current, "-$rbflag");
+
+        range = range = CharacterRange.at(string, 0, 2);
+        expect(range.isEmpty, false);
+        expect(range.current, "-$rbflag");
+
+        range = range = CharacterRange.at(string, 2, "-$rbflag".length - 1);
+        expect(range.isEmpty, false);
+        expect(range.current, rbflag);
+        expect(range.stringBeforeLength, 1);
+
+        range = range = CharacterRange.at(string, 0, string.length);
+        expect(range.isEmpty, false);
+        expect(range.current, string);
+      });
+    });
+  });
 }
 
 void tests() {
@@ -191,13 +261,14 @@
   for (var char in expected) {
     expect(actual.contains(char), true);
   }
-  for (int i = 1; i < expected.length; i++) {
+  for (var 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());
+  expect(actual.getRange(1, 2).toString(), expected.take(2).skip(1).join());
 
   if (expected.isNotEmpty) {
     expect(actual.skipLast(1).toList(),
@@ -238,6 +309,8 @@
 
     expect(actual.elementAt(i), expected[i]);
     expect(actual.skip(i).first, expected[i]);
+    expect(actual.characterAt(i).toString(), expected[i]);
+    expect(actual.getRange(i, i + 1).toString(), expected[i]);
   }
   expect(it.moveNext(), false);
   for (var i = expected.length - 1; i >= 0; i--) {
@@ -256,13 +329,13 @@
   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;
+    var steps = min(5, expected.length);
+    for (var s = 0; s <= steps; s++) {
+      var 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;
+      for (var t = s + 1; t <= steps; t++) {
+        var j = expected.length * t ~/ steps;
         var slice = expected.sublist(i, j).join();
         var gcs = gc(slice);
         expect(actual.containsAll(gcs), true);
@@ -273,15 +346,15 @@
   {
     // Random walk back and forth.
     var it = actual.iterator;
-    int pos = -1;
+    var pos = -1;
     if (random.nextBool()) {
       pos = expected.length;
       it = actual.iteratorAtEnd;
     }
-    int steps = 5 + random.nextInt(expected.length * 2 + 1);
-    bool lastMove = false;
+    var steps = 5 + random.nextInt(expected.length * 2 + 1);
+    var lastMove = false;
     while (true) {
-      bool back = false;
+      var back = false;
       if (pos < 0) {
         expect(lastMove, false);
         expect(it.isEmpty, true);
@@ -534,7 +607,7 @@
     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;
+    var first = true;
     for (var range in it.split(a)) {
       if (range.isEmpty) {
         // First range is empty.