漢数字を数値に変換する(rparsec版)

前の(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>

「一〜九千九百九十九」の数。を何回か(0〜3回)繰り返した後「一〜九」一個が付いたり付かなかったり*1

<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()というのは、「千百」とかのように前にが付かない場合などの表現で、optional(1)の場合、代わりに「1」と見なして計算する。
「|」はor接続(選択)で、plusと同じ意味。


数全体の部分は同じなので省略。


まあパーサぐらい使えるようになっておこうかという気持ちで始めつつも、字句解析と構文解析の区別も無く、構文解析と評価の区別もあるような無いような感じで、自分の勉強になったかも微妙かも。

*1:下のだと空文字列を許してしまうので本当はもう少し複雑