String.fromCharCodes allows start and end after end of iterable.
The current `String.fromCharCodes` behavior, throwing if `start` or `end` is larger than the length of the `charCodes` iterable, is inconsistent with the argument being an `Iterable<int>`, which the user is not expected to know the length of. Most other operations that accepts or produces an `Iterable` and restricts it to a range, will allow the range to exceed the length of the iterable, acting like `.take(end).skip(start)`, just without needing to create wrappers that hide the original value. (`List.setRange` is another exception, and should probably be fixed by allowing the range to be partially filled, since it's too hard to change it to require a `List` argument.) Fixes #50253, #53937 Tested: Added to `corelib/string_fromcharcodes_test.dart` Bug: https://dartbug.com/53937, https://dartbug.com/50253, https://dartbug.com/23282 Change-Id: Ie19c5fa8e715ea1c58c9c77c247f2a563654c1aa Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/333921 Commit-Queue: Lasse Nielsen <lrn@google.com> Reviewed-by: Nate Bosch <nbosch@google.com> Reviewed-by: Stephen Adams <sra@google.com> Reviewed-by: Martin Kustermann <kustermann@google.com> Reviewed-by: Ömer Ağacan <omersa@google.com>
This commit is contained in:
committed by
Commit Queue
parent
5543293a16
commit
a1783a9c34
@@ -2,6 +2,11 @@
|
||||
|
||||
### Libraries
|
||||
|
||||
#### `dart:core`
|
||||
|
||||
- `String.fromCharCodes` now allow `start` and `end` to be after the end of
|
||||
the `Iterable` argument, just like `skip` and `take` does on an `Iterable`.
|
||||
|
||||
#### `dart:nativewrappers`
|
||||
|
||||
- **Breaking Change** [#51896][]: The NativeWrapperClasses are marked `base` so
|
||||
|
||||
@@ -598,6 +598,16 @@ class String {
|
||||
@patch
|
||||
factory String.fromCharCodes(Iterable<int> charCodes,
|
||||
[int start = 0, int? end]) {
|
||||
RangeError.checkNotNegative(start, "start");
|
||||
if (end != null) {
|
||||
var maxLength = end - start;
|
||||
if (maxLength < 0) {
|
||||
throw RangeError.range(end, start, null, "end");
|
||||
}
|
||||
if (maxLength == 0) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
if (charCodes is JSArray<int>) {
|
||||
return _stringFromJSArray(charCodes, start, end);
|
||||
}
|
||||
@@ -622,8 +632,12 @@ class String {
|
||||
static String _stringFromJSArray(
|
||||
JSArray<int> list, int start, int? endOrNull) {
|
||||
int len = list.length;
|
||||
int end = RangeError.checkValidRange(start, endOrNull, len);
|
||||
int end = endOrNull ?? len;
|
||||
if (start > 0 || end < len) {
|
||||
// JS `List.slice` allows positive indices greater than list length,
|
||||
// and `end` before `start` (always empty result).
|
||||
// If `start >= len`, then the result will be empty, whether `endOrNull`
|
||||
// is `null` or not.
|
||||
list = JS('!', '#.slice(#, #)', list, start, end);
|
||||
}
|
||||
return Primitives.stringFromCharCodes(list);
|
||||
@@ -632,34 +646,18 @@ class String {
|
||||
static String _stringFromUint8List(
|
||||
NativeUint8List charCodes, int start, int? endOrNull) {
|
||||
int len = charCodes.length;
|
||||
int end = RangeError.checkValidRange(start, endOrNull, len);
|
||||
if (start >= len) return "";
|
||||
int end = (endOrNull == null || endOrNull > len) ? len : endOrNull;
|
||||
return Primitives.stringFromNativeUint8List(charCodes, start, end);
|
||||
}
|
||||
|
||||
static String _stringFromIterable(
|
||||
Iterable<int> charCodes, int start, int? end) {
|
||||
if (start < 0) throw RangeError.range(start, 0, charCodes.length);
|
||||
if (end != null && end < start) {
|
||||
throw RangeError.range(end, start, charCodes.length);
|
||||
}
|
||||
var it = charCodes.iterator;
|
||||
for (int i = 0; i < start; i++) {
|
||||
if (!it.moveNext()) {
|
||||
throw RangeError.range(start, 0, i);
|
||||
}
|
||||
}
|
||||
var list = JSArray<int>.of(JS('', 'new Array()'));
|
||||
if (end == null) {
|
||||
while (it.moveNext()) list.add(it.current);
|
||||
} else {
|
||||
for (int i = start; i < end; i++) {
|
||||
if (!it.moveNext()) {
|
||||
throw RangeError.range(end, start, i);
|
||||
}
|
||||
list.add(it.current);
|
||||
}
|
||||
}
|
||||
return Primitives.stringFromCharCodes(list);
|
||||
if (end != null) charCodes = charCodes.take(end);
|
||||
if (start > 0) charCodes = charCodes.skip(start);
|
||||
final list = List<int>.of(charCodes);
|
||||
final asJSArray = JS<JSArray<int>>('!', '#', list); // trusted downcast
|
||||
return Primitives.stringFromCharCodes(asJSArray);
|
||||
}
|
||||
|
||||
@JSExportName('is')
|
||||
|
||||
@@ -556,6 +556,16 @@ class String {
|
||||
@patch
|
||||
factory String.fromCharCodes(Iterable<int> charCodes,
|
||||
[int start = 0, int? end]) {
|
||||
RangeError.checkNotNegative(start, "start");
|
||||
if (end != null) {
|
||||
var maxLength = end - start;
|
||||
if (maxLength < 0) {
|
||||
throw RangeError.range(end, start, null, "end");
|
||||
}
|
||||
if (maxLength == 0) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
if (charCodes is JSArray) {
|
||||
// Type promotion doesn't work unless the check is `is JSArray<int>`,
|
||||
// which is more expensive.
|
||||
@@ -575,9 +585,14 @@ class String {
|
||||
}
|
||||
|
||||
static String _stringFromJSArray(JSArray list, int start, int? endOrNull) {
|
||||
// Caller guarantees: endOrNull == null || endOrNull > start
|
||||
int len = list.length;
|
||||
int end = RangeError.checkValidRange(start, endOrNull, len);
|
||||
int end = endOrNull ?? len;
|
||||
if (start > 0 || end < len) {
|
||||
// JS `List.slice` allows positive indices greater than list length,
|
||||
// and end before start (empty result).
|
||||
// If `start >= len`, then the result will be empty, whether `endOrNull`
|
||||
// is `null` or not.
|
||||
list = JS('JSArray', '#.slice(#, #)', list, start, end);
|
||||
}
|
||||
return Primitives.stringFromCharCodes(list);
|
||||
@@ -586,34 +601,16 @@ class String {
|
||||
static String _stringFromUint8List(
|
||||
NativeUint8List charCodes, int start, int? endOrNull) {
|
||||
int len = charCodes.length;
|
||||
int end = RangeError.checkValidRange(start, endOrNull, len);
|
||||
if (start >= len) return "";
|
||||
int end = (endOrNull == null || endOrNull > len) ? len : endOrNull;
|
||||
return Primitives.stringFromNativeUint8List(charCodes, start, end);
|
||||
}
|
||||
|
||||
static String _stringFromIterable(
|
||||
Iterable<int> charCodes, int start, int? end) {
|
||||
if (start < 0) throw new RangeError.range(start, 0, charCodes.length);
|
||||
if (end != null && end < start) {
|
||||
throw new RangeError.range(end, start, charCodes.length);
|
||||
}
|
||||
var it = charCodes.iterator;
|
||||
for (int i = 0; i < start; i++) {
|
||||
if (!it.moveNext()) {
|
||||
throw new RangeError.range(start, 0, i);
|
||||
}
|
||||
}
|
||||
var list = [];
|
||||
if (end == null) {
|
||||
while (it.moveNext()) list.add(it.current);
|
||||
} else {
|
||||
for (int i = start; i < end; i++) {
|
||||
if (!it.moveNext()) {
|
||||
throw new RangeError.range(end, start, i);
|
||||
}
|
||||
list.add(it.current);
|
||||
}
|
||||
}
|
||||
return Primitives.stringFromCharCodes(list);
|
||||
if (end != null) charCodes = charCodes.take(end);
|
||||
if (start > 0) charCodes = charCodes.skip(start);
|
||||
return Primitives.stringFromCharCodes(List<int>.of(charCodes));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -125,28 +125,44 @@ abstract final class _StringBase implements String {
|
||||
*/
|
||||
static String createFromCharCodes(
|
||||
Iterable<int> charCodes, int start, int? end, int? limit) {
|
||||
// Validate start/end first.
|
||||
RangeError.checkNotNegative(start, "start");
|
||||
if (end != null) {
|
||||
var maxLength = end - start;
|
||||
if (maxLength < 0) {
|
||||
throw RangeError.range(end, start, null, "end");
|
||||
}
|
||||
if (maxLength == 0) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
// TODO(srdjan): Also skip copying of wide typed arrays.
|
||||
final ccid = ClassID.getID(charCodes);
|
||||
if ((ccid != ClassID.cidArray) &&
|
||||
(ccid != ClassID.cidGrowableObjectArray) &&
|
||||
(ccid != ClassID.cidImmutableArray)) {
|
||||
if (charCodes is Uint8List) {
|
||||
final actualEnd =
|
||||
RangeError.checkValidRange(start, end, charCodes.length);
|
||||
return _createOneByteString(charCodes, start, actualEnd - start);
|
||||
final int codeCount = charCodes.length;
|
||||
if (codeCount <= start) return "";
|
||||
if (end == null || end > codeCount) {
|
||||
end = codeCount;
|
||||
}
|
||||
return _createOneByteString(charCodes, start, end - start);
|
||||
} else if (charCodes is! Uint16List) {
|
||||
return _createStringFromIterable(charCodes, start, end);
|
||||
}
|
||||
}
|
||||
final int codeCount = charCodes.length;
|
||||
final actualEnd = RangeError.checkValidRange(start, end, codeCount);
|
||||
final len = actualEnd - start;
|
||||
if (codeCount <= start) return "";
|
||||
if (end == null || end > codeCount) {
|
||||
end = codeCount;
|
||||
}
|
||||
final len = end - start;
|
||||
if (len == 0) return "";
|
||||
|
||||
final typedCharCodes = unsafeCast<List<int>>(charCodes);
|
||||
|
||||
final int actualLimit =
|
||||
limit ?? _scanCodeUnits(typedCharCodes, start, actualEnd);
|
||||
final int actualLimit = limit ?? _scanCodeUnits(typedCharCodes, start, end);
|
||||
if (actualLimit < 0) {
|
||||
throw new ArgumentError(typedCharCodes);
|
||||
}
|
||||
@@ -155,12 +171,12 @@ abstract final class _StringBase implements String {
|
||||
}
|
||||
if (actualLimit <= _maxUtf16) {
|
||||
return _TwoByteString._allocateFromTwoByteList(
|
||||
typedCharCodes, start, actualEnd);
|
||||
typedCharCodes, start, end);
|
||||
}
|
||||
// TODO(lrn): Consider passing limit to _createFromCodePoints, because
|
||||
// the function is currently fully generic and doesn't know that its
|
||||
// charCodes are not all Latin-1 or Utf-16.
|
||||
return _createFromCodePoints(typedCharCodes, start, actualEnd);
|
||||
// charCodes are not all Latin-1 or UTF-16.
|
||||
return _createFromCodePoints(typedCharCodes, start, end);
|
||||
}
|
||||
|
||||
static int _scanCodeUnits(List<int> charCodes, int start, int end) {
|
||||
@@ -175,58 +191,57 @@ abstract final class _StringBase implements String {
|
||||
|
||||
static String _createStringFromIterable(
|
||||
Iterable<int> charCodes, int start, int? end) {
|
||||
assert(start >= 0);
|
||||
assert(end == null || start <= end);
|
||||
// Treat charCodes as Iterable.
|
||||
if (charCodes is EfficientLengthIterable) {
|
||||
int length = charCodes.length;
|
||||
final endVal = RangeError.checkValidRange(start, end, length);
|
||||
final charCodeList = new List<int>.from(
|
||||
charCodes.take(endVal).skip(start),
|
||||
growable: false);
|
||||
return createFromCharCodes(charCodeList, 0, charCodeList.length, null);
|
||||
final int codeCount = charCodes.length;
|
||||
if (start >= codeCount) return "";
|
||||
if (end == null || end > codeCount) {
|
||||
end = codeCount;
|
||||
}
|
||||
}
|
||||
// Don't know length of iterable, so iterate and see if all the values
|
||||
// are there.
|
||||
if (start < 0) throw new RangeError.range(start, 0, charCodes.length);
|
||||
var it = charCodes.iterator;
|
||||
for (int i = 0; i < start; i++) {
|
||||
if (!it.moveNext()) {
|
||||
throw new RangeError.range(start, 0, i);
|
||||
}
|
||||
if (!it.moveNext()) return "";
|
||||
}
|
||||
List<int> charCodeList;
|
||||
int bits = 0; // Bitwise-or of all char codes in list.
|
||||
final endVal = end;
|
||||
if (endVal == null) {
|
||||
var list = <int>[];
|
||||
while (it.moveNext()) {
|
||||
int code = it.current;
|
||||
// Bitwise-or of all char codes in list.
|
||||
// There are two valid ranges:
|
||||
// 0x00-0xFF: Valid one-byte string.
|
||||
// 0x100-0xFFFFF: Valid two-byte string with bits-values in range
|
||||
// 0x10000-0xFFFFF already encoded as surrogate pairs.
|
||||
// Numbers above that, or negative, correspond to input "char codes"
|
||||
// outside of the range U+0000 .. U+10FFFF.
|
||||
int bits = 0;
|
||||
int takeCount = (end == null) ? -1 : (end - start); // -1 means no limit.
|
||||
var list = <int>[];
|
||||
while (takeCount != 0 && it.moveNext()) {
|
||||
takeCount -= 1;
|
||||
int code = it.current;
|
||||
if (code <= 0xFFFF) {
|
||||
bits |= code;
|
||||
list.add(code);
|
||||
} else {
|
||||
code -= 0x10000;
|
||||
// Any value in the range 0x100..0xFFFFF can be used for the second
|
||||
// number. Using 0xD800 to represent containing surrogate pairs.
|
||||
bits |= code | 0xD800;
|
||||
list
|
||||
..add(0xD800 + (code >>> 10))
|
||||
..add(0xDC00 + (code & 0x3FF));
|
||||
}
|
||||
charCodeList = makeListFixedLength<int>(list);
|
||||
} else {
|
||||
if (endVal < start) {
|
||||
throw new RangeError.range(endVal, start, charCodes.length);
|
||||
}
|
||||
int len = endVal - start;
|
||||
charCodeList = new List<int>.generate(len, (int i) {
|
||||
if (!it.moveNext()) {
|
||||
throw new RangeError.range(endVal, start, start + i);
|
||||
}
|
||||
int code = it.current;
|
||||
bits |= code;
|
||||
return code;
|
||||
});
|
||||
}
|
||||
int length = charCodeList.length;
|
||||
if (bits < 0) {
|
||||
throw new ArgumentError(charCodes);
|
||||
if (bits < 0 || bits > 0xFFFFF) {
|
||||
throw ArgumentError.value(charCodes, "charCodes",
|
||||
"Contains invalid character code, not 0 <= code <= 0x10FFFF");
|
||||
}
|
||||
List<int> codeUnitList = makeListFixedLength<int>(list);
|
||||
int length = codeUnitList.length;
|
||||
bool isOneByteString = (bits <= _maxLatin1);
|
||||
if (isOneByteString) {
|
||||
return _createOneByteString(charCodeList, 0, length);
|
||||
return _createOneByteString(codeUnitList, 0, length);
|
||||
}
|
||||
return createFromCharCodes(charCodeList, 0, length, bits);
|
||||
return _TwoByteString._allocateFromTwoByteList(codeUnitList, 0, length);
|
||||
}
|
||||
|
||||
// Inlining is disabled as a workaround to http://dartbug.com/37800.
|
||||
|
||||
@@ -61,19 +61,16 @@ abstract final class StringBase implements String {
|
||||
bool _isWhitespace(int codeUnit);
|
||||
|
||||
// Constants used by replaceAll encoding of string slices between matches.
|
||||
// A string slice (start+length) is encoded in a single Smi to save memory
|
||||
// A string slice (start+length) is encoded in a single "Smi" to save memory
|
||||
// overhead in the common case.
|
||||
// We use fewer bits for length (11 bits) than for the start index (19+ bits).
|
||||
// For long strings, it's possible to have many large indices,
|
||||
// but it's unlikely to have many long lengths since slices don't overlap.
|
||||
// If there are few matches in a long string, then there are few long slices,
|
||||
// and if there are many matches, there'll likely be many short slices.
|
||||
//
|
||||
// Encoding is: 0((start << _lengthBits) | length)
|
||||
// Wasm does not have a Smi type, so the entire 64-bit integer value can
|
||||
// be used. Strings are limited to 2^32-1 characters, so using ~32 bits
|
||||
// for both is reasonable.
|
||||
// Encoding is: -((start << _lengthBits) | length)
|
||||
|
||||
// Number of bits used by length.
|
||||
// This is the shift used to encode and decode the start index.
|
||||
static const int _lengthBits = 11;
|
||||
static const int _lengthBits = 31;
|
||||
// The maximal allowed length value in an encoded slice.
|
||||
static const int _maxLengthValue = (1 << _lengthBits) - 1;
|
||||
// Mask of length in encoded smi value.
|
||||
@@ -81,14 +78,8 @@ abstract final class StringBase implements String {
|
||||
static const int _startBits = _maxUnsignedSmiBits - _lengthBits;
|
||||
// Maximal allowed start index value in an encoded slice.
|
||||
static const int _maxStartValue = (1 << _startBits) - 1;
|
||||
// We pick 30 as a safe lower bound on available bits in a negative smi.
|
||||
// TODO(lrn): Consider allowing more bits for start on 64-bit systems.
|
||||
static const int _maxUnsignedSmiBits = 30;
|
||||
|
||||
// For longer strings, calling into C++ to create the result of a
|
||||
// [replaceAll] is faster than [_joinReplaceAllOneByteResult].
|
||||
// TODO(lrn): See if this limit can be tweaked.
|
||||
static const int _maxJoinReplaceOneByteStringLength = 500;
|
||||
// Size of unsigned "Smi"s, which are all non-negative Wasm integers.
|
||||
static const int _maxUnsignedSmiBits = 63;
|
||||
|
||||
int get hashCode {
|
||||
int hash = getHash(this);
|
||||
@@ -111,7 +102,7 @@ abstract final class StringBase implements String {
|
||||
* It's `null` if unknown.
|
||||
*/
|
||||
static String createFromCharCodes(
|
||||
Iterable<int> charCodes, int start, int? end, int? limit) {
|
||||
Iterable<int> charCodes, int start, int? end) {
|
||||
// TODO(srdjan): Also skip copying of wide typed arrays.
|
||||
final ccid = ClassID.getID(charCodes);
|
||||
if (ccid != ClassID.cidFixedLengthList &&
|
||||
@@ -119,100 +110,119 @@ abstract final class StringBase implements String {
|
||||
ccid != ClassID.cidGrowableList &&
|
||||
ccid != ClassID.cidImmutableList) {
|
||||
if (charCodes is Uint8List) {
|
||||
final actualEnd =
|
||||
RangeError.checkValidRange(start, end, charCodes.length);
|
||||
return createOneByteString(charCodes, start, actualEnd - start);
|
||||
} else if (charCodes is! Uint16List) {
|
||||
end = _actualEnd(end, charCodes.length);
|
||||
if (start >= end) return "";
|
||||
return createOneByteString(charCodes, start, end - start);
|
||||
} else if (charCodes is Uint16List) {
|
||||
end = _actualEnd(end, charCodes.length);
|
||||
if (start >= end) return "";
|
||||
for (var i = start; i < end; i++) {
|
||||
if (charCodes[i] > _maxLatin1) {
|
||||
return TwoByteString.allocateFromTwoByteList(charCodes, start, end);
|
||||
}
|
||||
}
|
||||
return _createFromOneByteCodes(charCodes, start, end);
|
||||
} else {
|
||||
return _createStringFromIterable(charCodes, start, end);
|
||||
}
|
||||
}
|
||||
final int codeCount = charCodes.length;
|
||||
final actualEnd = RangeError.checkValidRange(start, end, codeCount);
|
||||
final len = actualEnd - start;
|
||||
if (len == 0) return "";
|
||||
end = _actualEnd(end, charCodes.length);
|
||||
final len = end - start;
|
||||
if (len <= 0) return "";
|
||||
|
||||
final typedCharCodes = unsafeCast<List<int>>(charCodes);
|
||||
|
||||
final int actualLimit =
|
||||
limit ?? _scanCodeUnits(typedCharCodes, start, actualEnd);
|
||||
if (actualLimit < 0) {
|
||||
// The bitwise-or of char codes below 0xFFFF in the input,
|
||||
// and of the char codes above - 0x10000.
|
||||
// If the result is negative, there was a negative input.
|
||||
// If the result is in the range 0x00..0xFF, all inputs were in that range.
|
||||
// If the result is in the range 0x100..0xFFFF, either all inputs were in
|
||||
// that range, or `multiCodeUnitChars` below is greater than zero.
|
||||
// If the result is > 0xFFFFF, the input contained a value > 0x10FFFF,
|
||||
// which is invalid.
|
||||
int bits = 0;
|
||||
// The count of char codes above 0xFFFF in the input.
|
||||
// If greater than zero, the char codes cannot directly be used
|
||||
// as the content of a one-byte or two-byte string,
|
||||
// but must be a two-byte string with this many code units *more* than
|
||||
// `end - start` to account for surrogate pairs.
|
||||
int multiCodeUnitChars = 0;
|
||||
for (var i = start; i < end; i++) {
|
||||
var code = typedCharCodes[i];
|
||||
var nonBmpCode = code - 0x10000;
|
||||
if (nonBmpCode < 0) {
|
||||
bits |= code;
|
||||
continue;
|
||||
}
|
||||
bits |= nonBmpCode | 0x10000;
|
||||
multiCodeUnitChars += 1;
|
||||
}
|
||||
if (bits < 0 || bits > 0xFFFFF) {
|
||||
throw ArgumentError(typedCharCodes);
|
||||
}
|
||||
if (actualLimit <= _maxLatin1) {
|
||||
return createOneByteString(typedCharCodes, start, len);
|
||||
if (multiCodeUnitChars == 0) {
|
||||
if (bits <= _maxLatin1) {
|
||||
return createOneByteString(typedCharCodes, start, len);
|
||||
}
|
||||
assert(bits <= _maxUtf16);
|
||||
return TwoByteString.allocateFromTwoByteList(typedCharCodes, start, end);
|
||||
}
|
||||
if (actualLimit <= _maxUtf16) {
|
||||
return TwoByteString.allocateFromTwoByteList(
|
||||
typedCharCodes, start, actualEnd);
|
||||
}
|
||||
// TODO(lrn): Consider passing limit to _createFromCodePoints, because
|
||||
// the function is currently fully generic and doesn't know that its
|
||||
// charCodes are not all Latin-1 or Utf-16.
|
||||
return _createFromCodePoints(typedCharCodes, start, actualEnd);
|
||||
return _createFromAdjustedCodePoints(
|
||||
typedCharCodes, start, end, end - start + multiCodeUnitChars);
|
||||
}
|
||||
|
||||
static int _scanCodeUnits(List<int> charCodes, int start, int end) {
|
||||
int bits = 0;
|
||||
for (int i = start; i < end; i++) {
|
||||
int code = charCodes[i];
|
||||
bits |= code;
|
||||
}
|
||||
return bits;
|
||||
}
|
||||
static int _actualEnd(int? end, int length) =>
|
||||
(end == null || end > length) ? length : end;
|
||||
|
||||
static String _createStringFromIterable(
|
||||
Iterable<int> charCodes, int start, int? end) {
|
||||
// Treat charCodes as Iterable.
|
||||
bool endKnown = false;
|
||||
if (charCodes is EfficientLengthIterable) {
|
||||
int length = charCodes.length;
|
||||
final endVal = RangeError.checkValidRange(start, end, length);
|
||||
final charCodeList =
|
||||
List<int>.from(charCodes.take(endVal).skip(start), growable: false);
|
||||
return createFromCharCodes(charCodeList, 0, charCodeList.length, null);
|
||||
endKnown = true;
|
||||
int knownEnd = charCodes.length;
|
||||
if (end == null || end > knownEnd) end = knownEnd;
|
||||
if (start >= end) return "";
|
||||
}
|
||||
// Don't know length of iterable, so iterate and see if all the values
|
||||
// are there.
|
||||
if (start < 0) throw RangeError.range(start, 0, charCodes.length);
|
||||
|
||||
var it = charCodes.iterator;
|
||||
for (int i = 0; i < start; i++) {
|
||||
if (!it.moveNext()) {
|
||||
throw RangeError.range(start, 0, i);
|
||||
}
|
||||
|
||||
int skipCount = start;
|
||||
while (skipCount > 0) {
|
||||
if (!it.moveNext()) return "";
|
||||
skipCount--;
|
||||
}
|
||||
List<int> charCodeList;
|
||||
int bits = 0; // Bitwise-or of all char codes in list.
|
||||
final endVal = end;
|
||||
if (endVal == null) {
|
||||
var list = <int>[];
|
||||
while (it.moveNext()) {
|
||||
int code = it.current;
|
||||
|
||||
// Bitwise-or of all char codes in list,
|
||||
// plus code - 0x10000 for values above 0x10000.
|
||||
// If <0 or >0xFFFFF at the end, inputs were not valid.
|
||||
int bits = 0;
|
||||
int takeCount = end == null ? -1 : end - start;
|
||||
final list = <int>[];
|
||||
while (takeCount != 0 && it.moveNext()) {
|
||||
takeCount--;
|
||||
int code = it.current;
|
||||
int nonBmpChar = code - 0x10000;
|
||||
if (nonBmpChar < 0) {
|
||||
bits |= code;
|
||||
list.add(code);
|
||||
} else {
|
||||
bits |= nonBmpChar | 0xD800;
|
||||
list
|
||||
..add(0xD800 | (nonBmpChar >> 10))
|
||||
..add(0xDC00 | (nonBmpChar & 0x3FF));
|
||||
}
|
||||
charCodeList = makeListFixedLength<int>(list);
|
||||
} else {
|
||||
if (endVal < start) {
|
||||
throw RangeError.range(endVal, start, charCodes.length);
|
||||
}
|
||||
int len = endVal - start;
|
||||
charCodeList = List<int>.generate(len, (int i) {
|
||||
if (!it.moveNext()) {
|
||||
throw RangeError.range(endVal, start, start + i);
|
||||
}
|
||||
int code = it.current;
|
||||
bits |= code;
|
||||
return code;
|
||||
});
|
||||
}
|
||||
int length = charCodeList.length;
|
||||
if (bits < 0) {
|
||||
if (bits < 0 || bits > 0xFFFFF) {
|
||||
throw ArgumentError(charCodes);
|
||||
}
|
||||
List<int> charCodeList = makeListFixedLength<int>(list);
|
||||
int length = charCodeList.length;
|
||||
bool isOneByteString = (bits <= _maxLatin1);
|
||||
if (isOneByteString) {
|
||||
return createOneByteString(charCodeList, 0, length);
|
||||
}
|
||||
return createFromCharCodes(charCodeList, 0, length, bits);
|
||||
return TwoByteString.allocateFromTwoByteList(charCodeList, 0, length);
|
||||
}
|
||||
|
||||
static String createOneByteString(List<int> charCodes, int start, int len) {
|
||||
@@ -240,24 +250,31 @@ abstract final class StringBase implements String {
|
||||
return result;
|
||||
}
|
||||
|
||||
static String _createFromCodePoints(List<int> charCodes, int start, int end) {
|
||||
/// Creates two-byte string for [codePoints] from [start] to [end].
|
||||
///
|
||||
/// The code points contain a number of code points above 0xFFFF,
|
||||
/// `length - (end - start)` of them, which is why they require
|
||||
/// a two-byte string of length [length].
|
||||
static String _createFromAdjustedCodePoints(
|
||||
List<int> codePoints, int start, int end, int length) {
|
||||
assert(length > end - start);
|
||||
TwoByteString result = TwoByteString.withLength(length);
|
||||
int cursor = 0;
|
||||
for (int i = start; i < end; i++) {
|
||||
int c = charCodes[i];
|
||||
if (c < 0) throw ArgumentError.value(i);
|
||||
if (c > 0xff) {
|
||||
return _createFromAdjustedCodePoints(charCodes, start, end);
|
||||
var code = codePoints[i];
|
||||
var nonBmpCode = code - 0x10000;
|
||||
if (nonBmpCode < 0) {
|
||||
result._setAt(cursor++, code);
|
||||
} else {
|
||||
result
|
||||
.._setAt(cursor++, 0xD800 | (nonBmpCode >>> 10))
|
||||
.._setAt(cursor++, 0xDC00 | (nonBmpCode & 0x3FF));
|
||||
}
|
||||
}
|
||||
return _createFromOneByteCodes(charCodes, start, end);
|
||||
}
|
||||
|
||||
static String _createFromAdjustedCodePoints(
|
||||
List<int> codePoints, int start, int end) {
|
||||
StringBuffer a = StringBuffer();
|
||||
for (int i = start; i < end; i++) {
|
||||
a.writeCharCode(codePoints[i]);
|
||||
if (cursor != length) {
|
||||
throw ConcurrentModificationError(codePoints);
|
||||
}
|
||||
return a.toString();
|
||||
return result;
|
||||
}
|
||||
|
||||
String operator [](int index) => String.fromCharCode(codeUnitAt(index));
|
||||
@@ -638,10 +655,7 @@ abstract final class StringBase implements String {
|
||||
if (startIndex == 0 && length == 0) return this;
|
||||
length += _addReplaceSlice(matches, startIndex, this.length);
|
||||
bool replacementIsOneByte = replacement is OneByteString;
|
||||
if (replacementIsOneByte &&
|
||||
length < _maxJoinReplaceOneByteStringLength &&
|
||||
this is OneByteString) {
|
||||
// TODO(lrn): Is there a cut-off point, or is runtime always faster?
|
||||
if (replacementIsOneByte && this is OneByteString) {
|
||||
return _joinReplaceAllOneByteResult(this, matches, length);
|
||||
}
|
||||
return _joinReplaceAllResult(this, matches, length, replacementIsOneByte);
|
||||
@@ -699,7 +713,6 @@ abstract final class StringBase implements String {
|
||||
* If they are, then we have to check the base string slices to know
|
||||
* whether the result must be a one-byte string.
|
||||
*/
|
||||
|
||||
String _joinReplaceAllResult(String base, List matches, int length,
|
||||
bool replacementStringsAreOneByte) {
|
||||
if (length < 0) throw ArgumentError.value(length);
|
||||
@@ -793,9 +806,7 @@ abstract final class StringBase implements String {
|
||||
}
|
||||
if (matches.isEmpty) return this;
|
||||
length += _addReplaceSlice(matches, startIndex, this.length);
|
||||
if (replacementStringsAreOneByte &&
|
||||
length < _maxJoinReplaceOneByteStringLength &&
|
||||
this is OneByteString) {
|
||||
if (replacementStringsAreOneByte && this is OneByteString) {
|
||||
return _joinReplaceAllOneByteResult(this, matches, length);
|
||||
}
|
||||
return _joinReplaceAllResult(
|
||||
|
||||
@@ -10,7 +10,14 @@ class String {
|
||||
@patch
|
||||
factory String.fromCharCodes(Iterable<int> charCodes,
|
||||
[int start = 0, int? end]) {
|
||||
return StringBase.createFromCharCodes(charCodes, start, end, null);
|
||||
RangeError.checkNotNegative(start, "start");
|
||||
if (end != null) {
|
||||
if (end < start) {
|
||||
throw RangeError.range(end, start, null, "end");
|
||||
}
|
||||
if (end == start) return "";
|
||||
}
|
||||
return StringBase.createFromCharCodes(charCodes, start, end);
|
||||
}
|
||||
|
||||
@patch
|
||||
|
||||
@@ -123,8 +123,11 @@ abstract final class String implements Comparable<String>, Pattern {
|
||||
/// ```
|
||||
/// If [start] and [end] are provided, only the values of [charCodes]
|
||||
/// at positions from `start` to, but not including, `end`, are used.
|
||||
/// The `start` and `end` values must satisfy
|
||||
/// `0 <= start <= end <= charCodes.length`.
|
||||
/// The `start` and `end` values must satisfy `0 <= start <= end`.
|
||||
/// If [start] is omitted, it defaults to zero, the start of [charCodes],
|
||||
/// and if [end] is omitted, all char-codes after [start] are included.
|
||||
/// If [charCodes] does not have [end], or even [start], elements,
|
||||
/// the specified char-codes may be shorter than `end - start`, or even empty.
|
||||
external factory String.fromCharCodes(Iterable<int> charCodes,
|
||||
[int start = 0, int? end]);
|
||||
|
||||
|
||||
@@ -6,12 +6,13 @@ import "package:expect/expect.dart";
|
||||
import "dart:typed_data";
|
||||
|
||||
main() {
|
||||
iter(count, [values]) => values is List
|
||||
Iterable<int> iter(count, [values]) => values is List
|
||||
? new Iterable<int>.generate(count, (x) => values[x])
|
||||
: new Iterable<int>.generate(count, (x) => values);
|
||||
test(expect, iter, [start = 0, end]) {
|
||||
void test(String expect, Iterable<int> iter, [int start = 0, int? end]) {
|
||||
var actual = new String.fromCharCodes(iter, start, end);
|
||||
Expect.equals(expect, actual);
|
||||
Expect.equals(expect, actual,
|
||||
'$iter:${iter.runtimeType}[${start > 0 ? start : ""}:${end ?? ""}]');
|
||||
}
|
||||
|
||||
testThrows(iterable, [start = 0, end]) {
|
||||
@@ -141,23 +142,26 @@ main() {
|
||||
"ABCDEFGH".codeUnits,
|
||||
]) {
|
||||
test("ABCDEFGH", iterable);
|
||||
// start varies, end is null.
|
||||
// start provided, end is null.
|
||||
test("ABCDEFGH", iterable, 0);
|
||||
test("BCDEFGH", iterable, 1);
|
||||
test("H", iterable, 7);
|
||||
test("", iterable, 8);
|
||||
// start = 0, end varies.
|
||||
test("", iterable, 10);
|
||||
// start = 0, end provided.
|
||||
test("ABCDEFGH", iterable, 0);
|
||||
test("A", iterable, 0, 1);
|
||||
test("AB", iterable, 0, 2);
|
||||
test("ABCDEFG", iterable, 0, 7);
|
||||
test("ABCDEFGH", iterable, 0, 8);
|
||||
test("ABCDEFGH", iterable, 0, 10);
|
||||
test("", iterable, 0, 0);
|
||||
// Both varying.
|
||||
test("ABCDEFGH", iterable, 0, 8);
|
||||
test("AB", iterable, 0, 2);
|
||||
// Both provided and start > 0.
|
||||
test("GH", iterable, 6, 8);
|
||||
test("GH", iterable, 6, 10);
|
||||
test("DE", iterable, 3, 5);
|
||||
test("", iterable, 8, 10);
|
||||
test("", iterable, 10, 12);
|
||||
test("", iterable, 3, 3);
|
||||
}
|
||||
// Can split surrogates in input, but not a single big code point.
|
||||
@@ -196,9 +200,13 @@ main() {
|
||||
testThrowsRange(iterable, -1);
|
||||
testThrowsRange(iterable, 0, -1);
|
||||
testThrowsRange(iterable, 2, 1);
|
||||
testThrowsRange(iterable, 0, length + 1);
|
||||
testThrowsRange(iterable, length + 1);
|
||||
testThrowsRange(iterable, length + 1, length + 2);
|
||||
|
||||
// Positions after end are acceptable.
|
||||
test(string, iterable, 0, length + 1);
|
||||
test(string.substring(string.length ~/ 2), iterable, string.length ~/ 2,
|
||||
length + 1);
|
||||
test("", iterable, length + 1);
|
||||
test("", iterable, length + 1, length + 2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,12 +227,12 @@ main() {
|
||||
const cLatin1 = const [0x00, 0xff];
|
||||
const cUtf16 = const [0x00, 0xffff, 0xdfff, 0xdbff, 0xdfff, 0xdbff];
|
||||
const cCodepoints = const [0x00, 0xffff, 0xdfff, 0x10ffff, 0xdbff];
|
||||
List gLatin1 = cLatin1.toList(growable: true);
|
||||
List gUtf16 = cUtf16.toList(growable: true);
|
||||
List gCodepoints = cCodepoints.toList(growable: true);
|
||||
List fLatin1 = cLatin1.toList(growable: false);
|
||||
List fUtf16 = cUtf16.toList(growable: false);
|
||||
List fCodepoints = cCodepoints.toList(growable: false);
|
||||
List<int> gLatin1 = cLatin1.toList(growable: true);
|
||||
List<int> gUtf16 = cUtf16.toList(growable: true);
|
||||
List<int> gCodepoints = cCodepoints.toList(growable: true);
|
||||
List<int> fLatin1 = cLatin1.toList(growable: false);
|
||||
List<int> fUtf16 = cUtf16.toList(growable: false);
|
||||
List<int> fCodepoints = cCodepoints.toList(growable: false);
|
||||
Uint8List bLatin1 = new Uint8List(2)..setRange(0, 2, cLatin1);
|
||||
Uint16List wLatin1 = new Uint16List(2)..setRange(0, 2, cLatin1);
|
||||
Uint16List wUtf16 = new Uint16List(6)..setRange(0, 6, cUtf16);
|
||||
|
||||
@@ -143,23 +143,26 @@ main() {
|
||||
"ABCDEFGH".codeUnits,
|
||||
]) {
|
||||
test("ABCDEFGH", iterable);
|
||||
// start varies, end is null.
|
||||
// start provided, end is null.
|
||||
test("ABCDEFGH", iterable, 0);
|
||||
test("BCDEFGH", iterable, 1);
|
||||
test("H", iterable, 7);
|
||||
test("", iterable, 8);
|
||||
// start = 0, end varies.
|
||||
test("", iterable, 10);
|
||||
// start = 0, end provided.
|
||||
test("ABCDEFGH", iterable, 0);
|
||||
test("A", iterable, 0, 1);
|
||||
test("AB", iterable, 0, 2);
|
||||
test("ABCDEFG", iterable, 0, 7);
|
||||
test("ABCDEFGH", iterable, 0, 8);
|
||||
test("ABCDEFGH", iterable, 0, 10);
|
||||
test("", iterable, 0, 0);
|
||||
// Both varying.
|
||||
test("ABCDEFGH", iterable, 0, 8);
|
||||
test("AB", iterable, 0, 2);
|
||||
// Both provided and start > 0.
|
||||
test("GH", iterable, 6, 8);
|
||||
test("GH", iterable, 6, 10);
|
||||
test("DE", iterable, 3, 5);
|
||||
test("", iterable, 8, 10);
|
||||
test("", iterable, 10, 12);
|
||||
test("", iterable, 3, 3);
|
||||
}
|
||||
// Can split surrogates in input, but not a single big code point.
|
||||
@@ -198,9 +201,13 @@ main() {
|
||||
testThrowsRange(iterable, -1);
|
||||
testThrowsRange(iterable, 0, -1);
|
||||
testThrowsRange(iterable, 2, 1);
|
||||
testThrowsRange(iterable, 0, length + 1);
|
||||
testThrowsRange(iterable, length + 1);
|
||||
testThrowsRange(iterable, length + 1, length + 2);
|
||||
|
||||
// Positions after end are acceptable.
|
||||
test(string, iterable, 0, length + 1);
|
||||
test(string.substring(string.length ~/ 2), iterable, string.length ~/ 2,
|
||||
length + 1);
|
||||
test("", iterable, length + 1);
|
||||
test("", iterable, length + 1, length + 2);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user