blob: 2369eb9d8a7fba07de822af14ed66befae09e3c2 [file] [log] [blame]
// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
part of "characters.dart";
/// The grapheme clusters of a string.
class _Characters extends Iterable<String> implements Characters {
// Try to avoid allocating more empty grapheme clusters.
static const Characters _empty = const _Characters._("");
final String string;
const _Characters._(this.string);
factory _Characters(String string) =>
string.isEmpty ? _empty : _Characters._(string);
Character get iterator => _Character(string);
String get first => string.isEmpty
? throw StateError("No element")
: string.substring(
0, Breaks(string, 0, string.length, stateSoTNoBreak).nextBreak());
String get last => string.isEmpty
? throw StateError("No element")
: string.substring(
BackBreaks(string, string.length, 0, stateEoTNoBreak).nextBreak());
String get single {
if (string.isEmpty) throw StateError("No element");
int firstEnd =
Breaks(string, 0, string.length, stateSoTNoBreak).nextBreak();
if (firstEnd == string.length) return string;
throw StateError("Too many elements");
}
bool get isEmpty => string.isEmpty;
bool get isNotEmpty => string.isNotEmpty;
int get length {
if (string.isEmpty) return 0;
var brk = Breaks(string, 0, string.length, stateSoTNoBreak);
int length = 0;
while (brk.nextBreak() >= 0) length++;
return length;
}
Iterable<T> whereType<T>() {
Iterable<Object> self = this;
if (self is Iterable<T>) {
return self.map<T>((x) => x);
}
return Iterable<T>.empty();
}
String join([String separator = ""]) {
if (separator == "") return string;
return _explodeReplace(separator, "", 0);
}
String lastWhere(bool test(String element), {String orElse()}) {
int cursor = string.length;
var brk = BackBreaks(string, cursor, 0, stateEoTNoBreak);
int next = 0;
while ((next = brk.nextBreak()) >= 0) {
String current = string.substring(next, cursor);
if (test(current)) return current;
cursor = next;
}
if (orElse != null) return orElse();
throw StateError("no element");
}
String elementAt(int index) {
RangeError.checkNotNegative(index, "index");
int count = 0;
if (string.isNotEmpty) {
var breaks = Breaks(string, 0, string.length, stateSoTNoBreak);
int start = 0;
int end = 0;
while ((end = breaks.nextBreak()) >= 0) {
if (count == index) return string.substring(start, end);
count++;
start = end;
}
}
throw RangeError.index(index, this, "index", null, count);
}
bool contains(Object other) {
if (other is String) {
if (other.isEmpty) return false;
int next = Breaks(other, 0, other.length, stateSoTNoBreak).nextBreak();
if (next != other.length) return false;
// [other] is single grapheme cluster.
return _indexOf(other, 0) >= 0;
}
return false;
}
int indexOf(Characters other, [int startIndex]) {
int length = string.length;
if (startIndex == null) {
startIndex = 0;
} else {
RangeError.checkValidRange(startIndex, length, length, "startIndex");
}
return _indexOf(other.string, startIndex);
}
/// Finds first occurrence of [otherString] at grapheme cluster boundaries.
///
/// Only finds occurrences starting at or after [startIndex].
int _indexOf(String otherString, int startIndex) {
int otherLength = otherString.length;
if (otherLength == 0) {
return nextBreak(string, 0, string.length, startIndex);
}
int length = string.length;
while (startIndex + otherLength <= length) {
int matchIndex = string.indexOf(otherString, startIndex);
if (matchIndex < 0) return matchIndex;
if (isGraphemeClusterBoundary(string, 0, length, matchIndex) &&
isGraphemeClusterBoundary(
string, 0, length, matchIndex + otherLength)) {
return matchIndex;
}
startIndex = matchIndex + 1;
}
return -1;
}
/// Finds last occurrence of [otherString] at grapheme cluster boundaries.
///
/// Starts searching backwards at [startIndex].
int _lastIndexOf(String otherString, int startIndex) {
int otherLength = otherString.length;
if (otherLength == 0) {
return previousBreak(string, 0, string.length, startIndex);
}
int length = string.length;
while (startIndex >= 0) {
int matchIndex = string.lastIndexOf(otherString, startIndex);
if (matchIndex < 0) return matchIndex;
if (isGraphemeClusterBoundary(string, 0, length, matchIndex) &&
isGraphemeClusterBoundary(
string, 0, length, matchIndex + otherLength)) {
return matchIndex;
}
startIndex = matchIndex - 1;
}
return -1;
}
bool startsWith(Characters other, [int startIndex = 0]) {
int length = string.length;
RangeError.checkValueInInterval(startIndex, 0, length, "startIndex");
String otherString = other.string;
if (otherString.isEmpty) return true;
return string.startsWith(otherString, startIndex) &&
isGraphemeClusterBoundary(
string, 0, length, startIndex + otherString.length);
}
bool endsWith(Characters other, [int endIndex]) {
int length = string.length;
if (endIndex == null) {
endIndex = length;
} else {
RangeError.checkValueInInterval(endIndex, 0, length, "endIndex");
}
String otherString = other.string;
if (otherString.isEmpty) return true;
int otherLength = otherString.length;
int start = endIndex - otherLength;
return start >= 0 &&
string.startsWith(otherString, start) &&
isGraphemeClusterBoundary(string, 0, endIndex, start);
}
Characters replaceAll(Characters pattern, Characters replacement,
[int startIndex = 0]) {
if (startIndex > 0) {
RangeError.checkValueInInterval(
startIndex, 0, string.length, "startIndex");
}
if (pattern.string.isEmpty) {
if (string.isEmpty) return replacement;
var replacementString = replacement.string;
return Characters(
_explodeReplace(replacementString, replacementString, startIndex));
}
int start = startIndex;
StringBuffer buffer;
int next = -1;
while ((next = this.indexOf(pattern, start)) >= 0) {
(buffer ??= StringBuffer())
..write(string.substring(start, next))
..write(replacement);
start = next + pattern.string.length;
}
if (buffer == null) return this;
buffer.write(string.substring(start));
return Characters(buffer.toString());
}
// Replaces every internal grapheme cluster boundary with
// [internalReplacement] and adds [outerReplacement] at both ends
// Starts at [startIndex].
String _explodeReplace(
String internalReplacement, String outerReplacement, int startIndex) {
var buffer = StringBuffer(string.substring(0, startIndex));
var breaks = Breaks(string, startIndex, string.length, stateSoTNoBreak);
int index = 0;
String replacement = outerReplacement;
while ((index = breaks.nextBreak()) >= 0) {
buffer..write(replacement)..write(string.substring(startIndex, index));
startIndex = index;
replacement = internalReplacement;
}
buffer.write(outerReplacement);
return buffer.toString();
}
Characters replaceFirst(Characters source, Characters replacement,
[int startIndex = 0]) {
if (startIndex != 0) {
RangeError.checkValueInInterval(
startIndex, 0, string.length, "startIndex");
}
int index = _indexOf(source.string, startIndex);
if (index < 0) return this;
return Characters(string.replaceRange(
index, index + source.string.length, replacement.string));
}
bool containsAll(Characters other) {
return _indexOf(other.string, 0) >= 0;
}
Characters skip(int count) {
RangeError.checkNotNegative(count, "count");
if (count == 0) return this;
if (string.isNotEmpty) {
var breaks = Breaks(string, 0, string.length, stateSoTNoBreak);
int startIndex = 0;
while (count > 0) {
int index = breaks.nextBreak();
if (index >= 0) {
count--;
startIndex = index;
} else {
return _empty;
}
}
return _Characters(string.substring(startIndex));
}
return this;
}
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) {
count--;
endIndex = index;
} else {
return this;
}
}
return _Characters._(string.substring(0, endIndex));
}
return this;
}
Characters skipWhile(bool Function(String) test) {
if (string.isNotEmpty) {
var breaks = Breaks(string, 0, string.length, stateSoTNoBreak);
int index = 0;
int startIndex = 0;
while ((index = breaks.nextBreak()) >= 0) {
if (!test(string.substring(startIndex, index))) {
if (startIndex == 0) return this;
return _Characters._(string.substring(startIndex));
}
startIndex = index;
}
}
return _empty;
}
Characters takeWhile(bool Function(String) test) {
if (string.isNotEmpty) {
var breaks = Breaks(string, 0, string.length, stateSoTNoBreak);
int index = 0;
int endIndex = 0;
while ((index = breaks.nextBreak()) >= 0) {
if (!test(string.substring(endIndex, index))) {
if (endIndex == 0) return _empty;
return _Characters._(string.substring(0, endIndex));
}
endIndex = index;
}
}
return this;
}
Characters where(bool Function(String) test) =>
_Characters(super.where(test).join());
Characters operator +(Characters other) => _Characters(string + other.string);
Characters getRange(int start, int end) {
RangeError.checkNotNegative(start, "start");
if (end < start) throw RangeError.range(end, start, null, "end");
if (string.isEmpty) return this;
var breaks = Breaks(string, 0, string.length, stateSoTNoBreak);
int startIndex = 0;
int endIndex = string.length;
end -= start;
while (start > 0) {
int index = breaks.nextBreak();
if (index >= 0) {
startIndex = index;
start--;
} else {
return _empty;
}
}
while (end > 0) {
int index = breaks.nextBreak();
if (index >= 0) {
endIndex = index;
end--;
} else {
if (startIndex == 0) return this;
return _Characters(string.substring(startIndex));
}
}
if (startIndex == 0 && endIndex == string.length) return this;
return _Characters(string.substring(startIndex, endIndex));
}
Characters skipLast(int count) {
RangeError.checkNotNegative(count, "count");
if (count == 0) return this;
if (string.isNotEmpty) {
var breaks = BackBreaks(string, string.length, 0, stateEoTNoBreak);
int endIndex = string.length;
while (count > 0) {
int index = breaks.nextBreak();
if (index >= 0) {
endIndex = index;
count--;
} else {
return _empty;
}
}
return _Characters(string.substring(0, endIndex));
}
return _empty;
}
Characters skipLastWhile(bool Function(String) test) {
if (string.isNotEmpty) {
var breaks = BackBreaks(string, string.length, 0, stateEoTNoBreak);
int index = 0;
int end = string.length;
while ((index = breaks.nextBreak()) >= 0) {
if (!test(string.substring(index, end))) {
if (end == string.length) return this;
return _Characters(string.substring(0, end));
}
end = index;
}
}
return _empty;
}
Characters takeLast(int count) {
RangeError.checkNotNegative(count, "count");
if (count == 0) return this;
if (string.isNotEmpty) {
var breaks = BackBreaks(string, string.length, 0, stateEoTNoBreak);
int startIndex = string.length;
while (count > 0) {
int index = breaks.nextBreak();
if (index >= 0) {
startIndex = index;
count--;
} else {
return this;
}
}
return _Characters(string.substring(startIndex));
}
return this;
}
Characters takeLastWhile(bool Function(String) test) {
if (string.isNotEmpty) {
var breaks = BackBreaks(string, string.length, 0, stateEoTNoBreak);
int index = 0;
int start = string.length;
while ((index = breaks.nextBreak()) >= 0) {
if (!test(string.substring(index, start))) {
return _Characters(string.substring(start));
}
start = index;
}
}
return this;
}
int indexAfter(Characters other, [int startIndex]) {
int length = string.length;
String otherString = other.string;
int otherLength = otherString.length;
if (startIndex == null) {
startIndex = 0;
} else {
RangeError.checkValueInInterval(startIndex, 0, length, "startIndex");
}
if (otherLength > startIndex) startIndex = otherLength;
int start = _indexOf(other.string, startIndex - otherLength);
if (start < 0) return start;
return start + otherLength;
}
Characters insertAt(int index, Characters other) {
int length = string.length;
RangeError.checkValidRange(index, length, length, "index");
if (string.isEmpty) {
assert(index == 0);
return other;
}
return _Characters._(string.replaceRange(index, index, other.string));
}
int lastIndexAfter(Characters other, [int startIndex]) {
String otherString = other.string;
int otherLength = otherString.length;
if (startIndex == null) {
startIndex = string.length;
} else {
RangeError.checkValueInInterval(
startIndex, 0, string.length, "startIndex");
}
if (otherLength > startIndex) return -1;
int start = _lastIndexOf(otherString, startIndex - otherLength);
if (start < 0) return start;
return start + otherLength;
}
int lastIndexOf(Characters other, [int startIndex]) {
if (startIndex == null) {
startIndex = string.length;
} else {
RangeError.checkValueInInterval(
startIndex, 0, string.length, "startIndex");
}
return _lastIndexOf(other.string, startIndex);
}
Characters replaceSubstring(int startIndex, int endIndex, Characters other) {
RangeError.checkValidRange(
startIndex, endIndex, string.length, "startIndex", "endIndex");
if (startIndex == 0 && endIndex == string.length) return other;
return _Characters._(
string.replaceRange(startIndex, endIndex, other.string));
}
Characters substring(int startIndex, [int endIndex]) {
endIndex = RangeError.checkValidRange(
startIndex, endIndex, string.length, "startIndex", "endIndex");
return _Characters(string.substring(startIndex, endIndex));
}
Characters toLowerCase() => _Characters(string.toLowerCase());
Characters toUpperCase() => _Characters(string.toUpperCase());
bool operator ==(Object other) =>
other is Characters && string == other.string;
int get hashCode => string.hashCode;
String toString() => string;
}
class _Character implements Character {
static const int _directionForward = 0;
static const int _directionBackward = 0x04;
static const int _directionMask = 0x04;
static const int _cursorDeltaMask = 0x03;
final String _string;
int _start;
int _end;
// Encodes current state,
// whether we are moving forwards or backwards ([_directionMask]),
// and how far ahead the cursor is from the start/end ([_cursorDeltaMask]).
int _state;
// The [current] value is created lazily and cached to avoid repeated
// or unnecessary string allocation.
String _currentCache;
_Character(String string) : this._(string, 0, 0, stateSoTNoBreak);
_Character._(this._string, this._start, this._end, this._state);
int get start => _start;
int get end => _end;
String get current => _currentCache ??=
(_start == _end ? null : _string.substring(_start, _end));
bool moveNext() {
int state = _state;
int cursor = _end;
if (state & _directionMask != _directionForward) {
state = stateSoTNoBreak;
} else {
cursor += state & _cursorDeltaMask;
}
var breaks = Breaks(_string, cursor, _string.length, state);
var next = breaks.nextBreak();
_currentCache = null;
_start = _end;
if (next >= 0) {
_end = next;
_state =
(breaks.state & 0xF0) | _directionForward | (breaks.cursor - next);
return true;
}
_state = stateEoTNoBreak | _directionBackward;
return false;
}
bool movePrevious() {
int state = _state;
int cursor = _start;
if (state & _directionMask == _directionForward) {
state = stateEoTNoBreak;
} else {
cursor -= state & _cursorDeltaMask;
}
var breaks = BackBreaks(_string, cursor, 0, state);
var next = breaks.nextBreak();
_currentCache = null;
_end = _start;
if (next >= 0) {
_start = next;
_state =
(breaks.state & 0xF0) | _directionBackward | (next - breaks.cursor);
return true;
}
_state = stateSoTNoBreak | _directionForward;
return false;
}
List<int> get codeUnits => _CodeUnits(_string, _start, _end);
Runes get runes => Runes(current);
void reset(int index) {
RangeError.checkValueInInterval(index, 0, _string.length, "index");
_reset(index);
}
void resetStart() {
_reset(0);
}
void resetEnd() {
_state = stateEoTNoBreak | _directionBackward;
_currentCache = null;
_start = _end = _string.length;
}
void _reset(int index) {
_state = stateSoTNoBreak | _directionForward;
_currentCache = null;
_start = _end = index;
}
Character copy() {
return _Character._(_string, _start, _end, _state);
}
}
class _CodeUnits extends ListBase<int> {
final String _string;
final int _start;
final int _end;
_CodeUnits(this._string, this._start, this._end);
int get length => _end - _start;
int operator [](int index) {
RangeError.checkValidIndex(index, this, "index", _end - _start);
return _string.codeUnitAt(_start + index);
}
void operator []=(int index, int value) {
throw UnsupportedError("Cannot modify an unmodifiable list");
}
@override
void set length(int newLength) {
throw UnsupportedError("Cannot modify an unmodifiable list");
}
}