前回(F#で数の読みを考える)、アラビア数字からなる数値の読みを出力するプログラムをF#で考えてみました。今度は、アラビア数字の数値文字列と漢数字文字列の変換を考えてみます。
まず、アラビア数字から漢数字への変換は、前回の位取りのある数値の読みを出力するプログラムを修正することで簡単にできます。音便などを考える必要もありません。
※垓と穣の間の「ジョ」はこのブログでは表示できないので、二つの漢字をつなげて「禾予」と表示しています。適切に表示でき、実行できる環境ならば、これを本来の文字で置き換えてください。
let san2kan num_str = let rec reverse = function | "" -> "" | x -> x.[1..] |> reverse |> (+) <| string x.[0] let rec loop rank str = let jusshin_kurai = function | 0 -> "" | 1 -> "十" | 2 -> "百" | 3 -> "千" | _ -> "" let manshin_kurai = function | 1 -> "万" | 2 -> "億" | 3 -> "兆" | 4 -> "京" | 5 -> "垓" | 6 -> "禾予" | 7 -> "穣" | 8 -> "溝" | 9 -> "澗" | 10 -> "正" | 11 -> "載" | 12 -> "極" | 13 -> "恒河沙" | 14 -> "阿僧祇" | 15 -> "那由他" | 16 -> "不可思議" | 17 -> "無量大数" | _ -> "" let kazu_yomi = function | '2' -> "二" | '3' -> "三" | '4' -> "四" | '5' -> "五" | '6' -> "六" | '7' -> "七" | '8' -> "八" | '9' -> "九" | _ -> "" match String.length str > 3 with | false -> "" | true -> match manshin_kurai rank with | "" when rank > 1 -> failwith <| "エラー: 桁が多すぎます: '" + num_str + "'" | kurai -> let s = [| for i in 0..3 -> if (i = 0 || (i = 3 && String.length num_str<>4)) && str.[i] = '1' then "一" + jusshin_kurai i elif str.[i] = '0' then "" else kazu_yomi str.[i] + jusshin_kurai i |] loop <| rank + 1 <| str.[4..] |> (+) <| match s.[3]+s.[2]+s.[1]+s.[0] with | "" -> "" | x -> x + kurai match num_str with | "" -> "" | "0" -> "零" | x -> x |> reverse |> (+) <| "000" |> loop 0 [<EntryPoint>] let main args = let routine line = let number = String.collect( fun x -> if x = ',' then "" else string x ) line if not ( String.forall( fun x -> System.Char.IsDigit x ) number ) then failwith <| "エラー: 数字以外の文字があります: '" + line + "'" number |> san2kan |> printfn "%s" try match args.Length with | 0 -> let line = System.Console.ReadLine() routine line | 1 -> routine args.[0] | _ -> failwith "エラー: 引数はひとつだけ指定してください" 0 with Failure msg -> eprintfn "%s" msg eprintf "機 能: 算用数字を漢数字に変換する" -1
以上のようになります。今回は各桁の値は配列に保持することにし、for文を使ったシーケンス式で値を計算しています。前回は説明していませんでしたが、百と十の位は値が一のときはその表記を省略しています。千は、万や億などに属する場合は省略せずに一千と書き、単純な千のときは省略するようにしています。
今度は、アラビア数字を漢数字の数値に変換する方法を考えてみます。
これは漢数字の階層構造に目を付けて考えていきます。漢数字に現れる単語は、重複するものもありますが、一つずつ増えていくものと、十倍ずつ増えていくものと、万倍ずつ増えていくものの三つのグループに分類できます。この三者の階層構造によって漢数字は組み立てられています。このことを踏まえて、再帰を使って記述すると、次のプログラムができます。
open System.Text.RegularExpressions let convert_kan2san input = let separate input pattern = let m = Regex.Match(input, "(.*?)" + pattern + "(.*)") if m.Success then Some (m.Groups.[1].Value, m.Groups.[2].Value) else None let place_digit = function | "" -> "1" | "一" -> "1" | "二" -> "2" | "三" -> "3" | "四" -> "4" | "五" -> "5" | "六" -> "6" | "七" -> "7" | "八" -> "8" | "九" -> "9" | _ -> failwith "エラー:正規の漢数字ではありません:" + input let place_4digits skip data = let rec place_digits skip str rank = match [|"千";"百";"十";""|].[rank] with | "" -> if skip || str <> "" then place_digit str else "0" | kurai -> match separate str kurai with | Some (left, right) -> (place_digit left) + place_digits false right (rank+1) | None -> (if skip then "" else "0") + place_digits skip str (rank+1) place_digits skip data 0 let rec place_digits skip str rank = match [| "無量大数";"不可思議";"那由他";"阿僧祇";"恒河沙"; "極";"載";"正";"澗";"溝";"穣";"禾予";"垓";"京";"兆";"億";"万";""|].[rank] with | "" -> if skip || str <> "" then place_4digits skip str else "0000" | kurai -> match separate str kurai with | Some (left, right) -> (place_4digits skip left) + place_digits false right (rank+1) | None -> (if skip then "" else "0000") + place_digits skip str (rank+1) if input = "" || input = "零" then "0" else place_digits true input 0 [<EntryPoint>] let main args = let routine line = line |> convert_kan2san |> printfn "%s" try match args.Length with | 0 -> let line = System.Console.ReadLine() routine line | 1 -> routine args.[0] | _ -> failwith "エラー: 引数はひとつだけ指定してください" 0 with Failure msg -> eprintfn "%s" msg eprintf "機 能: 漢数字を算用数字に変換します" -1
位を表す漢字を見つけるための関数として、separateを定義しています。戻り値の値をオプション型にし、見つかったときは位の文字の左と右の文字列からなるタプルを返します。見つからなかったときはNoneを返します。
ここでは再帰関数place_digitsを使って文字列を連結させながらの繰り返しを記述しています。この関数は十進の位と万進の位のそれぞれの処理用に同じ名前で別々に定義しています。最初のパターンマッチで位の配列が終わりかどうか判断させて、終わっていなければ次のパターンマッチで、先の関数separateを使い文字列中に現在対象としている位があるかどうか調べています。
この一連の処理で問題となるのが、位の文字が存在しないときの処理です。解析中の漢数字中の最高位の位が見つかるまでは、文字列中に現れない万進の位も十進の位も空文字列に変換されます。しかし最高位の位がどの万進と十進の位の組み合わせかが確定すると、今度は文字列中に現れない万進の位は”0000”、現れない十進の位は”0”に変換されます。つまり最高位が見つかったかどうかを示すものがどうしても必要になります。この状態を示す変数として、再帰で渡され続ける引数skipを使っています。
上記の十進の位と万進の位についての再帰を使った繰り返しは、ほとんど同じ処理をしています。これはクラスを使って一つにまとめることができます。まだ使ったことのないクラスを使う練習として参考までに書いてみます。
open System.Text.RegularExpressions type Converter( array, func, str ) = let ranks : string [] = array let subconvert = func let padding = str member this.place_digits(skip, str, rank) = match ranks.[rank] with | "" -> if skip || str <> "" then subconvert skip str else padding | kurai -> let separate input pattern = let m = Regex.Match(input, "(.*?)" + pattern + "(.*)") if m.Success then Some (m.Groups.[1].Value, m.Groups.[2].Value) else None match separate str kurai with | Some (left, right) -> (subconvert skip left) + this.place_digits(false, right, rank+1) | None -> (if skip then "" else padding) + this.place_digits(skip, str, rank+1) let convert_kan2san input = let rank_ju = [|"千";"百";"十";""|] let rank_man = [| "無量大数";"不可思議";"那由他";"阿僧祇";"恒河沙"; "極";"載";"正";"澗";"溝";"穣";"禾予";"垓";"京";"兆";"億";"万";""|] let place_digit skip = function | "" -> "1" | "一" -> "1" | "二" -> "2" | "三" -> "3" | "四" -> "4" | "五" -> "5" | "六" -> "6" | "七" -> "7" | "八" -> "8" | "九" -> "9" | _ -> failwith "エラー:正規の漢数字ではありません:" + input let place_4digits skip data = let c = Converter( rank_ju, place_digit, "0" ) c.place_digits( skip, data, 0 ) let c = Converter( rank_man, place_4digits, "0000" ) if input = "" || input = "零" then "0" else c.place_digits( true, input, 0 )
処理を共通にするために関数place_digitに意味のない引数skipを持たせます。思いのほか、クラスを作るのも使うのも難しくはありませんでした。ただ今回はクラスを作ることで、かえって処理の流れが見えにくくなってしまったようです。