前の(d:id:terazzo:20101212:1292147950)はあんまりだったのでパーサを使って書き直したよ。
ruby1.9.2にrparsec-1.0をgitからインストールして使用。
使い方は同じ。
include JaNumber minna = Witch.new minna.age = 三十八歳 # 年齢をセット index = Library.new index.volumes = 十万三千冊 # 図書館の蔵書数
ソースコード
各漢数字を数値に変換しつつ、前後のつながりを見て計算する。
「一〜九」「十〜千」「万〜」は前回同様digits、classes、unitsと表記している。
また今回は、singlet:「一〜九」の1文字の数、quadruplet:「1〜9999」の数、と表記している。
名前は適当なので専門用語とかでどういうのかはわからない。
どこがrparsec部分かわかりやすいようにimportせずにパッケージ名から書いている。
# -*- coding: utf-8 -*- module JaNumber module Constants DIGITS = "一二三四五六七八九" CLASSES = "十百千" UNITS = "万億兆" # 増やしたければどーぞ end module JaNumberParser require 'rparsec' # <digit> ::= "一" | "二" | "三" | "四" | "五" | "六" | "七" | "八" | "九" digits = Constants::DIGITS.split(//).map.with_index { |c, i| RParsec::Parsers.char(c).map {|c| 1 + i} }.inject(RParsec::Parsers.zero) {|result, p| result.plus(p)} # <class> ::= "千" | "百" | "十" classes = Constants::CLASSES.split(//).map.with_index { |c, i| RParsec::Parsers.char(c).map {|c| 10 ** (1 + i)} }.inject(RParsec::Parsers.zero) {|result, p| result.plus(p)} # <unit> ::= "万" | "億" | "兆" units = Constants::UNITS.split(//).map.with_index { |c, i| RParsec::Parsers.char(c).map {|c| 10000 ** (1 + i)} }.inject(RParsec::Parsers.zero) {|result, p| result.plus(p)} # <singlet> ::= <digit> singlet = digits # <quadruplet> ::= <digit>? <class> <quadruplet>? | <singlet> quadruplet = nil lazy_quadruplet = RParsec::Parsers.lazy{quadruplet} quadruplet = RParsec::Parsers.sequence(digits.optional(1), classes, lazy_quadruplet.optional(0)) { |digit_value, class_value, next_value| digit_value * class_value + next_value } | singlet # <number> ::= <quadruplet> <unit> <number>? | <quadruplet> number = nil lazy_number = RParsec::Parsers.lazy{number} number = RParsec::Parsers.sequence(quadruplet, units, lazy_number.optional(0)) { |quadruplet_value, unit_value, next_value| quadruplet_value * unit_value + next_value } | quadruplet define_method :parse do |ja_number| number.parse ja_number end module_function :parse end def method_missing(name, *args) # 漢数字で始まっている場合だけ処理 # "三十八歳"の"三十八"の部分を切り出してパーサで数値に変換 if matcher = /^[#{Constants::UNITS}#{Constants::CLASSES}#{Constants::DIGITS}]+/.match(name.to_s) JaNumberParser.parse matcher[0] else super end end end
簡単な説明
万の累乗単位の足し算と考えるのは前回と同じ。
百九十五兆九千五百五十二億 ↓ (((百) + (九十) + (五)) * 兆) + (((九千) + (五百) + (五十) + (二)) * 億)
これを元にEBNFを考える。今回は計算の為なのであまり厳密に考えないで、少々並びが怪しくても受け取れる程度で勘弁してもらう。
まず数字の定義
<digit> ::= "一" | "二" | "三" | "四" | "五" | "六" | "七" | "八" | "九" <class> ::= "千" | "百" | "十" <unit> ::= "万" | "億" | "兆"
以下、これらの組み合わせで数を表現していく。
「一〜九」の一桁の数は
<singlet> ::= <digit>
「百」とか「三十」などの千・百・十単位の数。前に「一〜九」が付いたり付かなかったり
<doublet> ::= <digit>? <class>
「一〜九千九百九十九」の数。
<quadruplet> ::= <doublet>* <singlet>?
最後に数全体。「一〜九千九百九十九」に万・億・兆などが付いたものを繰り返した後「一〜九千九百九十九」が付いたり付かなかったり
<number> ::= (<quadruplet> <unit>)* <quadruplet>?
但しこのままrparsecのパーサに落とし込んでもちゃんと動かない。理由はあまりよくわからないけど、例えば
これを解消するために、とりあえず先頭のマッチを生かしつつ後続の繰り返しを制御するように書き換える。
「千九百十九」なら「千 + (九百 + (十 + (九))」のように考える。
<quadruplet> ::= <digit>? <class> <quadruplet>? | <singlet>
「三億四千九百万二百三」なら、「(三)億 + ((四千九百)万 + (二百三))のように考える。
<number> ::= <quadruplet> <unit> <number>? | <quadruplet>
これをrparsecのパーサに落としていく。
まず、数字の部分。文字列を受け取って数値を返す。
# <digit> ::= "一" | "二" | "三" | "四" | "五" | "六" | "七" | "八" | "九" digits = Constants::DIGITS.split(//).map.with_index { |c, i| RParsec::Parsers.char(c).map {|c| 1 + i} }.inject(RParsec::Parsers.zero) {|result, p| result.plus(p)}
「一」〜「九」それぞれをchar(c)で1文字読み取りパーサにする。
「map {|c| 1 + i}」というは「一」を受け取ったら1を、「二」を受け取ったら2を、……返すパーサにするということ。
最後にinjectでplusしていって、「一」〜「九」を一つのパーサにしている。
「一〜九千九百九十九」の部分。
# <quadruplet> ::= <digit>? <class> <quadruplet>? | <singlet> quadruplet = nil lazy_quadruplet = RParsec::Parsers.lazy{quadruplet} quadruplet = RParsec::Parsers.sequence(digits.optional(1), classes, lazy_quadruplet.optional(0)) { |digit_value, class_value, next_value| digit_value * class_value + next_value } | singlet
再帰を表現するためにlazy()というのを使っている。rparsecのチュートリアルにもある慣用的な表現っぽい。
sequence(...){}の部分は、トークンの並びとその値の表現になっている。上の場合だと、「
optional()というのは、「千百」とかのように前に
「|」はor接続(選択)で、plusと同じ意味。
数全体の部分は同じなので省略。
まあパーサぐらい使えるようになっておこうかという気持ちで始めつつも、字句解析と構文解析の区別も無く、構文解析と評価の区別もあるような無いような感じで、自分の勉強になったかも微妙かも。
*1:下のだと空文字列を許してしまうので本当はもう少し複雑