2014年02月09日

F#で数の読みを考える

今年はF#をやっています。関数型のプログラミングは何年も前から興味はあって、本も買ってはみたものの、なかなか先に進めませんでした。でもつい最近、自分でも気づかないうちに、目から鱗が落ちたみたいです。PythonやJavascriptも好きですが、これもなかなか楽しく作れます。

入門用のプログラムで動作を確認するのも飽きたので、去年Pythonで作った日本語の読み文字列を出力するプログラムをF#で作ってみようと考えました。全体はまだ無理ですが、整数の読みを出力するぐらいなら何とかできました。コンソールアプリケーションとして使えるようにしてみます。

まず、Pythonでのコード。以前のものから無駄なところを修正したものです。標準入力の数字列を読み込んで、位取りをしないときと、したときの両方の読みの文字列を出力します。

import sys

def yomi_raretsu_kazu(num):
    yomi = {
        u'0':u'レイ',
        u'1':u'イチ',
        u'2':u'ニー',
        u'3':u'サン',
        u'4':u'ヨン',
        u'5':u'ゴー',
        u'6':u'ロク',
        u'7':u'ナナ',
        u'8':u'ハチ',
        u'9':u'キュー',
    }
    if not num:
        return u''
    for n, y in yomi.items():
        num = num.replace( n, y )
    return num

def yomi_integer(num):

    if num == None or num == u'':
        return u''

    if num == u'0':
        return u'ゼロ'

    manshin_kurai = {
        0:u'',
        1:u'マン',
        2:u'オク',
        3:u'チョー',
        4:u'ケー',
        5:u'ガイ'
    }

    kazu_yomi = {
        u'0':u'',
        u'1':u'',
        u'2':u'ニ',
        u'3':u'サン',
        u'4':u'ヨン',
        u'5':u'ゴ',
        u'6':u'ロク',
        u'7':u'ナナ',
        u'8':u'ハチ',
        u'9':u'キュー',
    }

    list = []
    rank = 0
    rev  = num[::-1] + u'000'
    while len(rev) > 3:

        if rank >= len(manshin_kurai):
            return yomi_raretsu_kazu(num)

        if rev[0] == u'1':
           s0 = u'イチ'
        else:
           s0 = kazu_yomi[rev[0]]

        if rev[1] == u'0':
           s1 = u''
        else:
           s1 = kazu_yomi[rev[1]] + u'ジュー'

        if rev[2] == u'0':
            s2 = u''
        elif rev[2] == u'3':
            s2 = u'サンビャク'
        elif rev[2] == u'6':
            s2 = u'ロッピャク'
        elif rev[2] == u'8':
            s2 = u'ハッピャク'
        else:
            s2 = kazu_yomi[rev[2]] + u'ヒャク'

        if rev[3] == u'0':
            s3 = u''
        elif rev[3] == u'1' and len(num) != 4:
            s3 = u'イッセン'
        elif rev[3] == u'3':
            s3 = u'サンゼン'
        elif rev[3] == u'8':
            s3 = u'ハッセン'
        else:
            s3 = kazu_yomi[rev[3]] + u'セン'

        if s3+s2+s1+s0 != "":
            list.insert( 0, s3+s2+s1+s0 + manshin_kurai[rank] )
        else:
            list.insert(0,"")
        rev = rev[4:]
        rank += 1
    return ''.join(list)

if __name__ == '__main__':
    item = sys.stdin.readline().rstrip('\n')
    print( yomi_raretsu_kazu( item ))
    print( yomi_integer( item ))

これを参考にF#でもやってみます。F#では2つのプログラムにしてみます。

まず、位取りしない方です。

let yomi_raretsu_kazu str =
    String.collect ( fun ch ->
        match ch with
        | '0' -> "レイ"
        | '1' -> "イチ"
        | '2' -> "ニー"
        | '3' -> "サン"
        | '4' -> "ヨン"
        | '5' -> "ゴー"
        | '6' -> "ロク"
        | '7' -> "ナナ"
        | '8' -> "ハチ"
        | '9' -> "キュー"
        |  _  -> "" ) str

とてもシンプルです。Pythonでは辞書型を使って実現しましたが、F#では、文字をパターンマッチするラムダ式を作って、その結果を高階関数のcollectで連結しました。F#の特徴的なものを使って簡潔に書けました。

この関数をコンソールアプリケーションにしてみます。引数が1つの時はそれを変換し、引数が何もないときは標準入力から文字列を取り込みます。引数が2つ以上になるとエラーです。

let yomi_raretsu_kazu str =
    String.collect ( fun ch ->
        match ch with
        | '0' -> "レイ"
        | '1' -> "イチ"
        | '2' -> "ニー"
        | '3' -> "サン"
        | '4' -> "ヨン"
        | '5' -> "ゴー"
        | '6' -> "ロク"
        | '7' -> "ナナ"
        | '8' -> "ハチ"
        | '9' -> "キュー"
        |  _  -> "" ) str

