String#bytespliceの拡張

Posted by Shugo Maeda on December 12, 2023 · 2 mins read

NaClの前田です。 Ruby Advent Calendar 2023の12日目の記事です。昨日は@mokioさんでした。

今回はRuby 3.3で拡張される予定のString#bytespliceを紹介します。

String#bytespliceとは

String#bytespliceは、筆者がRuby 3.2で導入したメソッドです。

String#bytespliceはString#[]=と同様に文字列の一部を置き換えるメソッドですが、大きな違いはString#[]=はコードポイント単位でオフセットや長さを指定するのに対し、String#bytespliceはバイト単位で指定することです。

s = "あああいいいあああ"
p s.byteindex(/ああ/, 6) #=> 18
x, y = Regexp.last_match.byteoffset(0) #=> [18, 24]
s.bytesplice(x...y, "おおお")
p s #=> "あああいいいおおおあ"

何がうれしいかというと、コードポイント単位でオフセットを指定した場合は文字列の先頭からコードポイントを数える必要があるのに対し、バイト単位で指定した場合はその必要がないということです。

バイト単位で指定するということはオフセットがコードポイント境界に乗っていないこともありえますが、そういった場合はIndexErrorが発生します。

s = "あいうえお"
s.bytesplice(1, 2, "x") #=> offset 1 does not land on character boundary (IndexError)

Ruby 3.3での拡張

Ruby 3.2ではbytespliceの第2引数(第1引数がRangeでなくIntegerの場合は第3引数)で指定された文字列全体で置換することしかできませんでした。 このため、文字列の一部をその文字列中の他の位置にコピーするためには、いったん文字列オブジェクトのコピーを作成する必要がありました。

s = "abcxxxxxx"
s2 = s[0..2]
s.bytesplice(-3..-1, s2) #=> "abcxxxabc"
p s

Ruby 3.3では追加の引数でコピー先だけでなくコピー元の範囲も指定できるようになったため、文字列オブジェクトのコピーが必要なくなりました。

s = "abcxxxxxx"
s.bytesplice(-3..-1, s, 0..2) #=> "abcxxxabc"
p s

コピー元とコピー先の範囲が重なっている場合も問題なく動作します。

s = "abcxx"
s.bytesplice(-3..-1, s, 0..2) #=> "ababc"
p s

Rangeの代りにIntegerでオフセットと長さを指定することもできます。

s = "abcxx"
s.bytesplice(-3, 3, s, 0, 3) #=> "ababc"
p s

ユースケース

Rubyでテキストエディタを作成したいことがよくありますが、ギャップ・バッファを実装する際に従来は

s = @contents.byteslice(@gap_end, len)
@contents.bytesplice(@gap_start, len, s)

のように記述する必要があったのが

@contents.bytesplice(@gap_start, len, @contents, @gap_end, len)

と記述できます。便利ですね。

まとめ

Ruby 3.3で拡張される予定のString#bytespliceを紹介しました。 みなさんもRubyでテキストエディタを作ってみてください。