日本語を含んでもきっちり揃えるljust・rjust・centerにしておく

カレンダーコマンドを作るには、等幅フォントを使って、日付ごとの文字数を揃えておく必要がある。数値だけのカレンダーなら、簡単に揃えられる。しかし、そこに日本語の祝日が割り込んでくると、厄介なことになってくる...。

  • 祝日の名称は、文字数が一定でない。
  • そんな時はljustやrjustで揃えればいいんだもんね、とやってみると...
irb(main):028:0> p "秋分の日".rjust(14,'#'), "勤労感謝の日".rjust(14,'#'), "こどもの日".rjust(14,'#')
"##########秋分の日"
"########勤労感謝の日"
"#########こどもの日"

ずれた...。

  • 一方、同じ文字幅の半角文字ならきっちり揃う。本来はこうなって欲しい。
irb(main):030:0> p "12345678".rjust(14,'#'), "123456789012".rjust(14,'#'), "1234567890".rjust(14,'#')
"######12345678"
"##123456789012"
"####1234567890"
  • この原因は、全角も半角も同じ1文字と数えて文字数を合わせようとしているから。
  • できることなら全角は2文字と数えて欲しい。

refineによるString拡張

  • そうゆう訳で、jcalコマンドではStringクラスを勝手に拡張して、ljust_ja・rjust_ja・center_jaメソッドを追加していた。
  • refineを使って定義しているので、Stringクラスに影響を与えるのは、このコードを書いたファイルの中だけ。
module JcalEx
  refine String do
    def length_ja
      half_lenght = count(" -~")
      full_length = (length - half_lenght) * 2
      half_lenght + full_length
    end

    def ljust_ja(width, padstr=' ')
      n = [0, width - length_ja].max
      self + padstr * n
    end

    def rjust_ja(width, padstr=' ')
      n = [0, width - length_ja].max
      padstr * n + self
    end

    def center_ja(width, padstr=' ')
      n = [0, width - length_ja].max
      padstr * (n/2) + self
    end
  end
end
using JcalEx
  • jcalコマンドで使うならこれで用が足りた。
  • 最初はとにかくちゃんと動くことが大切。

重複を排除する

  • でもコードを眺めていると、重複しているのが気になってくる。
  • 重複を排除してみた。
module JcalEx
  refine String do
    def length_ja
      half_lenght = count(" -~")
      full_length = (length - half_lenght) * 2
      half_lenght + full_length
    end

    def margin(width)
      [0, width - length_ja].max
    end

    def ljust_ja (width, padstr=' ') self + padstr * margin(width) end
    def rjust_ja (width, padstr=' ') padstr * margin(width) + self end
    def center_ja(width, padstr=' ') padstr * (margin(width) / 2) + self + padstr * (margin(width + 1) / 2) end
  end
end
using JcalEx
  • 重複は排除された気はするが、分かりにくくなった気もする。

発想を変える

  • ところで、このljust_jaは、オリジナルのljustの機能を満たしていない。
  • padstrが1文字であることを前提に処理している。
  • オリジナルのljustなら、何文字指定してもOKなのだ。
  • しかし、そうゆうことまで考えると、処理がちょっと複雑になってきそう。
  • そこで発想を変えて、オリジナルのljustに不足している部分だけを補うようにしてみたらどうか?
  • 例えば、全角文字を2文字と数えて欲しいのであれば、何らかの半角文字2文字と置き換えてしまう。
  • その後、置き換えた文字列をljustで処理したら、再び元の全角文字に戻す。
  • この発想で試してみる。
module JcalEx
  refine String do
    def length_ja
      half_lenght = count(" -~")
      full_length = (length - half_lenght) * 2
      half_lenght + full_length
    end

    def align_ja(method, width, padstr, dummy='A'*length_ja)
      dummy.succ!.empty? && break while padstr.include?(dummy)
      eval "dummy.#{method}(width, padstr).sub(dummy, self)"
    end

    def ljust_ja(width, padstr=' ') align_ja(:ljust, width, padstr) end
    def rjust_ja(width, padstr=' ') align_ja(:rjust, width, padstr) end
    def center_ja(width, padstr=' ') align_ja(:center, width, padstr) end
  end
