漢数字を数値に変換する(DSLっぽく)
意図的にスルーする気はなかったけど、今までRuby使ったりRubyの本読んだりする機会が全く無かった。
のだけど、『メタプログラミングRuby』イイヨーとみんなが言うので読んでみたよ。Ruby初心者が読むにはちょっと刺激的な本だったかも。
折角なので復習も兼ねて言語内DSLっぽいの作ってみた。
使い方
下のソースコードを適当なファイル名(ja_number.rbなど)で保存してreqiureして使う。
1.8以前の場合は、irb、rubyの起動パラメータ-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の良いところだと思う(実装が平易になるので。)