[<EntryPoint>]
let main args =
    let routine line =
        if not (String.forall(fun x -> System.Char.IsDigit x) line) then
            failwith <| "エラー: 数字以外の文字があります: '" + line + "'"
        line
        |> yomi_raretsu_kazu
        |> 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
            eprintfn "機 能: 位取りのない数字列の読みを返します"
            -1

エントリーポイントとして関数mainを定義しています。これは先ほど作った関数の呼び出しと、例外処理からなっています。この呼出のあたりは、わざわざパイプライン演算子「|>」を使って書いてます。理屈さえ分かれば、括弧を使って記述するより、処理の流れが明快に表現できるところが、面白いです。

次は、位取りをする方です。それなりに複雑になりますが、パターンマッチで書くと、見た目にも明快です。

let yomi_integer num_str =
    let rec reverse = function
        | "" -> ""
        | x  -> x.[1..] |> reverse |> (+) <| string x.[0]
    let rec loop rank str =
        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, manshin_kurai rank) with
        | (false, _    ) -> ""
        | (true,  ""   ) when rank > 1 ->
            failwith  <| "エラー: 桁が多すぎます: '" + num_str + "'"
        | (true,  kurai) ->
            let s0 =
                match str.[0] with
                | '1' when kurai <> "" ->
                    match kurai.[0] with
                    |'カ'|'ケ'|'コ'|'サ'|'セ'|'チ' -> "イッ"
                    | _ -> "イチ"
                | '6' when kurai <> "" ->
                    match kurai.[0] with
                    |'カ'|'ケ'|'コ' ->  "ロッ"
                    | _ -> "ロク"
                | '8' when kurai <> "" ->
                    match kurai.[0] with
                    |'カ'|'ケ'|'コ'|'サ'|'セ'|'チ' -> "ハッ"
                    | _ -> "ハチ"
                |  x  -> kazu_yomi x
            let s1 =
                match (str.[1], str.[0]) with
                | ('0',  _ ) -> ""
                | ( x , '0') when kurai <> "" ->
                    kazu_yomi x + match kurai.[0] with
                                  |'カ'|'ケ'|'コ'|'サ'|'セ'|'チ' -> "ジッ"
                                  | _ -> "ジュー"
                | ( x ,  _ ) -> kazu_yomi x + "ジュー"
            let s2 =
                match (str.[2],str.[1], str.[0]) with
                | ('0', _,  _ ) -> ""
                | ('3', _,  _ ) -> "サンビャク"
                | ('6', _,  _ ) -> "ロッピャク"
                | ('8', _,  _ ) -> "ハッピャク"
                | ( x ,'0','0') when kurai <> "" ->
                    kazu_yomi x + match kurai.[0] with
                                  |'カ'|'ケ'|'コ' -> "ヒャッ"
                                  | _ -> "ヒャク"
                | ( x , _,  _ ) ->  kazu_yomi x + "ヒャク"
            let s3 =
                match str.[3] with
                | '0' -> ""
                | '1' when String.length num_str <> 4 -> "イッセン"
                | '3' -> "サンゼン"
                | '8' -> "ハッセン"
                |  x  ->  kazu_yomi x + "セン"
            loop <| rank + 1 <| str.[4..]
            |> (+) <| match s3+s2+s1+s0 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
        |> yomi_integer
        |> 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

(高位における音便を考慮に入れていなかったので処理を追加、ついでにパターンマッチにタプルが使えることを覚えたので整理。2014/02/11修正)

これは、再帰関数の習作になりました。まず数値文字列を逆順にしたほうが添え字を桁の番号にできるので、反転用の関数reverseを作ります。これを再帰で定義します。Pythonほどではありませんが、文字列でスライスが使えるのは楽でいいです。ついでにパイプライン演算子を使って、括弧を使わない表記にしています。

次の関数loopも再帰です。日本語での数字の読みは万進法なので4桁ずつ区切って処理していますが、これを再帰を使って連結していきます。なお、繰り返しの最後で4桁に満たない可能性もあるので、この再帰関数を最初に呼び出す段階であらかじめ”000”をくっつけています。

中心になる関数yomi_integerそのものは数字だけしか受け付けませんが、入力文字列では桁区切りのコンマはあった方が見やすいので、それを呼び出す関数mainでその桁区切りのコンマを削除しています。

作り始めたばかりですが、この言語は面白いです。元々.NETの言語ですが、Monoのおかげで、Windows以外でも使えます。今回Visual Studioだけでなく、オープンソースのgithubにあるfsharpでも動作を確認しました。今までちょっとしたことはPythonで書いていましたが、しばらくはF#です。

参考リンク:



posted by takayan at 02:08 | Comment(0) | TrackBack(0) | プログラミング | このブログの読者になる | 更新情報をチェックする

2014年02月15日

F#で漢数字とアラビア数字の変換を考える

前回(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を持たせます。思いのほか、クラスを作るのも使うのも難しくはありませんでした。ただ今回はクラスを作ることで、かえって処理の流れが見えにくくなってしまったようです。



posted by takayan at 14:07 | Comment(0) | TrackBack(0) | プログラミング | このブログの読者になる | 更新情報をチェックする