end
using JcalEx
  • 試してみると、いい感じで動いてくれる!
    • ブラウザの表示環境によってはズレて見えるが、#の数は一致しているはず。
    • ターミナルで出力すると、ぴったり揃うのだ。
irb(main):025:0> p "秋分の日".rjust_ja(14,'#'), "勤労感謝の日".rjust_ja(14,'#'), "建国記念日".rjust_ja(14,'#')
"######秋分の日"
"##勤労感謝の日"
"####建国記念日"

irb(main):026:0> p "12345678".rjust_ja(14,'#'), "123456789012".rjust_ja(14,'#'), "1234567890".rjust_ja(14,'#')
"######12345678"
"##123456789012"
"####1234567890"

破綻と修行

  • しかし、意地悪くこうゆうのを試すと破綻していることに気付く。
    • 4文字幅を指定したのに、3文字で出力されている。
irb(main):039:0> p "x".rjust_ja(4,'ABCDEFGHIJKLMNOPQRSTUVWXYZ')
"ABx"
  • さらなる修行が必要である。
  • そもそも1文字なら半角のはずなので、オリジナルのrjustで用が足りるはず。
  • よって、半角しか存在しない時は、オリジナルのrjustを実行するようにした。
    • 必要に迫られ全角を数えるfull_lengthメソッドを追加したら、
    • 実はlength_jaはたった1行で書けることに気付いてしまった。
module JcalEx
  refine String do
    def full_length() count("^ -~") end
    def length_ja() length + full_length end

    def align_ja(method, width, padstr, dummy='A'*length_ja)
      return eval "#{method}(width, padstr)" if full_length == 0
      dummy.succ!.empty? && break while padstr.include?(dummy)
      eval "dummy.#{method}(width, padstr).sub(dummy, self)"
    end

    def ljust_ja(width, padstr=' ') align_ja(:ljust, width, padstr) end
    def rjust_ja(width, padstr=' ') align_ja(:rjust, width, padstr) end
    def center_ja(width, padstr=' ') align_ja(:center, width, padstr) end
  end
end
using JcalEx
  • 但し、究極の意地悪コードを書けば、これでも破綻してしまう...。
irb(main):020:0> p "漢".rjust_ja(4,'AAABACADAEAFAGAHAIAJAKALAMANAOAPAQARASATAUAVAWAXAYAZBABBBCBDBEBFBGBHBIBJBKBLBMBNBOBPBQBRBSBTBUBVBWBXBYBZCACBCCCDCECFCGCHCICJCKCLCMCNCOCPCQCRCSCTCUCVCWCXCYCZDADBDCDDDEDFDGDHDIDJDKDLDMDNDODPDQDRDSDTDUDVDWDXDYDZEAEBECEDEEEFEGEHEIEJEKELEMENEOEPEQERESETEUEVEWEXEYEZFAFBFCFDFEFFFGFHFIFJFKFLFMFNFOFPFQFRFSFTFUFVFWFXFYFZGAGBGCGDGEGFGGGHGIGJGKGLGMGNGOGPGQGRGSGTGUGVGWGXGYGZHAHBHCHDHEHFHGHHHIHJHKHLHMHNHOHPHQHRHSHTHUHVHWHXHYHZIAIBICIDIEIFIGIHIIIJIKILIMINIOIPIQIRISITIUIVIWIXIYIZJAJBJCJDJEJFJGJHJIJJJKJLJMJNJOJPJQJRJSJTJUJVJWJXJYJZKAKBKCKDKEKFKGKHKIKJKKKLKMKNKOKPKQKRKSKTKUKVKWKXKYKZLALBLCLDLELFLGLHLILJLKLLLMLNLOLPLQLRLSLTLULVLWLXLYLZMAMBMCMDMEMFMGMHMIMJMKMLMMMNMOMPMQMRMSMTMUMVMWMXMYMZNANBNCNDNENFNGNHNINJNKNLNMNNNONPNQNRNSNTNUNVNWNXNYNZOAOBOCODOEOFOGOHOIOJOKOLOMONOOOPOQOROSOTOUOVOWOXOYOZPAPBPCPDPEPFPGPHPIPJPKPLPMPNPOPPPQPRPSPTPUPVPWPXPYPZQAQBQCQDQEQFQGQHQIQJQKQLQMQNQOQPQQQRQSQTQUQVQWQXQYQZRARBRCRDRERFRGRHRIRJRKRLRMRNRORPRQRRRSRTRURVRWRXRYRZSASBSCSDSESFSGSHSISJSKSLSMSNSOSPSQSRSSSTSUSVSWSXSYSZTATBTCTDTETFTGTHTITJTKTLTMTNTOTPTQTRTSTTTUTVTWTXTYTZUAUBUCUDUEUFUGUHUIUJUKULUMUNUOUPUQURUSUTUUUVUWUXUYUZVAVBVCVDVEVFVGVHVIVJVKVLVMVNVOVPVQVRVSVTVUVVVWVXVYVZWAWBWCWDWEWFWGWHWIWJWKWLWMWNWOWPWQWRWSWTWUWVWWWXWYWZXAXBXCXDXEXFXGXHXIXJXKXLXMXNXOXPXQXRXSXTXUXVXWXXXYXZYAYBYCYDYEYFYGYHYIYJYKYLYMYNYOYPYQYRYSYTYUYVYWYXYYYZZAZBZCZDZEZFZGZHZIZJZKZLZMZNZOZPZQZRZSZTZUZVZWZXZYZZ')
"A漢"
  • しかし、実用上はまったく問題なさそうなので、修行はここまで。

