今回のお題
こちらの記事 を見て、数字の1~1000を漢数字で出力する部分が練習にちょうどよさそうだなーと思ったのでやってみました。
以下のように「一」から「千」までカウントアップしていく関数を作ってみます。
一
二
三
(中略)
九
十
十一
(中略)
九百九十九
千
実現方法ですが、せっかくなので以下の記事で紹介されているparse関数を使ってやってみました。
Redではparse関数を使うことで構文解析したり、独自の構文(DSL)を作成したりできます。
今回の場合構文解析というほど大げさなものでもないというか、構文解析でやると余計複雑になりそうな気もしますが、まあ練習のためいうことで。
作ったコード
色々やってみた結果、最終的に以下のようなコードになりました。
Github - get-kanji-number.red
改行やコメントを分かりやすくたくさん入れていますが、慣れたらもっと減らしてもよいような気がします。
以下、コードの説明になります。
コード説明
数字と漢数字のハッシュの生成
まずは数字を漢数字に、桁数を漢数字に変換するためのハッシュ(hash!)を作成します。
hash!のインスタンスを作成する時は、make hash! [ ] でblockの中にキーと値のペアを記載します。
; 数字から漢数字に変換するためのハッシュ
kansuji-map: make hash! [
#"0" ""
#"1" "一"
#"2" "二"
#"3" "三"
#"4" "四"
#"5" "五"
#"6" "六"
#"7" "七"
#"8" "八"
#"9" "九"
]
; 桁数の数値から漢字を取得するためのハッシュ
keta-map: make hash! [
1 ""
2 "十"
3 "百"
4 "千"
]
ちなみに#""で囲われている値はchar!型のリテラルになります。
あと前回言及するの忘れてましたがコメントは「;」セミコロンで開始します。
解析する文字セットの定義
次にparseで利用する文字セットをそれぞれ定義します。
; パースに使う文字セット
zero: #"0" ; 0
one: #"1" ; 1
one-to-nine: charset [#"1" - #"9"] ; 1から9
two-to-nine: charset [#"2" - #"9"] ; 2から9
ハイフンで区切っているものは「 左のchar! ~ 右のchar! 」までの範囲のセットを意味します。
数値を漢数字に変換する関数
上記の内容を使って与えられた数値を漢数字に変換する関数が以下になります。
; 数値から漢数字の表現に置き換える関数
get-kanji-number: func [num [integer!] return: [string!]] [
num-text: mold num ; 数値を文字列化
keta: (length? num-text) + 1 ; 文字列の長さ +1 に桁数のインデックスをセット。
; パース部分
rejoin parse num-text [ collect [any [
(keta: keta - 1) ; 1文字処理するごとに桁数を -1 する
zero ; 0の場合は常に何もしない
| set current one-to-nine end keep (select kansuji-map current) ; 1の位が 1 ~ 9 の場合の処理
| one keep (select keta-map keta) ; 1の位以外が1の場合の処理
| set current two-to-nine keep ( rejoin [ select kansuji-map current select keta-map keta] ) ; その他の場合
]
]
]
]
parseには構文ルールをblockで渡すのですが、これ自体がDSLになっていて、parse専用のキーワードが使えるようになっています。
長いので細かい部分は前述のRedのサイトも参照していただくとして、ざっくり雰囲気で説明しますと・・・
parseでは入力された数値を1文字ずつblockで指定されている条件分岐に合致しているかをチェックします。
今回の場合、条件分岐は以下の4つです。
zero ; 0の場合
| one-to-nine end ; 1~9で、かつ一番最後の文字の場合
| one ; 1の位以外が1の場合
| two-to-nine ; 1の位以外が2~9の場合
これに、漢数字を取得するための処理を追加したのが最終的なソースコードになります。
ポイントとなる箇所について、以下に簡単に説明します。
分岐の制御
基本的には事前に定義しておいた文字セットを「|」区切りで並べると、処理中の1文字がその文字セットにマッチした場合にその分岐の処理がされるようになっています。
また、特殊な条件として「end」を付けると「最後の文字の場合」だけその分岐に入ります。
今回の関数の場合2つめの分岐が「end」を指定しています。
処理中の文字を保持する
いくつかの分岐で時々出てきているset current XXXX という記載はcurrentというwordに現在処理している文字の値をセットするという意味になります。
currentというwordを後で( )の中で使うためにこうしています。
構文解析のエスケープ
parseのロジックでは前述のとおり、独自のDSLが機能しており、通常のRedのコードの構文解析は行われません。
通常のRedのコードとして何か処理をしたい場合、( )で括ることでparseの構文解析をエスケープしてRedのコードを動作させることができます。
( )内はparseのコンテキストと切り離されているので、parse中の文字を使うためには事前にsetキーワードで値を保持しておく必要があります。
文字の収集
parseの先頭にcollectと指定すると、後でkeepキーワードを使うことができます。
keepはその直後に指定されているデータを結果セットに追加するという機能を持っています。
今回の場合、数字が0の場合には何もする必要がない(感じでは表現されない)ので、zeroの分岐だけkeepが入っていません、
数字から漢数字への変換
解析中の数字を漢数字に変換する時は、事前に作っておいたハッシュを使います。
select hash! key で指定したハッシュからキーに対応する値を取得できるので、これで漢数字を取得しています。
漢数字の場合、桁数と数字によって微妙に読み方が変わってくるので、それぞれの分岐で正しい文字列が取得できるように処理を行っています。
総括
色々説明してない箇所がありますが長くなりすぎるのでこの辺で・・・
Redではparse関数を使うことで構文解析が行えますが、簡単なルールの構文であれば結構簡単にparseすることができます。
parseを使うことで複雑な分岐が簡潔に書けることがありますが、普通の関数呼び出しと比べてデバッグが大変になるので、使いどころはよく考えた方がよさそうだなーと実際やってみて思いました(笑)