Skip to content

Commit f8d22cc

Browse files
committed
Add String#bytesplice with tests.
1 parent 6f0ce72 commit f8d22cc

File tree

5 files changed

+160
-0
lines changed

5 files changed

+160
-0
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Compatibility:
1414
* Add support for array pattern matching. This is opt-in via `--pattern-matching` since pattern matching is not fully supported yet. (#2683, @razetime).
1515
* Fix `Array#[]` with `ArithmeticSequence` argument when step is negative (@itarato).
1616
* Fix `Range#size` and return `nil` for beginningless Range when end isn't Numeric (@rwstauner).
17+
* Add `String#bytesplice` (@itarato).
1718

1819
Performance:
1920

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# -*- encoding: utf-8 -*-
2+
require_relative '../../spec_helper'
3+
4+
describe "String#bytesplice" do
5+
ruby_version_is "3.2" do
6+
it "raises IndexError when index is out of boundary" do
7+
-> { "hello".bytesplice(-6, 0, "xxx") }.should raise_error(IndexError)
8+
end
9+
10+
it "replaces with integer indices" do
11+
"hello".bytesplice(-5, 0, "xxx").should == "xxxhello"
12+
"hello".bytesplice(0, 0, "xxx").should == "xxxhello"
13+
"hello".bytesplice(0, 1, "xxx").should == "xxxello"
14+
"hello".bytesplice(0, 5, "xxx").should == "xxx"
15+
"hello".bytesplice(0, 6, "xxx").should == "xxx"
16+
end
17+
18+
it "raises RangeError when range is out of boundary" do
19+
-> { "hello".bytesplice(-6...-6, "xxx") }.should raise_error(RangeError)
20+
end
21+
22+
it "replaces with ranges" do
23+
"hello".bytesplice(-5...-5, "xxx").should == "xxxhello"
24+
"hello".bytesplice(0...0, "xxx").should == "xxxhello"
25+
"hello".bytesplice(0..0, "xxx").should == "xxxello"
26+
"hello".bytesplice(0...1, "xxx").should == "xxxello"
27+
"hello".bytesplice(0..1, "xxx").should == "xxxllo"
28+
"hello".bytesplice(0..-1, "xxx").should == "xxx"
29+
"hello".bytesplice(0...5, "xxx").should == "xxx"
30+
"hello".bytesplice(0...6, "xxx").should == "xxx"
31+
end
32+
33+
it "raises TypeError when integer index is provided without length argument" do
34+
-> { "hello".bytesplice(0, "xxx") }.should raise_error(TypeError)
35+
end
36+
37+
it "replaces on an empty string" do
38+
"".bytesplice(0, 0, "").should == ""
39+
"".bytesplice(0, 0, "xxx").should == "xxx"
40+
end
41+
end
42+
end
43+
44+
describe "String#bytesplice with multibyte characters" do
45+
ruby_version_is "3.2" do
46+
it "raises IndexError when index is out of byte size boundary" do
47+
-> { "こんにちは".bytesplice(-16, 0, "xxx") }.should raise_error(IndexError)
48+
end
49+
50+
it "raises IndexError when index is not on a codepoint boundary" do
51+
-> { "こんにちは".bytesplice(1, 0, "xxx") }.should raise_error(IndexError)
52+
end
53+
54+
it "raises IndexError when length is not matching the codepoint boundary" do
55+
-> { "こんにちは".bytesplice(0, 1, "xxx") }.should raise_error(IndexError)
56+
-> { "こんにちは".bytesplice(0, 2, "xxx") }.should raise_error(IndexError)
57+
end
58+
59+
it "replaces with integer indices" do
60+
"こんにちは".bytesplice(-15, 0, "xxx").should == "xxxこんにちは"
61+
"こんにちは".bytesplice(0, 0, "xxx").should == "xxxこんにちは"
62+
"こんにちは".bytesplice(0, 3, "xxx").should == "xxxんにちは"
63+
"こんにちは".bytesplice(3, 3, "はは").should == "こははにちは"
64+
"こんにちは".bytesplice(15, 0, "xxx").should == "こんにちはxxx"
65+
end
66+
67+
it "replaces with range" do
68+
"こんにちは".bytesplice(-15...-16, "xxx").should == "xxxこんにちは"
69+
"こんにちは".bytesplice(0...0, "xxx").should == "xxxこんにちは"
70+
"こんにちは".bytesplice(0..2, "xxx").should == "xxxんにちは"
71+
"こんにちは".bytesplice(0...3, "xxx").should == "xxxんにちは"
72+
"こんにちは".bytesplice(0..5, "xxx").should == "xxxにちは"
73+
"こんにちは".bytesplice(0..-1, "xxx").should == "xxx"
74+
"こんにちは".bytesplice(0...15, "xxx").should == "xxx"
75+
"こんにちは".bytesplice(0...18, "xxx").should == "xxx"
76+
end
77+
78+
it "treats negative length for range as 0" do
79+
"こんにちは".bytesplice(0...-100, "xxx").should == "xxxこんにちは"
80+
"こんにちは".bytesplice(3...-100, "xxx").should == "こxxxんにちは"
81+
"こんにちは".bytesplice(-15...-100, "xxx").should == "xxxこんにちは"
82+
end
83+
84+
it "raises when ranges not match codepoint boundaries" do
85+
-> { "こんにちは".bytesplice(0..0, "x") }.should raise_error(IndexError)
86+
-> { "こんにちは".bytesplice(0..1, "x") }.should raise_error(IndexError)
87+
# Begin is incorrect
88+
-> { "こんにちは".bytesplice(-4..-1, "x") }.should raise_error(IndexError)
89+
-> { "こんにちは".bytesplice(-5..-1, "x") }.should raise_error(IndexError)
90+
# End is incorrect
91+
-> { "こんにちは".bytesplice(-3..-2, "x") }.should raise_error(IndexError)
92+
-> { "こんにちは".bytesplice(-3..-3, "x") }.should raise_error(IndexError)
93+
end
94+
end
95+
end

spec/truffle/methods/String.txt

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ b
1515
bytes
1616
bytesize
1717
byteslice
18+
bytesplice
1819
capitalize
1920
capitalize!
2021
casecmp

src/main/ruby/truffleruby/core/string.rb

+59
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,65 @@ def byteslice(index_or_range, length = undefined)
7070
byteslice index, length
7171
end
7272

73+
def bytesplice(*args)
74+
is_range = Primitive.is_a?(args[0], Range)
75+
76+
if args.size == 3
77+
start = Primitive.rb_to_int(args[0])
78+
start += bytesize if start < 0
79+
80+
len = Primitive.rb_to_int(args[1])
81+
str = StringValue(args[2])
82+
elsif args.size == 2
83+
unless is_range
84+
raise(TypeError, "wrong argument type #{Primitive.class(args[0])} (expected Range)")
85+
end
86+
87+
start, len = Primitive.range_normalized_start_length(args[0], bytesize)
88+
len = 0 if len < 0
89+
str = StringValue(args[1])
90+
else
91+
raise(ArgumentError, "wrong number of arguments (given #{args.size}, expected 2..3)")
92+
end
93+
94+
if len < 0
95+
raise(IndexError, "negative length #{len}")
96+
end
97+
98+
if bytesize < start || start < 0
99+
if is_range
100+
raise(RangeError, "#{args[0]} out of range")
101+
else
102+
raise(IndexError, "index #{start} out of string")
103+
end
104+
end
105+
106+
len = bytesize - start if len > bytesize - start
107+
finish = start + len
108+
109+
unless on_codepoint_boundary?(start)
110+
raise(IndexError, "offset #{start} does not land on character boundary")
111+
end
112+
unless on_codepoint_boundary?(finish)
113+
raise(IndexError, "offset #{finish} does not land on character boundary")
114+
end
115+
116+
Primitive.check_mutable_string(self)
117+
enc = Primitive.encoding_ensure_compatible_str(self, str)
118+
Primitive.string_splice(self, str, start, len, enc)
119+
end
120+
121+
private def on_codepoint_boundary?(byte_pos)
122+
char_pos = Primitive.byte_index_to_character_index(self, byte_pos)
123+
adjusted_byte_pos = if char_pos >= size
124+
bytesize
125+
else
126+
Primitive.character_index_to_byte_index(self, char_pos)
127+
end
128+
129+
byte_pos == adjusted_byte_pos
130+
end
131+
73132
def self.try_convert(obj)
74133
Truffle::Type.try_convert obj, String, :to_str
75134
end

src/main/ruby/truffleruby/core/truffle/polyglot_methods.rb

+4
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ def byteslice(...)
8080
to_s.byteslice(...)
8181
end
8282

83+
def bytesplice(...)
84+
to_s.bytesplice(...)
85+
end
86+
8387
def capitalize(...)
8488
to_s.capitalize(...)
8589
end

0 commit comments

Comments
 (0)