[dart] r43716 committed - Add String.replaceFirstMapped....

14 views
Skip to first unread message

da...@googlecode.com

unread,
Feb 12, 2015, 4:04:32 AM2/12/15
to com...@dartlang.org
Revision: 43716
Author: l...@google.com
Date: Thu Feb 12 09:04:09 2015 UTC
Log: Add String.replaceFirstMapped.

Also refactory VM implementation of checking if a string is one-byte,
and adds dart2js implementation of replaceFirst for non-string/regexp
pattern.

BUG= http://dartbug.com/2979
R=floi...@google.com, ipo...@google.com

Review URL: https://codereview.chromium.org//920453002
https://code.google.com/p/dart/source/detail?r=43716

Modified:
/branches/bleeding_edge/dart/runtime/lib/string_patch.dart

/branches/bleeding_edge/dart/sdk/lib/_internal/compiler/js_lib/interceptors.dart

/branches/bleeding_edge/dart/sdk/lib/_internal/compiler/js_lib/js_string.dart

/branches/bleeding_edge/dart/sdk/lib/_internal/compiler/js_lib/string_helper.dart
/branches/bleeding_edge/dart/sdk/lib/core/string.dart
/branches/bleeding_edge/dart/tests/corelib/string_replace_test.dart

=======================================
--- /branches/bleeding_edge/dart/runtime/lib/string_patch.dart Fri Jan 30
09:46:04 2015 UTC
+++ /branches/bleeding_edge/dart/runtime/lib/string_patch.dart Thu Feb 12
09:04:09 2015 UTC
@@ -87,6 +87,13 @@

int get hashCode native "String_getHashCode";

