EastAsianWidth 対応 wcwidth

Unicode Character Database の EastAsianWidth に対応する wcwidth です。 コード・ポイントに対する文字が、 端末上で必ず半角なら 1、 必ず全角なら 2 を返します。 フォントによって半角かもしれず、 全角かもしれないなら 3 を返します。

文字幅情報は 2 ビットの並びにパックして並べてあります。 すべてのコード・ポイントの文字幅を並べると巨大になるので、ページ内がすべて同じ文字幅のページはページ単位で文字幅を求めるようにしています。 文字幅が混在しているページだけをビット・マップに詰め込み、 コード・ポイントのページ番号から詰め込まれた変換後のページ番号へと、 ページ・ディレクトリで変換します。 ページの大きさに 256 を選んでいます。

module Ucd
  def wcwidth(codepoint)
    if codepoint < 0x80
      1
    elsif codepoint < 0x020000
      page = EastAsianWidth::DIRECTORY.getbyte(codepoint >> 8)
      if (page & 3) != 0
        page
      else
        x = (page << 6) + (codepoint & 0xff)
        (EastAsianWidth::BITMAP.getbyte(x >> 2) >> ((x & 3) << 1)) & 3
      end
    elsif codepoint < 0x040000
      2
    elsif codepoint < 0x0e0100
      1
    else
      3
    end
  end

  module_function :wcwidth
end

ディレクトリとビット・マップは、 同じ値が連続して並んでいる箇所が多いので、 連長圧縮したものを展開して利用します。 圧縮列の先頭は展開後の長さです。 その後に区画が続きます。 区画の先頭は区画の展開後の長さで、 正の長さに対して非圧縮バイト列が並び、 負の長さに対して続くバイトを長さの絶対値分伸ばします。

module Ucd
  module EastAsianWidth
    def self.uncompress(runlen)
      t = ("\0".b * runlen[0])
      i, j = 1, 0
      while i < runlen.size
        count = runlen[i]; i += 1
        if count >= 0
          count.times { t.setbyte(j, runlen[i]); i += 1; j += 1 }
        else
          x = runlen[i]; i += 1
          (-count).times { t.setbyte(j, x); j += 1 }
        end
      end
      t
    end

    DIRECTORY = uncompress [
      512, 5, 0, 4, 8, 12, 16, -12, 1, 1, 20, -14, 1, 19, 24, 28, 32, 36, 40, 44,
      48, 52, 1, 1, 1, 56, 1, 1, 60, 64, 68, 72, 76, -26, 2, 1, 80, -86, 2, 8,
      84, 1, 1, 1, 1, 88, 1, 1, -43, 2, 1, 92, -8, 1, -25, 3, 7, 2, 2, 1, 1, 1,
      96, 100, -176, 1, 1, 104, -64, 1, 2, 108, 112, -13, 1]
    BITMAP = uncompress [
      1856, -40, 85, 51, 93, 215, 119, 125, 255, 247, 127, 255, 85, 117, 85, 85,
      87, 213, 87, 245, 95, 117, 127, 95, 247, 213, 127, 119, 93, 85, 85, 85,
      221, 85, 213, 85, 85, 245, 213, 85, 253, 85, 87, 213, 127, 87, 255, 93,
      245, 85, 85, 85, 85, 245, 213, -24, 85, 5, 117, 119, 119, 119, 87, -28, 85,
      5, 93, 85, 85, 85, 93, -24, 85, 7, 215, 253, 93, 87, 85, 255, 221, -8, 85,
      -28, 255, -8, 85, 15, 253, 255, 255, 255, 223, 255, 95, 85, 253, 255, 255,
      255, 223, 255, 95, -13, 85, 4, 93, 85, 85, 85, -16, 255, 1, 93, -43, 85,
      -24, 170, -44, 85, 12, 215, 127, 95, 95, 127, 255, 85, 85, 247, 93, 213,
      117, -13, 85, 5, 87, 85, 213, 253, 87, -9, 85, 1, 87, -20, 85, 11, 213, 93,
      93, 85, 213, 117, 85, 85, 125, 117, 213, -9, 85, 19, 213, 87, 213, 127,
      255, 255, 255, 85, 255, 255, 95, 85, 85, 85, 93, 85, 255, 255, 95, -7, 85,
      1, 95, -5, 85, 6, 117, 87, 85, 85, 85, 213, -6, 85, 42, 247, 213, 215, 213,
      93, 93, 117, 253, 215, 221, 255, 119, 85, 255, 85, 95, 85, 85, 87, 87, 117,
      85, 85, 85, 95, 255, 245, 245, 85, 85, 85, 85, 245, 245, 85, 85, 85, 93,
      93, 85, 85, 93, -5, 85, 1, 213, -20, 85, 1, 117, -5, 85, 1, 105, -77, 85,
      -34, 255, 1, 223, -24, 255, 1, 85, -9, 255, 31, 85, 85, 85, 255, 255, 255,
      255, 245, 95, 85, 85, 223, 255, 95, 85, 245, 245, 85, 95, 95, 245, 215,
      245, 95, 85, 85, 85, 245, 95, 85, 213, -5, 85, 7, 125, 93, 245, 85, 95, 85,
      119, -8, 85, 1, 119, -7, 85, 4, 223, 223, 127, 223, -11, 85, 1, 245, -7,
      85, 11, 245, 85, 255, 255, 223, 255, 255, 255, 255, 223, 85, -6, 255, -15,
      85, 1, 93, -5, 85, 1, 213, -7, 85, 3, 245, 255, 255, -53, 85, 2, 253, 95,
      -73, 85, -6, 170, 1, 154, -22, 170, 3, 85, 85, 85, -53, 170, 1, 90, -6, 85,
      4, 170, 170, 170, 85, -15, 170, 2, 106, 169, -20, 170, 2, 106, 169, -25,
      170, 2, 85, 169, -9, 170, 2, 90, 169, -22, 170, 1, 106, -10, 170, 2, 106,
      85, -9, 170, 3, 85, 85, 85, -11, 170, 1, 106, -10, 170, 2, 255, 255, -43,
      170, 1, 106, -48, 170, -16, 85, -35, 170, 1, 86, -13, 170, 1, 106, -38, 85,
      -7, 170, 1, 86, -32, 85, -41, 170, -23, 85, 7, 255, 255, 255, 255, 170,
      170, 90, -5, 85, -8, 170, 7, 106, 170, 170, 170, 170, 106, 170, -37, 85, 1,
      169, -23, 170, 1, 86, -31, 85, 2, 170, 106, -5, 85, 2, 93, 90, -63, 85, 4,
      255, 255, 127, 85, -7, 255, 1, 95, -14, 255, 2, 95, 85, -10, 255, 1, 127,
      -25, 85, 4, 106, 85, 85, 85, -10, 170, 7, 106, 85, 170, 170, 86, 85, 90,
      -43, 85]
  end
end