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) | プログラミング | このブログの読者になる | 更新情報をチェックする
この記事へのコメント
コメントを書く
お名前: [必須入力]

メールアドレス:

ホームページアドレス:

コメント: [必須入力]


この記事へのトラックバック
×

この広告は90日以上新しい記事の投稿がないブログに表示されております。