+ bool get _isOneByte {
+ // Alternatively return false and override it on one-byte string
classes.
+ int id = ClassID.getID(this);
+ return id == ClassID.cidOneByteString ||
+ id == ClassID.cidExternalOneByteString;
+ }
+
/**
* Create the most efficient string representation for specified
* [charCodes].
@@ -559,9 +566,7 @@
if (startIndex is! int) {
throw new ArgumentError("${startIndex} is not an int");
}
- if ((startIndex < 0) || (startIndex > this.length)) {
- throw new RangeError.range(startIndex, 0, this.length);
- }
+ RangeError.checkValueInInterval(startIndex, 0,
this.length, "startIndex");
Iterator iterator =
startIndex == 0 ? pattern.allMatches(this).iterator
: pattern.allMatches(this, startIndex).iterator;
@@ -608,15 +613,12 @@
}
}
length += _addReplaceSlice(matches, startIndex, this.length);
- bool replacementIsOneByte = (replacement is _OneByteString) ||
- (replacement is _ExternalOneByteString);
- if (replacementIsOneByte && length <
_maxJoinReplaceOneByteStringLength) {
+ bool replacementIsOneByte = replacement._isOneByte;
+ if (replacementIsOneByte &&
+ length < _maxJoinReplaceOneByteStringLength &&
+ this._isOneByte) {
// TODO(lrn): Is there a cut-off point, or is runtime always faster?
- bool thisIsOneByte = (this is _OneByteString) ||
- (this is _ExternalOneByteString);
- if (replacementIsOneByte && thisIsOneByte) {
- return _joinReplaceAllOneByteResult(this, matches, length);
- }
+ return _joinReplaceAllOneByteResult(this, matches, length);
}
return _joinReplaceAllResult(this, matches, length,
replacementIsOneByte);
@@ -688,26 +690,53 @@
bool replacementStringsAreOneByte = true;
for (Match match in pattern.allMatches(this)) {
length += _addReplaceSlice(matches, startIndex, match.start);
- String replacement = replace(match).toString();
+ var replacement = "${replace(match)}";
matches.add(replacement);
length += replacement.length;
- replacementStringsAreOneByte = replacementStringsAreOneByte &&
- (replacement is _OneByteString ||
- replacement is _ExternalOneByteString);
+ replacementStringsAreOneByte =
+ replacementStringsAreOneByte && replacement._isOneByte;
startIndex = match.end;
}
+ if (matches.isEmpty) return this;
length += _addReplaceSlice(matches, startIndex, this.length);
if (replacementStringsAreOneByte &&
- length < _maxJoinReplaceOneByteStringLength) {
- bool thisIsOneByte = (this is _OneByteString) ||
- (this is _ExternalOneByteString);
- if (thisIsOneByte) {
- return _joinReplaceAllOneByteResult(this, matches, length);
- }
+ length < _maxJoinReplaceOneByteStringLength &&
+ this._isOneByte) {
+ return _joinReplaceAllOneByteResult(this, matches, length);
}
return _joinReplaceAllResult(this, matches, length,
replacementStringsAreOneByte);
}
+
+ String replaceFirstMapped(Pattern pattern, String replace(Match match),
+ [int startIndex = 0]) {
+ if (pattern == null) throw new ArgumentError.notNull("pattern");
+ if (replace == null) throw new ArgumentError.notNull("replace");
+ if (startIndex == null) throw new ArgumentError.notNull("startIndex");
+ RangeError.checkValueInInterval(startIndex, 0,
this.length, "startIndex");
+
+ var matches = pattern.allMatches(this, startIndex).iterator;
+ if (!matches.moveNext()) return this;
+ var match = matches.current;
+ var replacement = "${replace(match)}";
+ var slices = [];
+ int length = 0;
+ if (match.start > 0) {
+ length += _addReplaceSlice(slices, 0, match.start);
+ }
+ slices.add(replacement);
+ length += replacement.length;
+ if (match.end < this.length) {
+ length += _addReplaceSlice(slices, match.end, this.length);
+ }
+ bool replacementIsOneByte = _replacement._isOneByte;
+ if (replacementIsOneByte &&
+ length < _maxJoinReplaceOneByteStringLength &&
+ this._isOneByte) {
+ return _joinReplaceAllOneByteResult(this, matches, length);
+ }
+ return _joinReplaceAllResult(this, slices, length,
replacementIsOneByte);
+ }

static String _matchString(Match match) => match[0];
static String _stringIdentity(String string) => string;
@@ -770,7 +799,7 @@
static String _interpolateSingle(Object o) {
final s = o.toString();
if (s is! String) {
- throw new ArgumentError(o);
+ throw new ArgumentError(Error.safeToString(o));
}
return s;
}
@@ -792,7 +821,7 @@
totalLength += s.length;
i++;
} else if (s is! String) {
- throw new ArgumentError(s);
+ throw new ArgumentError(Error.safeToString(e));
} else {
// Handle remaining elements without checking for one-byte-ness.
while (++i < numValues) {
@@ -800,7 +829,7 @@
final s = e.toString();
values[i] = s;
if (s is! String) {
- throw new ArgumentError(s);
+ throw new ArgumentError(Error.safeToString(e));
}
}
return _concatRangeNative(values, 0, numValues);
=======================================
---
/branches/bleeding_edge/dart/sdk/lib/_internal/compiler/js_lib/interceptors.dart
Tue Jan 27 13:11:38 2015 UTC
+++
/branches/bleeding_edge/dart/sdk/lib/_internal/compiler/js_lib/interceptors.dart
Thu Feb 12 09:04:09 2015 UTC
@@ -30,6 +30,7 @@
stringReplaceAllFuncUnchecked,
stringReplaceAllUnchecked,
stringReplaceFirstUnchecked,
+ stringReplaceFirstMappedUnchecked,
lookupAndCacheInterceptor,
lookupDispatchRecord,
StringMatch,
=======================================
---
/branches/bleeding_edge/dart/sdk/lib/_internal/compiler/js_lib/js_string.dart
Fri Jan 30 09:46:04 2015 UTC
+++
/branches/bleeding_edge/dart/sdk/lib/_internal/compiler/js_lib/js_string.dart
Thu Feb 12 09:04:09 2015 UTC
@@ -73,11 +73,17 @@
String replaceFirst(Pattern from, String to, [int startIndex = 0]) {
checkString(to);
checkInt(startIndex);
- if (startIndex < 0 || startIndex > this.length) {
- throw new RangeError.range(startIndex, 0, this.length);
- }
+ RangeError.checkValueInInterval(startIndex, 0,
this.length, "startIndex");
return stringReplaceFirstUnchecked(this, from, to, startIndex);
}
+
+ String replaceFirstMapped(Pattern from, String replace(Match match),
+ [int startIndex = 0]) {
+ checkNull(replace);
+ checkInt(startIndex);
+ RangeError.checkValueInInterval(startIndex, 0,
this.length, "startIndex");
+ return stringReplaceFirstMappedUnchecked(this, from, replace,
startIndex);
+ }

List<String> split(Pattern pattern) {
checkNull(pattern);
=======================================
---
/branches/bleeding_edge/dart/sdk/lib/_internal/compiler/js_lib/string_helper.dart
Thu Sep 4 13:49:30 2014 UTC
+++
/branches/bleeding_edge/dart/sdk/lib/_internal/compiler/js_lib/string_helper.dart
Thu Feb 12 09:04:09 2015 UTC
@@ -193,21 +193,34 @@
}


-stringReplaceFirstUnchecked(receiver, from, to, [int startIndex = 0]) {
+stringReplaceFirstUnchecked(receiver, from, to, int startIndex) {
if (from is String) {
- var index = receiver.indexOf(from, startIndex);
+ int index = receiver.indexOf(from, startIndex);
if (index < 0) return receiver;
return '${receiver.substring(0, index)}$to'
'${receiver.substring(index + from.length)}';
- } else if (from is JSSyntaxRegExp) {
+ }
+ if (from is JSSyntaxRegExp) {
return startIndex == 0 ?
stringReplaceJS(receiver, regExpGetNative(from), to) :
stringReplaceFirstRE(receiver, from, to, startIndex);
- } else {
- checkNull(from);
- // TODO(floitsch): implement generic String.replace (with patterns).
- throw "String.replace(Pattern) UNIMPLEMENTED";
}
+ checkNull(from);
+ Iterator<Match> matches = from.allMatches(receiver, startIndex).iterator;
+ if (!matches.moveNext()) return receiver;
+ Match match = matches.current;
+ return '${receiver.substring(0, match.start)}$to'
+ '${receiver.substring(match.end)}';
+}
+
+stringReplaceFirstMappedUnchecked(receiver, from, replace,
+ int startIndex) {
+ Iterator<Match> matches = from.allMatches(receiver, startIndex).iterator;
+ if (!matches.moveNext()) return receiver;
+ Match match = matches.current;
+ String replacement = "${replace(match)}";
+ return '${receiver.substring(0, match.start)}$replacement'
+ '${receiver.substring(match.end)}';
}

stringJoinUnchecked(array, separator) {
=======================================
--- /branches/bleeding_edge/dart/sdk/lib/core/string.dart Fri Nov 21
10:29:15 2014 UTC
+++ /branches/bleeding_edge/dart/sdk/lib/core/string.dart Thu Feb 12
09:04:09 2015 UTC
@@ -414,7 +414,7 @@
bool contains(Pattern other, [int startIndex = 0]);

/**
- * Returns a new string in which the first occurence of [from] in this
string
+ * Returns a new string in which the first occurence of [from] in this
string
* is replaced with [to], starting from [startIndex]:
*
* '0.0001'.replaceFirst(new RegExp(r'0'), ''); // '.0001'
@@ -422,6 +422,20 @@
*/
String replaceFirst(Pattern from, String to, [int startIndex = 0]);

