Help us understand the problem. What is going on with this article?

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

crowdworks
21世紀の新しいワークスタイルを提供する日本最大級のクラウドソーシング「クラウドワークス」のエンジニアチームです!
https://crowdworks.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away