Ruby: 日本語文字のprintf %sで桁幅を揃える

テキスト処理プログラムで、表っぽいものを作りたいときに、 printfでの桁揃えが役立つ。

sum = 0
shopping = [150, 2000, 10]
shopping.each {|item|
  printf("      %7d円\n", item)
  sum += item
}
printf("合計: %7d円\n", sum)

結果は以下のとおり。

          150円
         2000円
           10円
合計:    2160円

美しい。

では1点ずつ商品名を入れてみよう。

receipt2.rb

#!/usr/bin/env ruby
# -*- coding: utf-8 -*-
sum = 0
shopping = {"りんご" => 150, "図鑑" => 2000, "お菓子棒" => 10}
shopping.each {|item, price|
  printf("%-10s%7d円\n", item, price)
  sum += price
}
printf("%-10s%7d円\n", "合計:", sum)

さー実行してみようか。はい、残念。ガタガタになる。

(Ruby1.8まで)
図鑑       2000円
りんご     150円
お菓子棒     10円
合計:      2160円

(Ruby1.9以降)
りんご           150円
図鑑           2000円
お菓子棒           10円
合計:          2160円

Ruby1.8まではutf-8日本語が3バイトなので3桁で数え、 Ruby1.9以降はどんな文字でも1字は1桁だと数えるため、 fixed width font な仮想端末で漢字==2桁という風習を期待すると 悲しい目に遭う。実はこれ、Ruby1.8で euc-jp か sjis を使えば 2バイト漢字==2桁という風に処理してくれるのでちゃんと揃っていた。

Ruby1.9以降では日本語printfは揃わないのか! utf-8だと絶望的なのか! と悩んでいたが、これは Ruby1.9 以降でも utf-8 でも逃げられる手があった。 一度内部的に euc-jp にして、Ruby1.9 ではさらにバイナリモードに変えてから printfに幅計算をさせるとよい。

簡単な例では以下のようになる。上記プログラムのprintfを以下のように変える。

  require 'kconv'
  format = "%-10s%7d円\n".toeuc
  print(if RUBY_VERSION < "1.9"
          # Ruby1.8
          sprintf(format, item.toeuc, price)
        else
          # Ruby1.9以降
          sprintf(format.force_encoding("binary"),
                  item.toeuc.force_encoding("binary"), price)
        end.toutf8)

つまり、一度euc-jpに変えてsprintfしたものを、出力するときにまた UTF-8に変換し直している。Ruby1.9の場合は、euc-jpにしただけでは 文字数基準の桁幅計算になるので、さらにbinaryにしてバイト数計算で やらせている。

ただ、printfごとにやるのはたいへんなので、以下の定義を読み込ませる。

kprintf.rb

class String
  require 'kconv'
  if defined?("".force_encoding)
    def toeucbin()
      self.toeuc.force_encoding("binary")
    end
  else
    def toeucbin()
      self.toeuc
    end
  end
end

class IO
  def printf(*args)
    out = sprintf(*(args.collect{|x| x.is_a?(String) ? x.toeucbin : x}))
    print out.toutf8
  end
end

class Object
  def printf(*args)
    if args[0].is_a?(String)
      $stdout.printf(*args)
    else
      port = args.shift
      port.printf(*args)
    end
  end
end

これは、出力コードがutf8と決め打ちした場合。 kconv以外を希望の場合は適宜書き換えのこと。

さて、これをロードした上で以下のプログラムを動かしてみる。

receipt4.rb

#!/usr/bin/env ruby
# -*- coding: utf-8 -*-
require './kprintf.rb'
sum = 0
shopping = {"りんご" => 150, "図鑑" => 2000, "お菓子棒" => 10}
shopping.each {|item, price|
  printf("%-10s%7d円\n", item, price)
  sum += price
}
printf("%-10s%7d円\n", "合計:", sum)

はい実行してみよう。

(Ruby1.8まで)
りんご        150円
お菓子棒       10円
図鑑         2000円
合計:        2160円

(Ruby1.9以降)
りんご        150円
お菓子棒       10円
図鑑         2000円
合計:        2160円

おめでとう。