UTF-8 文字列を省略して一定の表示幅に収める

つまり Go の go/runewidth の runewidth.Truncate みたいなことをしたい。

GitHub - mattn/go-runewidth
https://github.com/mattn/go-runewidth

で、どうやら unicode-display_width という gem があるらしい。

GitHub - janlelis/unicode-display_width: Monospace Unicode character width in Ruby
https://github.com/janlelis/unicode-display_width

道具は揃ったので、順当に実装してみる。

require 'unicode/display_width'

def truncate_string(str, len, omission)
  return str.dup unless Unicode::DisplayWidth.of(str) > len
  lim = len - Unicode::DisplayWidth.of(omission)
  n = 0
  chars = str.each_char.take_while{|c| (n += Unicode::DisplayWidth.of(c)) <= lim }
  chars.join << omission
end

ここでちょっと欲が出た。

Unicode::DisplayWidth.of(str) == str.each_char.inject(0){|r, c| r += Unicode::DisplayWidth.of(c) }

のはずである。
すると、実質的に str に対して二回 Unicode::DisplayWidth.of が呼ばれているってことになるな、と考える。
これ省けばそっちのが速いんじゃね?
けど、文字ごとにメソッドを呼ぶオーバーヘッドもあるし?

というわけで試してみる。 2 回呼んでるならメモ化すればいいじゃないの精神。

def truncate_string2(str, len, omission)
  widths = Hash.new{|h, k| h[k] = Unicode::DisplayWidth.of(k) }
  return str.dup unless str.each_char.inject(0){|r, c| r + widths[c] } > len
  lim = len - widths[omission]
  n = 0
  chars = str.each_char.take_while{|c| (n += widths[c]) <= lim }
  chars.join << omission
end

ベンチ。

                     user     system      total        real
ascii: no memo   2.309000   0.000000   2.309000 (  2.298512)
ascii: memo      0.562000   0.000000   0.562000 (  0.566717)
mb: no memo      0.999000   0.000000   0.999000 (  1.001561)
mb: memo         0.733000   0.000000   0.733000 (  0.725408)

効いてるっぽい。

さらによく考えると、 str を終わりまで舐めて全長を出すのは無駄である。 len を超えた時点で省略するのは確定するんだから。
というわけでほい。

def truncate_string3(str, len, omission)
  widths = Hash.new{|h, k| h[k] = Unicode::DisplayWidth.of(k) }
  n = 0
  over_len_p = false
  str.each_char do |c|
    if (n += widths[c]) > len
      over_len_p = true
      break
    end
  end
  return str.dup unless over_len_p
  lim = len - widths[omission]
  n = 0
  chars = str.each_char.take_while{|c| (n += widths[c]) <= lim }
  chars.join << omission
end

ベンチ。

                                 user     system      total        real
ascii: all len               0.577000   0.000000   0.577000 (  0.569295)
ascii: break when over len   0.374000   0.000000   0.374000 (  0.370433)
mb: all len                  0.734000   0.000000   0.734000 (  0.731004)
mb: break when over len      0.421000   0.000000   0.421000 (  0.416125)

これもまた効いてるかな。

とりあえず自分が使うのに問題はなさそうだから一旦満足。