本を読む

読書やコンピュータなどに関するメモ

Rubyで文字列を桁数で切り詰める

 メールや端末へ出力するときに、文字数でもバイト数でもなくて、桁数で文字列を切り詰めることがあります。というか最近ありました。先頭から60桁で切る、とか、固定幅テキストで表を表す、とかいう話です。

 「桁数=バイト数」という前提が許されるのは昭和までですが、仲間うちのRuby on Railsアプリで桁数切りをやろうとして悩みました。

 もっと簡単な方法がありそうな気がしますが、プログラミング初級者がハマったメモとして残しておきます。簡単な方法があったら教えてえらい人。

 以下、Ruby 1.8+UTF-8環境での結果です。

うまくいかない例:正規表現とかprintfとか

 正規表現の繰り返し制御は、$KCODEが'NONE'の場合はバイト数で、'u'の場合は文字数でマッチするみたいです。

irb(main):001:0> 'あいうえお'.sub(/^(.{3}).*/, '\1')
=> "\343\201\202"
irb(main):002:0> $KCODE='u'
=> "u"
irb(main):003:0> 'あいうえお'.sub(/^(.{3}).*/, '\1')
=> "あいう"

 桁で切るために、一度EUCにして処理し、あとでUTF-8に戻すという方法も考えました。が、これだと、マルチバイト文字の途中に切れ目がある場合、文字の途中で切ってしまっておかしな結果になります。

irb(main):001:0> $KCODE = 'NONE'
=> "NONE"
irb(main):002:0> 'あお'
=> "\343\201\202\343\201\212"
irb(main):003:0> 'あお'.toeuc.sub(/^(.{3}).*/, '\1').toutf8
=> "\352\222\242"

 ちなみに、sptintf(printf)や%演算子は、$KCODEにかかわらずバイト数で切るようです。

irb(main):001:0> printf '%.4s', 'あお'
あ�=> nil

頭から数えて解決

 いい方法が思いつかなかったので、文字列の頭から数を数える単純な方法でメソッドを作ってみました。

class String
  def truncate_column(col)
    count = 0
    ary = []
    self.split(//u).each {|c|
      count += (c.size == 1 ? 1 : 2)
      return ary.join('') if count > col
      ary.push(c)
    }
    self
  end
end

 「半角カナ入力禁止」が許されるのも昭和までかもしれないので、いわゆる半角カナに対応してみます。

class String
  def truncate_column(col)
    count = 0
    ary = []
    self.split(//u).each {|c|
      if (c.size == 1) ||
         (/[。「」、・ヲァィゥェォャュョッーアイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワン゙゚]/u =~ c)
        count += 1
      else
        count += 2
      end
      return ary.join('') if count > col
      ary.push(c)
    }
    self
  end
end

日本語だけじゃなく

 といっても、UTF-8でマルチバイトの文字は、半角カナだけじゃありません。そこで、UAX#11のEast Asian Width属性を参照して全角半角を決めてみます。

 …えーと、こんなんなっちゃいました。

