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:
Lasse R.H. Nielsen 2023-11-17 13:34:22 +00:00 committed by Commit Queue
parent 5543293a16
commit a1783a9c34
9 changed files with 280 additions and 229 deletions

View file

@ -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

View file

@ -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')

View file

@ -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));
}
}

View file

@ -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.

View file

@ -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(

View file

@ -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

View file

@ -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]);

View file

@ -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);

View file

@ -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);
}
}