完成

  • この後、半角カタカナの存在にも気付いた。
  • メソッドの順序を並べ替えて、最終のコードは以下のようになった。
module JcalEx
  refine String do
    def full_length() count("^ -~。-゚") end
    def length_ja() length + full_length end
    def ljust_ja(width, padstr=' ') align_ja(:ljust, width, padstr) end
    def rjust_ja(width, padstr=' ') align_ja(:rjust, width, padstr) end
    def center_ja(width, padstr=' ') align_ja(:center, width, padstr) end

    def align_ja(method, width, padstr, dummy='A'*length_ja)
      return eval "#{method}(width, padstr)" if full_length == 0
      dummy.succ!.empty? && break while padstr.include?(dummy)
      eval "dummy.#{method}(width, padstr).sub(dummy, self)"
    end
  end
end
using JcalEx
  • ちょっと行数を節約することに固執し過ぎてしまったかもしれない。
  • 一般的なifブロックを使って、このようにしておいた方が良さそう。
module JcalEx
  refine String do
    def full_length() count("^ -~。-゚") end
    def length_ja() length + full_length end
    def ljust_ja(width, padstr=' ') align_ja(:ljust, width, padstr) end
    def rjust_ja(width, padstr=' ') align_ja(:rjust, width, padstr) end
    def center_ja(width, padstr=' ') align_ja(:center, width, padstr) end

    def align_ja(method, width, padstr, dummy='A'*length_ja)
      if full_length == 0
        eval "#{method}(width, padstr)"
      else
        dummy.succ!.empty? && break while padstr.include?(dummy)
        eval "dummy.#{method}(width, padstr).sub(dummy, self)"
      end
    end
  end
end
using JcalEx
  • eval改めsendを使うことにした。
module JcalEx
  refine String do
    def full_length() count("^ -~。-゚") end
    def length_ja() length + full_length end
    def ljust_ja(width, padstr=' ') align_ja(:ljust, width, padstr) end
    def rjust_ja(width, padstr=' ') align_ja(:rjust, width, padstr) end
    def center_ja(width, padstr=' ') align_ja(:center, width, padstr) end

    def align_ja(method, width, padstr, dummy='A'*length_ja)
      if full_length == 0
        send(method, width, padstr)
      else
        dummy.succ!.empty? && break while padstr.include?(dummy)
        dummy.send(method, width, padstr).sub(dummy, self)
      end
    end
  end
end
using JcalEx

仕様

  • Stringクラスに、ljust_ja・rjust_ja・center_jaインスタンスメソッドを追加した。
  • 全角混じりのテキストでも、ズレることなく左寄せ・右寄せ・中央寄せを行う。
    • 半角は1文字、全角は2文字と数えて処理する。
    • padstrは、すべて半角にしておく必要がある。

ダウンロード