Skip to content

Commit 29ad621

Browse files
committed
Add optional startIndex to String.replaceFirst
BUG= https://code.google.com/p/dart/issues/detail?id=3194 R=lrn@google.com Review URL: https://codereview.chromium.org//462463003 git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart@39490 260f80e4-7a28-3924-810f-c04153c831b5
1 parent d44664c commit 29ad621

File tree

5 files changed

+86
-18
lines changed

5 files changed

+86
-18
lines changed

runtime/lib/string_patch.dart

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -414,23 +414,29 @@ class _StringBase {
414414
return pattern.allMatches(this.substring(startIndex)).isNotEmpty;
415415
}
416416

417-
String replaceFirst(Pattern pattern, String replacement) {
417+
String replaceFirst(Pattern pattern,
418+
String replacement,
419+
[int startIndex = 0]) {
418420
if (pattern is! Pattern) {
419421
throw new ArgumentError("${pattern} is not a Pattern");
420422
}
421423
if (replacement is! String) {
422424
throw new ArgumentError("${replacement} is not a String");
423425
}
424-
StringBuffer buffer = new StringBuffer();
425-
int startIndex = 0;
426-
Iterator iterator = pattern.allMatches(this).iterator;
427-
if (iterator.moveNext()) {
428-
Match match = iterator.current;
429-
buffer..write(this.substring(startIndex, match.start))
430-
..write(replacement);
431-
startIndex = match.end;
426+
if (startIndex is! int) {
427+
throw new ArgumentError("${startIndex} is not an int");
432428
}
433-
return (buffer..write(this.substring(startIndex))).toString();
429+
if ((startIndex < 0) || (startIndex > this.length)) {
430+
throw new RangeError.range(startIndex, 0, this.length);
431+
}
432+
Iterator iterator =
433+
startIndex == 0 ? pattern.allMatches(this).iterator
434+
: pattern.allMatches(this, startIndex).iterator;
435+
if (!iterator.moveNext()) return this;
436+
Match match = iterator.current;
437+
return "${this.substring(0, match.start)}"
438+
"$replacement"
439+
"${this.substring(match.end)}";
434440
}
435441

436442
String replaceAll(Pattern pattern, String replacement) {

sdk/lib/_internal/lib/js_string.dart

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,13 @@ class JSString extends Interceptor implements String, JSIndexable {
7070
return stringReplaceAllFuncUnchecked(this, from, onMatch, onNonMatch);
7171
}
7272

73-
String replaceFirst(Pattern from, String to) {
73+
String replaceFirst(Pattern from, String to, [int startIndex = 0]) {
7474
checkString(to);
75-
return stringReplaceFirstUnchecked(this, from, to);
75+
checkInt(startIndex);
76+
if (startIndex < 0 || startIndex > this.length) {
77+
throw new RangeError.range(startIndex, 0, this.length);
78+
}
79+
return stringReplaceFirstUnchecked(this, from, to, startIndex);
7680
}
7781

7882
List<String> split(Pattern pattern) {

sdk/lib/_internal/lib/string_helper.dart

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,14 @@ stringReplaceJS(receiver, replacer, to) {
7777
return JS('String', r'#.replace(#, #)', receiver, replacer, to);
7878
}
7979

80+
stringReplaceFirstRE(receiver, regexp, to, startIndex) {
81+
var match = regexp._execGlobal(receiver, startIndex);
82+
if (match == null) return receiver;
83+
var start = match.start;
84+
var end = match.end;
85+
return "${receiver.substring(0,start)}$to${receiver.substring(end)}";
86+
}
87+
8088
const String ESCAPE_REGEXP = r'[[\]{}()*+?.\\^$|]';
8189

8290
stringReplaceAllUnchecked(receiver, from, to) {
@@ -185,12 +193,16 @@ stringReplaceAllStringFuncUnchecked(receiver, pattern, onMatch, onNonMatch) {
185193
}
186194

187195

188-
stringReplaceFirstUnchecked(receiver, from, to) {
196+
stringReplaceFirstUnchecked(receiver, from, to, [int startIndex = 0]) {
189197
if (from is String) {
190-
return stringReplaceJS(receiver, from, to);
198+
var index = receiver.indexOf(from, startIndex);
199+
if (index < 0) return receiver;
200+
return '${receiver.substring(0, index)}$to'
201+
'${receiver.substring(index + from.length)}';
191202
} else if (from is JSSyntaxRegExp) {
192-
var re = regExpGetNative(from);
193-
return stringReplaceJS(receiver, re, to);
203+
return startIndex == 0 ?
204+
stringReplaceJS(receiver, regExpGetNative(from), to) :
205+
stringReplaceFirstRE(receiver, from, to, startIndex);
194206
} else {
195207
checkNull(from);
196208
// TODO(floitsch): implement generic String.replace (with patterns).

sdk/lib/core/string.dart

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -409,11 +409,12 @@ abstract class String implements Comparable<String>, Pattern {
409409

410410
/**
411411
* Returns a new string in which the first occurence of [from] in this string
412-
* is replaced with [to]:
412+
* is replaced with [to], starting from [startIndex]:
413413
*
414414
* '0.0001'.replaceFirst(new RegExp(r'0'), ''); // '.0001'
415+
* '0.0001'.replaceFirst(new RegExp(r'0'), '7', 1); // '0.7001'
415416
*/
416-
String replaceFirst(Pattern from, String to);
417+
String replaceFirst(Pattern from, String to, [int startIndex = 0]);
417418

418419
/**
419420
* Replaces all substrings that match [from] with [replace].

tests/corelib/string_replace_test.dart

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,51 @@ class StringReplaceTest {
4242

4343
// Test replacing the empty string.
4444
Expect.equals("toAtoBtoCto", "AtoBtoCto".replaceFirst("", "to"));
45+
46+
// Test startIndex.
47+
Expect.equals(
48+
"foo-AAA-foo-bar", "foo-bar-foo-bar".replaceFirst("bar", "AAA", 4));
49+
50+
// Test startIndex skipping one case at the begining.
51+
Expect.equals(
52+
"foo-bar-AAA-bar", "foo-bar-foo-bar".replaceFirst("foo", "AAA", 1));
53+
54+
// Test startIndex skipping one case at the begining.
55+
Expect.equals(
56+
"foo-bar-foo-AAA", "foo-bar-foo-bar".replaceFirst("bar", "AAA", 5));
57+
58+
// Test startIndex replacing with the empty string.
59+
Expect.equals(
60+
"foo-bar--bar", "foo-bar-foo-bar".replaceFirst("foo", "", 1));
61+
62+
// Test startIndex with a RegExp with carat
63+
Expect.equals(
64+
"foo-bar-foo-bar",
65+
"foo-bar-foo-bar".replaceFirst(new RegExp(r"^foo"), "", 8));
66+
67+
// Test startIndex with a RegExp
68+
Expect.equals(
69+
"aaa{3}X{3}", "aaa{3}aaa{3}".replaceFirst(new RegExp(r"a{3}"), "X", 1));
70+
71+
// Test startIndex with regexp-looking String
72+
Expect.equals(
73+
"aaa{3}aaX", "aaa{3}aaa{3}".replaceFirst("a{3}", "X", 3));
74+
75+
// Test negative startIndex
76+
Expect.throws(
77+
() => "hello".replaceFirst("h", "X", -1), (e) => e is RangeError);
78+
79+
// Test startIndex too large
80+
Expect.throws(
81+
() => "hello".replaceFirst("h", "X", 6), (e) => e is RangeError);
82+
83+
// Test null startIndex
84+
Expect.throws(
85+
() => "hello".replaceFirst("h", "X", null), (e) => e is ArgumentError);
86+
87+
// Test object startIndex
88+
Expect.throws(
89+
() => "hello".replaceFirst("h", "X", new Object()));
4590
}
4691
}
4792

0 commit comments

Comments
 (0)