+ /**
+ * Replace the first occurence of [from] in this string.
+ *
+ * Returns a new string, which is this string
+ * except that the first match of [pattern], starting from [startIndex],
+ * is replaced by the result of calling [replace] with the match object.
+ *
+ * If the value returned by calling `replace` is not a [String], it
+ * is converted to a `String` using its `toString` method, which must
+ * then return a string.
+ */
+ String replaceFirstMapped(Pattern from, String replace(Match match),
+ [int startIndex = 0]);
+
/**
* Replaces all substrings that match [from] with [replace].
*
=======================================
--- /branches/bleeding_edge/dart/tests/corelib/string_replace_test.dart Fri
Aug 22 11:27:32 2014 UTC
+++ /branches/bleeding_edge/dart/tests/corelib/string_replace_test.dart Thu
Feb 12 09:04:09 2015 UTC
@@ -4,92 +4,215 @@

import "package:expect/expect.dart";

-class StringReplaceTest {
- static testMain() {
- Expect.equals(
- "AtoBtoCDtoE", "AfromBtoCDtoE".replaceFirst("from", "to"));
+main() {
+ // Test replaceFirst.
+ Expect.equals(
+ "AtoBtoCDtoE", "AfromBtoCDtoE".replaceFirst("from", "to"));

- // Test with the replaced string at the begining.
- Expect.equals(
- "toABtoCDtoE", "fromABtoCDtoE".replaceFirst("from", "to"));
+ // Test with the replaced string at the begining.
+ Expect.equals(
+ "toABtoCDtoE", "fromABtoCDtoE".replaceFirst("from", "to"));

- // Test with the replaced string at the end.
- Expect.equals(
- "toABtoCDtoEto", "fromABtoCDtoEto".replaceFirst("from", "to"));
+ // Test with the replaced string at the end.
+ Expect.equals(
+ "toABtoCDtoEto", "fromABtoCDtoEto".replaceFirst("from", "to"));
+
+ // Test when there are no occurence of the string to replace.
+ Expect.equals("ABC", "ABC".replaceFirst("from", "to"));
+
+ // Test when the string to change is the empty string.
+ Expect.equals("", "".replaceFirst("from", "to"));
+
+ // Test when the string to change is a substring of the string to
+ // replace.
+ Expect.equals("fro", "fro".replaceFirst("from", "to"));
+
+ // Test when the string to change is the replaced string.
+ Expect.equals("to", "from".replaceFirst("from", "to"));
+
+ // Test when the string to change is the replacement string.
+ Expect.equals("to", "to".replaceFirst("from", "to"));
+
+ // Test replacing by the empty string.
+ Expect.equals("", "from".replaceFirst("from", ""));
+ Expect.equals("AB", "AfromB".replaceFirst("from", ""));
+
+ // Test changing the empty string.
+ Expect.equals("to", "".replaceFirst("", "to"));
+
+ // Test replacing the empty string.
+ Expect.equals("toAtoBtoCto", "AtoBtoCto".replaceFirst("", "to"));
+
+ // Test startIndex.
+ Expect.equals(
+ "foo-AAA-foo-bar", "foo-bar-foo-bar".replaceFirst("bar", "AAA", 4));
+
+ // Test startIndex skipping one case at the begining.
+ Expect.equals(
+ "foo-bar-AAA-bar", "foo-bar-foo-bar".replaceFirst("foo", "AAA", 1));
+
+ // Test startIndex skipping one case at the begining.
+ Expect.equals(
+ "foo-bar-foo-AAA", "foo-bar-foo-bar".replaceFirst("bar", "AAA", 5));
+
+ // Test startIndex replacing with the empty string.
+ Expect.equals(
+ "foo-bar--bar", "foo-bar-foo-bar".replaceFirst("foo", "", 1));
+
+ // Test startIndex with a RegExp with carat
+ Expect.equals(
+ "foo-bar-foo-bar",
+ "foo-bar-foo-bar".replaceFirst(new RegExp(r"^foo"), "", 8));
+
+ // Test startIndex with a RegExp
+ Expect.equals(
+ "aaa{3}X{3}", "aaa{3}aaa{3}".replaceFirst(new RegExp(r"a{3}"), "X",
1));
+
+ // Test startIndex with regexp-looking String
+ Expect.equals(
+ "aaa{3}aaX", "aaa{3}aaa{3}".replaceFirst("a{3}", "X", 3));
+
+ // Test negative startIndex
+ Expect.throws(
+ () => "hello".replaceFirst("h", "X", -1), (e) => e is RangeError);
+
+ // Test startIndex too large
+ Expect.throws(
+ () => "hello".replaceFirst("h", "X", 6), (e) => e is RangeError);
+
+ // Test null startIndex
+ Expect.throws(
+ () => "hello".replaceFirst("h", "X", null), (e) => e is
ArgumentError);
+
+ // Test object startIndex
+ Expect.throws(
+ () => "hello".replaceFirst("h", "X", new Object()));
+
+
+ // Test replaceFirstMapped.
+
+ Expect.equals(
+ "AtoBtoCDtoE", "AfromBtoCDtoE".replaceFirstMapped("from", (_)
=> "to"));
+
+ // Test with the replaced string at the begining.
+ Expect.equals(
+ "toABtoCDtoE", "fromABtoCDtoE".replaceFirstMapped("from", (_)
=> "to"));
+
+ // Test with the replaced string at the end.
+ Expect.equals(
+ "toABtoCDtoEto",
+ "fromABtoCDtoEto".replaceFirstMapped("from", (_) => "to"));
+
+ // Test when there are no occurence of the string to replace.
+ Expect.equals("ABC", "ABC".replaceFirstMapped("from", (_) => "to"));
+
+ // Test when the string to change is the empty string.
+ Expect.equals("", "".replaceFirstMapped("from", (_) => "to"));
+
+ // Test when the string to change is a substring of the string to
+ // replace.
+ Expect.equals("fro", "fro".replaceFirstMapped("from", (_) => "to"));
+
+ // Test when the string to change is the replaced string.
+ Expect.equals("to", "from".replaceFirstMapped("from", (_) => "to"));
+
+ // Test when the string to change is the replacement string.
+ Expect.equals("to", "to".replaceFirstMapped("from", (_) => "to"));
+
+ // Test replacing by the empty string.
+ Expect.equals("", "from".replaceFirstMapped("from", (_) => ""));
+ Expect.equals("AB", "AfromB".replaceFirstMapped("from", (_) => ""));
+
+ // Test changing the empty string.
+ Expect.equals("to", "".replaceFirstMapped("", (_) => "to"));

- // Test when there are no occurence of the string to replace.
- Expect.equals("ABC", "ABC".replaceFirst("from", "to"));
+ // Test replacing the empty string.
+ Expect.equals("toAtoBtoCto", "AtoBtoCto".replaceFirstMapped("", (_)
=> "to"));

- // Test when the string to change is the empty string.
- Expect.equals("", "".replaceFirst("from", "to"));
+ // Test startIndex.
+ Expect.equals(
+ "foo-AAA-foo-bar",
+ "foo-bar-foo-bar".replaceFirstMapped("bar", (_) => "AAA", 4));

- // Test when the string to change is a substring of the string to
- // replace.
- Expect.equals("fro", "fro".replaceFirst("from", "to"));
+ // Test startIndex skipping one case at the begining.
+ Expect.equals(
+ "foo-bar-AAA-bar",
+ "foo-bar-foo-bar".replaceFirstMapped("foo", (_) => "AAA", 1));

- // Test when the string to change is the replaced string.
- Expect.equals("to", "from".replaceFirst("from", "to"));
+ // Test startIndex skipping one case at the begining.
+ Expect.equals(
+ "foo-bar-foo-AAA",
+ "foo-bar-foo-bar".replaceFirstMapped("bar", (_) => "AAA", 5));

- // Test when the string to change is the replacement string.
- Expect.equals("to", "to".replaceFirst("from", "to"));
+ // Test startIndex replacing with the empty string.
+ Expect.equals(
+ "foo-bar--bar", "foo-bar-foo-bar".replaceFirstMapped("foo", (_)
=> "", 1));

- // Test replacing by the empty string.
- Expect.equals("", "from".replaceFirst("from", ""));
- Expect.equals("AB", "AfromB".replaceFirst("from", ""));
+ // Test startIndex with a RegExp with carat
+ Expect.equals(
+ "foo-bar-foo-bar",
+ "foo-bar-foo-bar".replaceFirstMapped(new RegExp(r"^foo"), (_) => "",
8));

- // Test changing the empty string.
- Expect.equals("to", "".replaceFirst("", "to"));
+ // Test startIndex with a RegExp
+ Expect.equals(
+ "aaa{3}X{3}",
+ "aaa{3}aaa{3}".replaceFirstMapped(new RegExp(r"a{3}"), (_) => "X",
1));

- // Test replacing the empty string.
- Expect.equals("toAtoBtoCto", "AtoBtoCto".replaceFirst("", "to"));
+ // Test startIndex with regexp-looking String
+ Expect.equals(
+ "aaa{3}aaX", "aaa{3}aaa{3}".replaceFirstMapped("a{3}", (_) => "X",
3));

- // Test startIndex.
- Expect.equals(
- "foo-AAA-foo-bar", "foo-bar-foo-bar".replaceFirst("bar", "AAA",
4));
+ // Test negative startIndex
+ Expect.throws(
+ () => "hello".replaceFirstMapped("h", (_) => "X", -1),
+ (e) => e is RangeError);

- // Test startIndex skipping one case at the begining.
- Expect.equals(
- "foo-bar-AAA-bar", "foo-bar-foo-bar".replaceFirst("foo", "AAA",
1));
+ // Test startIndex too large
+ Expect.throws(
+ () => "hello".replaceFirstMapped("h", (_) => "X", 6),
+ (e) => e is RangeError);

- // Test startIndex skipping one case at the begining.
- Expect.equals(
- "foo-bar-foo-AAA", "foo-bar-foo-bar".replaceFirst("bar", "AAA",
5));
+ // Test null startIndex
+ Expect.throws(
+ () => "hello".replaceFirstMapped("h", (_) => "X", null),
+ (e) => e is ArgumentError);

- // Test startIndex replacing with the empty string.
- Expect.equals(
- "foo-bar--bar", "foo-bar-foo-bar".replaceFirst("foo", "", 1));
+ // Test object startIndex
+ Expect.throws(
+ () => "hello".replaceFirstMapped("h", (_) => "X", new Object()));

- // Test startIndex with a RegExp with carat
- Expect.equals(
- "foo-bar-foo-bar",
- "foo-bar-foo-bar".replaceFirst(new RegExp(r"^foo"), "", 8));
+ // Test replacement depending on argument.
+ Expect.equals(
+ "foo-BAR-foo-bar",
+ "foo-bar-foo-bar".replaceFirstMapped("bar", (v) =>
v[0].toUpperCase()));

- // Test startIndex with a RegExp
- Expect.equals(
- "aaa{3}X{3}", "aaa{3}aaa{3}".replaceFirst(new
RegExp(r"a{3}"), "X", 1));
+ Expect.equals(
+ "foo-[bar]-foo-bar",
+ "foo-bar-foo-bar".replaceFirstMapped("bar", (v) => "[${v[0]}]"));

- // Test startIndex with regexp-looking String
- Expect.equals(
- "aaa{3}aaX", "aaa{3}aaa{3}".replaceFirst("a{3}", "X", 3));
+ Expect.equals("foo-foo-bar-foo-bar-foo-bar",
+ "foo-bar-foo-bar".replaceFirstMapped("bar", (v) =>
v.input));

- // Test negative startIndex
- Expect.throws(
- () => "hello".replaceFirst("h", "X", -1), (e) => e is RangeError);
+ // Test replacement throwing.
+ Expect.throws(
+ () => "foo-bar".replaceFirstMapped("bar", (v) => throw 42),
+ (e) => e == 42);

- // Test startIndex too large
- Expect.throws(
- () => "hello".replaceFirst("h", "X", 6), (e) => e is RangeError);
+ // Test replacement returning non-String.
+ var o = new Object();
+ Expect.equals("foo-$o", "foo-bar".replaceFirstMapped("bar",
+ (v) { return o; }));

- // Test null startIndex
- Expect.throws(
- () => "hello".replaceFirst("h", "X", null), (e) => e is
ArgumentError);
+ Expect.equals("foo-42", "foo-bar".replaceFirstMapped("bar",
+ (v) { return 42;
}));

- // Test object startIndex
- Expect.throws(
- () => "hello".replaceFirst("h", "X", new Object()));
- }
+ // Test replacement returning object throwing on string-conversion.
+ var n = new Naughty();
+ Expect.throws(
+ () => "foo-bar".replaceFirstMapped("bar", (v) { return n; }));
}

-main() {
- StringReplaceTest.testMain();
+// Fails to return a String on toString, throws if converted by "$naughty".
+class Naughty {
+ toString() => this;
}
Reply all
Reply to author
Forward
0 new messages