class String
  RE_UAX11_H = Regexp.new("[#{[8361].pack('U')}
    #{[65377].pack('U')}-#{[65500].pack('U')}
    #{[65512].pack('U')}-#{[65518].pack('U')}]", Regexp::EXTENDED, 'u')
  RE_UAX11_Na = Regexp.new("[#{[162].pack('U')}-#{[163].pack('U')}
    #{[165].pack('U')}-#{[166].pack('U')}
    #{[172].pack('U')}
    #{[175].pack('U')}
    #{[10214].pack('U')}-#{[10219].pack('U')}
    #{[10629].pack('U')}-#{[10630].pack('U')}]", Regexp::EXTENDED, 'u')
  RE_UAX11_N = Regexp.new("[#{[128].pack('U')}-#{[160].pack('U')}
    #{[169].pack('U')}
    #{[171].pack('U')}
    #{[181].pack('U')}
    #{[187].pack('U')}
    #{[192].pack('U')}-#{[197].pack('U')}
    #{[199].pack('U')}-#{[207].pack('U')}
    #{[209].pack('U')}-#{[214].pack('U')}
    #{[217].pack('U')}-#{[221].pack('U')}
    #{[226].pack('U')}-#{[229].pack('U')}
    #{[231].pack('U')}
    #{[235].pack('U')}
    #{[238].pack('U')}-#{[239].pack('U')}
    #{[241].pack('U')}
    #{[244].pack('U')}-#{[246].pack('U')}
    #{[251].pack('U')}
    #{[253].pack('U')}
    #{[255].pack('U')}-#{[256].pack('U')}
    #{[258].pack('U')}-#{[272].pack('U')}
    #{[274].pack('U')}
    #{[276].pack('U')}-#{[282].pack('U')}
    #{[284].pack('U')}-#{[293].pack('U')}
    #{[296].pack('U')}-#{[298].pack('U')}
    #{[300].pack('U')}-#{[304].pack('U')}
    #{[308].pack('U')}-#{[311].pack('U')}
    #{[313].pack('U')}-#{[318].pack('U')}
    #{[323].pack('U')}
    #{[325].pack('U')}-#{[327].pack('U')}
    #{[332].pack('U')}
    #{[334].pack('U')}-#{[337].pack('U')}
    #{[340].pack('U')}-#{[357].pack('U')}
    #{[360].pack('U')}-#{[362].pack('U')}
    #{[364].pack('U')}-#{[461].pack('U')}
    #{[463].pack('U')}
    #{[465].pack('U')}
    #{[467].pack('U')}
    #{[469].pack('U')}
    #{[471].pack('U')}
    #{[473].pack('U')}
    #{[475].pack('U')}
    #{[477].pack('U')}-#{[592].pack('U')}
    #{[594].pack('U')}-#{[608].pack('U')}
    #{[610].pack('U')}-#{[707].pack('U')}
    #{[709].pack('U')}-#{[710].pack('U')}
    #{[712].pack('U')}
    #{[716].pack('U')}
    #{[718].pack('U')}-#{[719].pack('U')}
    #{[721].pack('U')}-#{[727].pack('U')}
    #{[732].pack('U')}
    #{[734].pack('U')}
    #{[736].pack('U')}-#{[767].pack('U')}
    #{[884].pack('U')}-#{[912].pack('U')}
    #{[938].pack('U')}-#{[944].pack('U')}
    #{[962].pack('U')}
    #{[970].pack('U')}-#{[1024].pack('U')}
    #{[1026].pack('U')}-#{[1039].pack('U')}
    #{[1104].pack('U')}
    #{[1106].pack('U')}-#{[4348].pack('U')}
    #{[4448].pack('U')}-#{[8207].pack('U')}
    #{[8209].pack('U')}-#{[8210].pack('U')}
    #{[8215].pack('U')}
    #{[8218].pack('U')}-#{[8219].pack('U')}
    #{[8222].pack('U')}-#{[8223].pack('U')}
    #{[8227].pack('U')}
    #{[8232].pack('U')}-#{[8239].pack('U')}
    #{[8241].pack('U')}
    #{[8244].pack('U')}
    #{[8246].pack('U')}-#{[8250].pack('U')}
    #{[8252].pack('U')}-#{[8253].pack('U')}
    #{[8255].pack('U')}-#{[8305].pack('U')}
    #{[8309].pack('U')}-#{[8318].pack('U')}
    #{[8320].pack('U')}
    #{[8325].pack('U')}-#{[8360].pack('U')}
    #{[8362].pack('U')}-#{[8363].pack('U')}
    #{[8365].pack('U')}-#{[8450].pack('U')}
    #{[8452].pack('U')}
    #{[8454].pack('U')}-#{[8456].pack('U')}
    #{[8458].pack('U')}-#{[8466].pack('U')}
    #{[8468].pack('U')}-#{[8469].pack('U')}
    #{[8471].pack('U')}-#{[8480].pack('U')}
    #{[8483].pack('U')}-#{[8485].pack('U')}
    #{[8487].pack('U')}-#{[8490].pack('U')}
    #{[8492].pack('U')}-#{[8526].pack('U')}
    #{[8533].pack('U')}-#{[8538].pack('U')}
    #{[8543].pack('U')}
    #{[8556].pack('U')}-#{[8559].pack('U')}
    #{[8570].pack('U')}-#{[8580].pack('U')}
    #{[8602].pack('U')}-#{[8631].pack('U')}
    #{[8634].pack('U')}-#{[8657].pack('U')}
    #{[8659].pack('U')}
    #{[8661].pack('U')}-#{[8678].pack('U')}
    #{[8680].pack('U')}-#{[8703].pack('U')}
    #{[8705].pack('U')}
    #{[8708].pack('U')}-#{[8710].pack('U')}
    #{[8713].pack('U')}-#{[8714].pack('U')}
    #{[8716].pack('U')}-#{[8718].pack('U')}
    #{[8720].pack('U')}
    #{[8722].pack('U')}-#{[8724].pack('U')}
    #{[8726].pack('U')}-#{[8729].pack('U')}
    #{[8731].pack('U')}-#{[8732].pack('U')}
    #{[8737].pack('U')}-#{[8738].pack('U')}
    #{[8740].pack('U')}
    #{[8742].pack('U')}
    #{[8749].pack('U')}
    #{[8751].pack('U')}-#{[8755].pack('U')}
    #{[8760].pack('U')}-#{[8763].pack('U')}
    #{[8766].pack('U')}-#{[8775].pack('U')}
    #{[8777].pack('U')}-#{[8779].pack('U')}
    #{[8781].pack('U')}-#{[8785].pack('U')}
    #{[8787].pack('U')}-#{[8799].pack('U')}
    #{[8802].pack('U')}-#{[8803].pack('U')}
    #{[8808].pack('U')}-#{[8809].pack('U')}
    #{[8812].pack('U')}-#{[8813].pack('U')}
    #{[8816].pack('U')}-#{[8833].pack('U')}
    #{[8836].pack('U')}-#{[8837].pack('U')}
    #{[8840].pack('U')}-#{[8852].pack('U')}
    #{[8854].pack('U')}-#{[8856].pack('U')}
    #{[8858].pack('U')}-#{[8868].pack('U')}
    #{[8870].pack('U')}-#{[8894].pack('U')}
    #{[8896].pack('U')}-#{[8977].pack('U')}
    #{[8979].pack('U')}-#{[9000].pack('U')}
    #{[9003].pack('U')}-#{[9290].pack('U')}
    #{[9450].pack('U')}
    #{[9548].pack('U')}-#{[9551].pack('U')}
    #{[9588].pack('U')}-#{[9599].pack('U')}
    #{[9616].pack('U')}-#{[9617].pack('U')}
    #{[9622].pack('U')}-#{[9631].pack('U')}
    #{[9634].pack('U')}
    #{[9642].pack('U')}-#{[9649].pack('U')}
    #{[9652].pack('U')}-#{[9653].pack('U')}
    #{[9656].pack('U')}-#{[9659].pack('U')}
    #{[9662].pack('U')}-#{[9663].pack('U')}
    #{[9666].pack('U')}-#{[9669].pack('U')}
    #{[9673].pack('U')}-#{[9674].pack('U')}
    #{[9676].pack('U')}-#{[9677].pack('U')}
    #{[9682].pack('U')}-#{[9697].pack('U')}
    #{[9702].pack('U')}-#{[9710].pack('U')}
    #{[9712].pack('U')}-#{[9732].pack('U')}
    #{[9735].pack('U')}-#{[9736].pack('U')}
    #{[9738].pack('U')}-#{[9741].pack('U')}
    #{[9744].pack('U')}-#{[9747].pack('U')}
    #{[9750].pack('U')}-#{[9755].pack('U')}
    #{[9757].pack('U')}
    #{[9759].pack('U')}-#{[9791].pack('U')}
    #{[9793].pack('U')}
    #{[9795].pack('U')}-#{[9823].pack('U')}
    #{[9826].pack('U')}
    #{[9830].pack('U')}
    #{[9835].pack('U')}
    #{[9838].pack('U')}
    #{[9840].pack('U')}-#{[10044].pack('U')}
    #{[10046].pack('U')}-#{[10101].pack('U')}
    #{[10112].pack('U')}-#{[10213].pack('U')}
    #{[10224].pack('U')}-#{[10628].pack('U')}
    #{[10631].pack('U')}-#{[11805].pack('U')}
    #{[12351].pack('U')}
    #{[19904].pack('U')}-#{[19967].pack('U')}
    #{[42752].pack('U')}-#{[43127].pack('U')}
    #{[55296].pack('U')}-#{[56320].pack('U')}
    #{[64256].pack('U')}-#{[65021].pack('U')}
    #{[65056].pack('U')}-#{[65059].pack('U')}
    #{[65136].pack('U')}-#{[65279].pack('U')}
    #{[65529].pack('U')}-#{[65532].pack('U')}
    #{[65536].pack('U')}-#{[120831].pack('U')}
    #{[917505].pack('U')}-#{[917631].pack('U')}]", Regexp::EXTENDED, 'u')

  def truncate_column(col)
    count = 0
    ary = []
    self.split(//u).each {|c|
      if (c.size == 1) ||
         (RE_UAX11_H =~ c) || (RE_UAX11_Na =~ c) || (RE_UAX11_N =~ c)
        count += 1
      else
        count += 2
      end
      return ary.join('') if count > col
      ary.push(c)
    }
    self
  end
end

 ちなみに定数部分は、こんなやっつけRubyスクリプトで生成しました。

#!/usr/bin/ruby -Ku

PROTOTYPES = %w{ A F H N Na W }
HALF_PROTOTYPES = %w{ H Na N }

h = {}
PROTOTYPES.each {|e| h[e] = [] }

current = nil
start_n = end_n = nil

open('EastAsianWidth.txt') {|f|
  f.each {|line|
    next if /^#/ =~ line || /^$/ =~ line
    code, prop = line.chomp.sub(/ .*/, '').split(/;/)
    code.gsub!(/[\dA-F]+\.\.(\dA-F)/, '\1')
    code_n = code.hex
    next if code_n < 0x80
    if (current == prop)
      end_n = code_n
    else
      if current
        h[current].push(
          if start_n == end_n
            "\#{[#{start_n}].pack('U')}"
          else
            "\#{[#{start_n}].pack('U')}-\#{[#{end_n}].pack('U')}"
          end
        )
      end
      start_n = end_n = code_n
      current = prop
    end
  }
}

HALF_PROTOTYPES.each {|e| puts "  RE_UAX11_#{e} = Regexp.new(\"[#{h[e].join("\n    ")}]\", Regexp::EXTENDED, 'u')" }

別解

 nkfを使う方法も考えたんですが、半角カナが2桁になっちゃうんですよね。

#!/usr/bin/ruby -Ku

require 'nkf'

class String
  def truncate_column(col)
    NKF.nkf("-F#{col} -Ww -x", self).sub(/\n.*/m, '')
  end
end

puts 'あアaあ'.truncate_column(4)

修正2008-06-19: 誤ってEast Asian Width属性のA・H・Naを半角扱いするコードになっていたので、H・Na・Nに修正。

コメント

コメントの投稿

管理者にだけ表示を許可する

トラックバック

http://emasaka.blog65.fc2.com/tb.php/409-0d62dcdc

 | HOME | 

Categories

Recent Entries

Recent Comments

Recent Trackbacks

Appendix

emasaka

emasaka

フリーター。
連絡先はこのへん

Monthly


FC2Ad