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.