漢数字を数値に変換する(DSLっぽく)

意図的にスルーする気はなかったけど、今までRuby使ったりRubyの本読んだりする機会が全く無かった。
のだけど、『メタプログラミングRuby』イイヨーとみんなが言うので読んでみたよ。Ruby初心者が読むにはちょっと刺激的な本だったかも。
折角なので復習も兼ねて言語内DSLっぽいの作ってみた。

使い方

下のソースコードを適当なファイル名(ja_number.rbなど)で保存してreqiureして使う。
1.8以前の場合は、irbrubyの起動パラメータ-Kに保存した文字コードを指定。(下はUTF-8の場合)

irb -Ku -r ja_number.rb

追記: 1.9以降の場合は、文字コード指定パラメータは要らないらしい。その代わりmagic commentが必須

irb -r ./ja_number.rb


JaNumberモジュールをincludeすると、アラビア数字の数の代わりに漢数字で書けるようになる。

include JaNumber

minna = Witch.new
minna.age = 三十八歳 # 年齢をセット

index = Library.new
index.volumes = 十万三千冊 # 図書館の蔵書数

ソースコード

各漢数字をメソッドとして定義しつつ、method_missingで受け取ったメソッド名でリストを作って評価する。

# -*- coding: utf-8 -*-
module JaNumber

  module Constants
      DIGITS  = "一二三四五六七八九"
      CLASSES = "十百千"
      UNITS   = "万億兆" # 増やしたければどーぞ
  end

  class Base
      def to_i
          unit_value + class_value + digit_value
      end
      def unit_value;  0; end	# default definition
      def class_value; 0; end	# default definition
      def digit_value; 0; end	# default definition
      def class_value_with(base); class_value + base ; end	# default definition

      # オブジェクトに特異メソッドを定義するための便利メソッド
      def eigen_def(name, &definition)
          (class << self;self;end).send(:define_method, name, &definition)
      end

      BASE = Base.new();

      # 一()から九()を定義
      Constants::DIGITS.split(//).each_with_index do |chr, i|
          define_method chr do
              prev = self
              Base.new().instance_eval do
                  eigen_def :unit_value do previous.unit_value; end
                  eigen_def :class_value do  previous.class_value; end
                  eigen_def :digit_value do  1 + i; end
                  eigen_def :class_value_with do |base|
                      previous.class_value + base * digit_value()
                  end
                  eigen_def :previous do prev; end
                  self
              end
          end
      end
      # 十()、百()、千()を定義
      Constants::CLASSES.split(//).each_with_index do |chr, i|
          define_method chr do
              base = 10 ** (1 + i)
              prev = self
              Base.new().instance_eval do
                  eigen_def :unit_value do previous.unit_value; end
                  eigen_def :class_value do previous.class_value_with(base); end
                  eigen_def :previous do prev; end
                  self
              end
          end
      end
      # 万()、億()、兆()を定義
      Constants::UNITS.split(//).each_with_index do |chr, i|
          define_method chr do
              base = 10000 ** (1 + i)
              prev = self
              Base.new().instance_eval do
                  eigen_def :unit_value do
                      previous.unit_value + base * (previous.class_value + previous.digit_value)
                  end
                  eigen_def :previous do prev; end
                  self
              end
          end
      end
  end

  def method_missing(name, *args)
     # 漢数字で始まっている場合だけ処理
     # "三十八歳"を、BASE.三().十().八().to_i()と解釈。後ろにゴミがあっても無視
     if matcher = /^[#{Constants::UNITS}#{Constants::CLASSES}#{Constants::DIGITS}]+/.match(name.to_s)
        matcher[0].split(//).inject(Base::BASE) do |prev, item| prev.send(item) end.to_i
     else
        super
     end
  end
end

簡単な説明

漢数字をよく見ると、万以上の位(今回はUnitと呼ぶ)と、千・百・十(今回はClassと呼ぶ)と、一から九(今回はDigitと呼ぶ)で働きが違う。


万の累乗単位で考えると、各部分の単純に足し算になっている。

百九十五兆九千五百五十二億
↓
(百九十五兆) + (九千五百五十二億)

千・百・十は、上の万単位の内部では各部分の単純に足し算になっている。

(百九十五兆) + (九千五百五十二億)
↓
(((百) + (九十) + (五)) * 兆)   +    (((九千) + (五百) + (五十) + (二)) * 億)

千・百・十は数字に続く場合と単体で1倍を表す場合がある。

百 → 1 * 100
九十 → 9 * 10

言ってみれば優先順位のある後置演算子のような構造をしている。

パーサを使って木構造にした方が奇麗そうだけど、今回は一文字ずつリストを作っていくことにする。

百九十五兆九千五百五十二億
↓
(ベース)<-百<-九<-十<-五<-兆<-九<-千<-五<-百<-五<-十<-二<-億

計算する時は、各位置での数値を万以上(unit_value)、千・百・十レベル(class_value)、一から九(digit_value)として別々に保持し、合算して全体の値とする。

1. 百     (unit_value=0, class_value=100, digit_value=0)
2. 百九    (unit_value=0, class_value=100, digit_value=9)
3. 百九十   (unit_value=0, class_value=190, digit_value=0)
4. 百九十五  (unit_value=0, class_value=190, digit_value=5)
5. 百九十五兆 (unit_value=195*10^12, class_value=0, digit_value=0)
6. 百九十五兆九(unit_value=195*10^12, class_value=0, digit_value=9)
...

万の累乗単位が繰り上がったところ(上の4→5の位置)でunit_valueを計算し直してclass_valueとdigit_valueはリセットする。
千・百・十が繰り上がったところ(上の2→3の位置)でclass_valueを計算し直してdigit_valueはリセットする。
一から九はとりあえずdigit_valueに入れておく。
動きの無い部分は一字前の値を使う。


千・百・十だけは前に一から九がくるかどうかで動きが変わる。なので前の数字に聞いてやるようにする。

#一から九の場合: 例:「千百二(1102)」の後に「十」が来たら「1120」になる
eigen_def(:class_value_with) do |base| prev.class_value + base * digit_value(); end

#それ意外の場合: 例:「千百(1100)」の後に「十」が来たら 「1110」になる
def class_value_with(base); class_value + base ; end

正直言って美しいロジックではないような気もするけど、リストの任意のところで切っても計算できるようになってると思う。特に使い道は考えてないけど。

追記

今思うとincludeして使うモジュールにConstantsとかBaseのような一般的な名前を含むのは良くないかも。JaNumberモジュールをincludeした時点でそれらの名前が汚染されてしまう。

もう一段モジュールを挟んだ方が良いのかな?変わった名前にして。

テスト

一応簡単にテスト書いてみた。

  require 'test/unit'
  class JaNumberTest < Test::Unit::TestCase
     include JaNumber
     def test3
        assert_equal 3, 三
     end
     def test10
        assert_equal 10, 十
     end
     def test11
        assert_equal 11, 十一
     end
     def test30
         assert_equal 30, 三十
     end
     def test13
         assert_equal 13, 十三 
     end
     def test33
         assert_equal 33, 三十三 
     end
     def test100
         assert_equal 100, 百 
     end
     def test103
         assert_equal 103, 百三 
     end
     def test113
         assert_equal 113, 百十三 
     end
     def test130
         assert_equal 130, 百三十 
     end
     def test133
         assert_equal 133, 百三十三 
     end
     def test1000
         assert_equal 1000, 千 
     end
     def test1003
         assert_equal 1003, 千三 
     end
     def test1013
         assert_equal 1013, 千十三 
     end
     def test1033
         assert_equal 1033, 千三十三 
     end
     def test3013
         assert_equal 3013, 三千十三 
     end
     def test3033
         assert_equal 3033, 三千三十三 
     end
     def test3300
         assert_equal 3300, 三千三百 
     end
     def test3303
         assert_equal 3303, 三千三百三 
     end
     def test3333
         assert_equal 3333, 三千三百三十三 
     end
     def test30000
         assert_equal 30000, 三万 
     end
     def test30003
         assert_equal 30003, 三万三 
     end
     def test300013
         assert_equal 30013, 三万十三 
     end
     def test30030
         assert_equal 30030, 三万三十 
     end
     def test30033
         assert_equal 30033, 三万三十三 
     end
     def test349000203
         assert_equal 349000203, 三億四千九百万二百三 
     end
  end

今回みたいに実装がトリッキーだとTDDで開発しにくいよね。
TDDの良いところだと思う(実装が平易になるので。)