Edited at

Ruby / Railsでマルチバイト文字の境界を意識しつつ指定のバイト数以下に切り捨てる

More than 1 year has passed since last update.

DBに文字列を格納する時にレコード長の制限があったりして、指定のバイト数以下に切り捨てたいことがあります。

例として8バイト以下に切り捨てたい場合で説明します。説明の便宜上、上限値をMAX_BYTESIZEで定義しておきます。

pry(main)> MAX_BYTESIZE=8

=> 8

ASCII文字だけならこんなかんじで簡単なんですが、

pry(main)> 'abcdefghijklmn'.byteslice(0...MAX_BYTESIZE)

=> "abcdefgh"

マルチバイト文字が入ってくると若干めんどくさいです。

マルチバイト文字の境界を何も考えずにぶった切ると、こんなかんじで末尾にゴミが入ってしまう可能性があります。

pry(main)> 'こんにちは'.byteslice(0...MAX_BYTESIZE)

=> "こん\xE3\x81"

こんなよくありがちな用途の汎用メソッドがRailsにないはずがないと思って調べたら、

ActiveSupport::Multibyte::Chars#limit でできることが分かりました。

pry(main)> 'こんにちは'.bytesize

=> 15
pry(main)> 'こん'.bytesize
=> 6
pry(main)> 'こんに'.bytesize
=> 9
pry(main)> 'こんにちは'.mb_chars.limit(MAX_BYTESIZE)
=> #<ActiveSupport::Multibyte::Chars:0x007fb59e37d420 @wrapped_string="こん">
pry(main)> 'こんにちは'.mb_chars.limit(MAX_BYTESIZE).to_s
=> "こん"
pry(main)> 'こんにちは'.mb_chars.limit(MAX_BYTESIZE).to_s.bytesize
=> 6

素のRubyで使いたい場合はActiveSupportだけ個別にインストールすればよいんじゃないかと。


(2017/04/18追記)

Ruby 2.1.0で追加されたString#scrubという不正なバイト列を除去するメソッドを使った方が素のRubyだけで実装できるしパフォーマンスも速かった。

[12] pry(main)> "寿司\u{1f363}"

=> "寿司🍣"
[13] pry(main)> "寿司\u{1f363}".size
=> 3
[14] pry(main)> "寿司\u{1f363}".bytesize
=> 10
[15] pry(main)> "寿司\u{1f363}".byteslice(0,10)
=> "寿司🍣"
[16] pry(main)> "寿司\u{1f363}".byteslice(0,10).scrub('')
=> "寿司🍣"
[17] pry(main)> "寿司\u{1f363}".byteslice(0,9)
=> "寿司\xF0\x9F\x8D"
[18] pry(main)> "寿司\u{1f363}".byteslice(0,9).scrub('')
=> "寿司"

手元のMacBook Pro (Retina, 13-inch、Early 2015)で軽くベンチーマークとってみた

require 'benchmark'

require 'active_support/multibyte'
MAX_LINE_SIZE = 10000
msg = "あ" * 3333 + "\u{1f363}"

Benchmark.bm 100 do |r|
r.report "raw" do
msg
end
r.report "byteslice" do
msg.byteslice(0, MAX_LINE_SIZE)
end
r.report "scrub" do
msg.byteslice(0, MAX_LINE_SIZE).scrub('')
end
r.report "mb_chars" do
msg.mb_chars.limit(MAX_LINE_SIZE).to_s
end
end

                user     system      total        real

raw 0.000000 0.000000 0.000000 ( 0.000005)
byteslice 0.000000 0.000000 0.000000 ( 0.000010)
scrub 0.000000 0.000000 0.000000 ( 0.000137)
mb_chars 0.000000 0.000000 0.000000 ( 0.001624)

String#scrub使うほうがActiveSupport::Multibyte::Chars#limitよりも一桁速いですね。