$Id: index.html,v 1.7 2005/08/26 12:46:50 aamine Exp $
2005 年度 LLDN (Lightweight Language Day and Night) 「キミならどう書く」規定部門 Ruby 編のコードを解説します。
青木峰郎 (日本Rubyの会)
Ruby標準添付ライブラリメンテナ。 主著『Rubyソースコード完全解説』 (インプレス)、 『ふつうのLinuxプログラミング』 (ソフトバンクパブリッシング)。
~/c/lldn % ruby -Ke jncalculator.rb JCALC> JCALC> 0 〇 JCALC> 1 一 JCALC> 1+1 二 JCALC> 一+〇 一 JCALC> 一万+百二十三 一万百二十三 JCALC> 一無量大数+一 一無量大数一 JCALC> 〇−一 負一 JCALC> 三×四 十二 JCALC> 三×四+五 十七 JCALC> 三×(四+五) 二十七 JCALC> 五+四×三 十七 JCALC> 一÷二 〇余 JCALC> quit ~/c/lldn %
プログラムは大きく二つに分けられる。
前者の、数式の解析には Racc というパーサジェネレータを使う。パーサジェネレータとは、 数式やプログラムのような構造のあるテキストを解析するためのツールである。
後者の、日本語表記と数値の相互変換については地道にコードを書くことにする。
数式の解析には、述べたように Racc を使う。 Racc は、文法の規則を BNF (以下参照) で与えると、 それを解析するための Ruby プログラム (パーサ) を生成してくれるツールである。 解析する、というのは、例えば電卓ならば文字の並びを計算に変換することだ。 以下の BNF を見てみると '+' だの '*' だのと書いてあって、いかにも電卓ぽい。
[日本語電卓のBNF]
stmt : expr | /* empty */ { nil } expr : expr '+' expr { val[0] + val[2] } | expr '-' expr { val[0] - val[2] } | expr '*' expr { val[0] * val[2] } | expr '/' expr { val[0] / val[2] } | '(' expr ')' { val[1] } | NUM | '-' NUM =UMINUS { -(val[1]) }
ただし、いま「文字の並びを計算に変換」と言ったが、 Racc が直接扱うのは文字ではなく、それを一段階処理した「記号」の列だ。 記号というのは、単語と、その値をセットにしたもののことである。 例えばこの計算機では "1+2" という文字列を「NUM (1)」「+ (+)」「NUM (2)」 という三つの記号の並びに変換して処理する。
そしてこの記号の列を作るのがスキャナ (字句解析器) である。 スキャナは StringScanner (strscan) という標準添付ライブラリを使って書く。 このクラスを使うと文字列を先頭から正規表現で切り出していくことができる。 [jncalculator.y: JNCalculator#yyparse]
次に数値の日本語表記について。 今回は桁数が途中で変化するため非常にややこしい。 そこでコンフィギュレーションをオブジェクトとして持ち、 柔軟に設定できるようにした。
仕様をオブジェクトにマップするためには、 まずはパターン (同じようなものの並び) を見付けるのが肝心だ。 そう考えるとまず見付かるのは、 仕様にある「〜兆」「〜億」「〜万」という単位ごとの区切りである。 例えば「三億十万五十二」ならば「三億」と「十万」と「五十二」という区切りである。 このまとまりを Component と呼んでおくことにする。
それぞれの Component は「○千○百○十○ (単位)」という形になっている。 最後の (一番小さい位の) Component を「兆」や「億」のような単位がない Component だと考えれば、 すべての日本語表記数値は「Component の列」と見ることができる。
さらに考えると、各 Component の「○千○百○十○」の部分は 一番小さな位の Component と同じだから、 これも共有できてもよさそうな気がする。
……というように考えていき、最終的に次のような構造になった。
Component クラスをデザインパターンを使って表現すると、 構造は Composite で処理は Chain of Responsibility、 そしてすべて合わせて見ると Interpreter だと考えられる。