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よりも一桁速いですね。