Skip to content
This repository was archived by the owner on Mar 23, 2023. It is now read-only.

Commit a09bb56

Browse files
S-YOUtrotterdylan
authored andcommitted
Implement str.replace (#168)
1 parent 5ce962d commit a09bb56

3 files changed

Lines changed: 189 additions & 0 deletions

File tree

runtime/str.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,70 @@ func strNew(f *Frame, t *Type, args Args, _ KWArgs) (*Object, *BaseException) {
478478
return s.ToObject(), nil
479479
}
480480

481+
// strReplace returns a copy of the string s with the first n non-overlapping
482+
// instances of old replaced by sub. If old is empty, it matches at the
483+
// beginning of the string. If n < 0, there is no limit on the number of
484+
// replacements.
485+
func strReplace(f *Frame, args Args, _ KWArgs) (*Object, *BaseException) {
486+
var raised *BaseException
487+
// TODO: Support unicode replace.
488+
expectedTypes := []*Type{StrType, StrType, StrType, ObjectType}
489+
argc := len(args)
490+
if argc == 3 {
491+
expectedTypes = expectedTypes[:argc]
492+
}
493+
if raised := checkMethodArgs(f, "replace", args, expectedTypes...); raised != nil {
494+
return nil, raised
495+
}
496+
n := -1
497+
if argc == 4 {
498+
n, raised = ToIntValue(f, args[3])
499+
if raised != nil {
500+
return nil, raised
501+
}
502+
}
503+
s := toStrUnsafe(args[0]).Value()
504+
// Returns early if no need to replace.
505+
if n == 0 {
506+
return NewStr(s).ToObject(), nil
507+
}
508+
509+
old := toStrUnsafe(args[1]).Value()
510+
sub := toStrUnsafe(args[2]).Value()
511+
numBytes := len(s)
512+
// Even if s and old is blank, replace should return sub, except n is negative.
513+
// This is CPython specific behavior.
514+
if numBytes == 0 && old == "" && n >= 0 {
515+
return NewStr("").ToObject(), nil
516+
}
517+
// If old is non-blank, pass to strings.Replace.
518+
if len(old) > 0 {
519+
return NewStr(strings.Replace(s, old, sub, n)).ToObject(), nil
520+
}
521+
522+
// If old is blank, insert sub after every bytes on s and beginning.
523+
if n < 0 {
524+
n = numBytes + 1
525+
}
526+
// Insert sub at beginning.
527+
buf := bytes.Buffer{}
528+
buf.WriteString(sub)
529+
n--
530+
// Insert after every byte.
531+
i := 0
532+
for n > 0 && i < numBytes {
533+
buf.WriteByte(s[i])
534+
buf.WriteString(sub)
535+
i++
536+
n--
537+
}
538+
// Write the remaining string.
539+
if i < numBytes {
540+
buf.WriteString(s[i:])
541+
}
542+
return NewStr(buf.String()).ToObject(), nil
543+
}
544+
481545
func strRepr(_ *Frame, o *Object) (*Object, *BaseException) {
482546
s := toStrUnsafe(o).Value()
483547
buf := bytes.Buffer{}
@@ -681,6 +745,7 @@ func initStrType(dict map[string]*Object) {
681745
dict["splitlines"] = newBuiltinFunction("splitlines", strSplitLines).ToObject()
682746
dict["startswith"] = newBuiltinFunction("startswith", strStartsWith).ToObject()
683747
dict["strip"] = newBuiltinFunction("strip", strStrip).ToObject()
748+
dict["replace"] = newBuiltinFunction("replace", strReplace).ToObject()
684749
dict["rstrip"] = newBuiltinFunction("rstrip", strRStrip).ToObject()
685750
dict["title"] = newBuiltinFunction("title", strTitle).ToObject()
686751
dict["upper"] = newBuiltinFunction("upper", strUpper).ToObject()

runtime/str_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,16 @@ func TestStrMethods(t *testing.T) {
259259
return NewLong(big.NewInt(2)).ToObject(), nil
260260
}).ToObject(),
261261
}))
262+
intIntType := newTestClass("IntInt", []*Type{ObjectType}, newStringDict(map[string]*Object{
263+
"__int__": newBuiltinFunction("__int__", func(f *Frame, _ Args, _ KWArgs) (*Object, *BaseException) {
264+
return NewInt(2).ToObject(), nil
265+
}).ToObject(),
266+
}))
267+
longIntType := newTestClass("LongInt", []*Type{ObjectType}, newStringDict(map[string]*Object{
268+
"__int__": newBuiltinFunction("__int__", func(f *Frame, _ Args, _ KWArgs) (*Object, *BaseException) {
269+
return NewLong(big.NewInt(2)).ToObject(), nil
270+
}).ToObject(),
271+
}))
262272
cases := []struct {
263273
methodName string
264274
args Args
@@ -379,6 +389,41 @@ func TestStrMethods(t *testing.T) {
379389
{"strip", wrapArgs("foo", "bar", "baz"), nil, mustCreateException(TypeErrorType, "'strip' of 'str' requires 2 arguments")},
380390
{"strip", wrapArgs("\xfboo", NewUnicode("o")), nil, mustCreateException(UnicodeDecodeErrorType, "'utf8' codec can't decode byte 0xfb in position 0")},
381391
{"strip", wrapArgs("foo", NewUnicode("o")), NewUnicode("f").ToObject(), nil},
392+
{"replace", wrapArgs("one!two!three!", "!", "@", 1), NewStr("one@two!three!").ToObject(), nil},
393+
{"replace", wrapArgs("one!two!three!", "!", ""), NewStr("onetwothree").ToObject(), nil},
394+
{"replace", wrapArgs("one!two!three!", "!", "@", 2), NewStr("one@two@three!").ToObject(), nil},
395+
{"replace", wrapArgs("one!two!three!", "!", "@", 3), NewStr("one@two@three@").ToObject(), nil},
396+
{"replace", wrapArgs("one!two!three!", "!", "@", 4), NewStr("one@two@three@").ToObject(), nil},
397+
{"replace", wrapArgs("one!two!three!", "!", "@", 0), NewStr("one!two!three!").ToObject(), nil},
398+
{"replace", wrapArgs("one!two!three!", "!", "@"), NewStr("one@two@three@").ToObject(), nil},
399+
{"replace", wrapArgs("one!two!three!", "x", "@"), NewStr("one!two!three!").ToObject(), nil},
400+
{"replace", wrapArgs("one!two!three!", "x", "@", 2), NewStr("one!two!three!").ToObject(), nil},
401+
{"replace", wrapArgs("\xd0\xb2\xd0\xbe\xd0\xbb", "", "\x00", -1), NewStr("\x00\xd0\x00\xb2\x00\xd0\x00\xbe\x00\xd0\x00\xbb\x00").ToObject(), nil},
402+
{"replace", wrapArgs("\xd0\xb2\xd0\xbe\xd0\xbb", "", "\x01\x02", -1), NewStr("\x01\x02\xd0\x01\x02\xb2\x01\x02\xd0\x01\x02\xbe\x01\x02\xd0\x01\x02\xbb\x01\x02").ToObject(), nil},
403+
{"replace", wrapArgs("abc", "", "-"), NewStr("-a-b-c-").ToObject(), nil},
404+
{"replace", wrapArgs("abc", "", "-", 3), NewStr("-a-b-c").ToObject(), nil},
405+
{"replace", wrapArgs("abc", "", "-", 0), NewStr("abc").ToObject(), nil},
406+
{"replace", wrapArgs("", "", ""), NewStr("").ToObject(), nil},
407+
{"replace", wrapArgs("", "", "a"), NewStr("a").ToObject(), nil},
408+
{"replace", wrapArgs("abc", "a", "--", 0), NewStr("abc").ToObject(), nil},
409+
{"replace", wrapArgs("abc", "xy", "--"), NewStr("abc").ToObject(), nil},
410+
{"replace", wrapArgs("123", "123", ""), NewStr("").ToObject(), nil},
411+
{"replace", wrapArgs("123123", "123", ""), NewStr("").ToObject(), nil},
412+
{"replace", wrapArgs("123x123", "123", ""), NewStr("x").ToObject(), nil},
413+
{"replace", wrapArgs("one!two!three!", "!", "@", NewLong(big.NewInt(1))), NewStr("one@two!three!").ToObject(), nil},
414+
{"replace", wrapArgs("foobar", "bar", "baz", newObject(intIntType)), NewStr("foobaz").ToObject(), nil},
415+
{"replace", wrapArgs("foobar", "bar", "baz", newObject(longIntType)), NewStr("foobaz").ToObject(), nil},
416+
{"replace", wrapArgs("", "", "x"), NewStr("x").ToObject(), nil},
417+
{"replace", wrapArgs("", "", "x", -1), NewStr("x").ToObject(), nil},
418+
{"replace", wrapArgs("", "", "x", 0), NewStr("").ToObject(), nil},
419+
{"replace", wrapArgs("", "", "x", 1), NewStr("").ToObject(), nil},
420+
{"replace", wrapArgs("", "", "x", 1000), NewStr("").ToObject(), nil},
421+
// TODO: Support unicode substring.
422+
{"replace", wrapArgs("foobar", "", NewUnicode("bar")), nil, mustCreateException(TypeErrorType, "'replace' requires a 'str' object but received a 'unicode'")},
423+
{"replace", wrapArgs("foobar", NewUnicode("bar"), ""), nil, mustCreateException(TypeErrorType, "'replace' requires a 'str' object but received a 'unicode'")},
424+
{"replace", wrapArgs("foobar", "bar", "baz", None), nil, mustCreateException(TypeErrorType, "an integer is required")},
425+
{"replace", wrapArgs("foobar", "bar", "baz", newObject(intIndexType)), nil, mustCreateException(TypeErrorType, "an integer is required")},
426+
{"replace", wrapArgs("foobar", "bar", "baz", newObject(longIndexType)), nil, mustCreateException(TypeErrorType, "an integer is required")},
382427
{"rstrip", wrapArgs("foo "), NewStr("foo").ToObject(), nil},
383428
{"rstrip", wrapArgs(" foo bar "), NewStr(" foo bar").ToObject(), nil},
384429
{"rstrip", wrapArgs("foo foo", "o"), NewStr("foo f").ToObject(), nil},

testing/str_test.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,85 @@ def __int__(self):
164164
assert "%x" % 0x1f == "1f"
165165
assert "%X" % 0xffff == "FFFF"
166166

167+
# Test replace
168+
assert 'one!two!three!'.replace('!', '@', 1) == 'one@two!three!'
169+
assert 'one!two!three!'.replace('!', '') == 'onetwothree'
170+
assert 'one!two!three!'.replace('!', '@', 2) == 'one@two@three!'
171+
assert 'one!two!three!'.replace('!', '@', 3) == 'one@two@three@'
172+
assert 'one!two!three!'.replace('!', '@', 4) == 'one@two@three@'
173+
assert 'one!two!three!'.replace('!', '@', 0) == 'one!two!three!'
174+
assert 'one!two!three!'.replace('!', '@') == 'one@two@three@'
175+
assert 'one!two!three!'.replace('x', '@') == 'one!two!three!'
176+
assert 'one!two!three!'.replace('x', '@', 2) == 'one!two!three!'
177+
assert 'abc'.replace('', '-') == '-a-b-c-'
178+
assert 'abc'.replace('', '-', 3) == '-a-b-c'
179+
assert 'abc'.replace('', '-', 0) == 'abc'
180+
assert ''.replace('', '') == ''
181+
assert ''.replace('', 'a') == 'a'
182+
assert 'abc'.replace('a', '--', 0) == 'abc'
183+
assert 'abc'.replace('xy', '--') == 'abc'
184+
assert '123'.replace('123', '') == ''
185+
assert '123123'.replace('123', '') == ''
186+
assert '123x123'.replace('123', '') == 'x'
187+
assert "\xd0\xb2\xd0\xbe\xd0\xbb".replace('', '\0') == "\x00\xd0\x00\xb2\x00\xd0\x00\xbe\x00\xd0\x00\xbb\x00"
188+
assert "\xd0\xb2\xd0\xbe\xd0\xbb".replace('', '\1\2') == '\x01\x02\xd0\x01\x02\xb2\x01\x02\xd0\x01\x02\xbe\x01\x02\xd0\x01\x02\xbb\x01\x02'
189+
190+
class S(str):
191+
pass
192+
193+
s = S('abc')
194+
assert type(s.replace(s, s)) is str
195+
assert type(s.replace('x', 'y')) is str
196+
assert type(s.replace('x', 'y', 0)) is str
197+
# CPython only, pypy supposed to be same as Go
198+
assert ''.replace('', 'x') == 'x'
199+
assert ''.replace('', 'x', -1) == 'x'
200+
assert ''.replace('', 'x', 0) == ''
201+
assert ''.replace('', 'x', 1) == ''
202+
assert ''.replace('', 'x', 1000) == ''
203+
try:
204+
''.replace(None, '')
205+
raise AssertionError
206+
except TypeError:
207+
pass
208+
try:
209+
''.replace('', None)
210+
raise AssertionError
211+
except TypeError:
212+
pass
213+
try:
214+
''.replace('', '', None)
215+
raise AssertionError
216+
except TypeError:
217+
pass
218+
219+
class A(object):
220+
def __int__(self):
221+
return 3
222+
class AL(object):
223+
def __int__(self):
224+
return 3L
225+
226+
class B(object):
227+
def __index__(self):
228+
return 3
229+
class BL(object):
230+
def __index__(self):
231+
return 3L
232+
233+
assert 'aaaaa'.replace('a', 'b', A()) == 'bbbaa'
234+
assert 'aaaaa'.replace('a', 'b', AL()) == 'bbbaa'
235+
try:
236+
'aaaaa'.replace('a', 'b', B())
237+
raise AssertionError
238+
except TypeError:
239+
pass
240+
try:
241+
'aaaaa'.replace('a', 'b', BL())
242+
raise AssertionError
243+
except TypeError:
244+
pass
245+
167246
# Test zfill
168247
assert '123'.zfill(2) == '123'
169248
assert '123'.zfill(3) == '123'

0 commit comments

Comments
 (0)