数を表すフランス語の文字列を数値に変換する(0-99)

(convert-inv は invert という名前でしたが invert に「逆変換する」という意味はないっぽいので名前を変えましたよ*1。 2018.9.2)

数をフランス語の文字列表現に変換する (0-9223372036854775807)の続きのようなものです。もう限界まで変換したので*2今度は逆変換プロシージャを定義します。つまり数のフランス語名から数そのものを求める訳です。

数のフランス語名をそのまま処理するのは大変なので、単語のリストに変換します。Scheme の処理系によっては string-split*3 が使えますが、JScheme には string-split がなくて似たようなことができる crack があるのでそれを使います。

(use-module "elf/iterate.scm")
(crack "neuf cent quatre-vingt-dix-neuf" " -")
 => ("neuf" "cent" "quatre" "vingt" "dix" "neuf")

*4

convert-inv<100、convert-inv<1000、convert-inv<10^6、... を定義していきましょう。
これらはそれぞれ、「100未満の数を表す言葉を、その言葉が表す数に変換するプロシージャ」、「1000未満の数を表す言葉を、その言葉が表す数に変換するプロシージャ」、「106未満の(略)」、... です。
これらのプロシージャには、数を表す文字列を単語のリストに変換したものが渡されます。

最初は、convert-inv<100 です。100未満の数を表す言葉は、100以上の数を表すときにもその一部を表すのに使われるので、convert-inv<100 は1段階大きな数を処理できる convert-inv<1000 から呼び出されます。convert-inv<100 に渡されるリストは次の3種類です。

  1. 空リスト
  2. 先頭の1個以上の要素から100未満の数を表す言葉を構成できるリスト
  3. 先頭の1個以上の要素から100未満の数を表す言葉を構成できないリスト

1 は例えば ("deux" "cent") というリストの "cent" まで読み終えた残りの部分が渡された、というような場合です。200という数の00の部分なので0を返せばよいでしょう。
3 は("deux" "cent" "mille") のようなリストの "cent" まで読み終えた残りの部分が渡されるのが典型的な場合なので、やはり0を返すのがよいと思います*5
2 は、普通に計算しましょう。

変換結果のほかに、リストをどこまで読んだかの情報もあると便利です*6
多値を使って、変換結果に加えてリストの未読部分も返すようにしましょう。

多値を(簡単に)扱うために receive を定義します*7

(define-macro (receive formals expression . body)
  `(call-with-values (lambda () ,expression) (lambda ,formals . ,body)))

それから、100未満の数を表す言葉を構成するのに使う単語のベクターを前に定義しました。

(define fst17s
  #("zéro" "un" "deux" "trois" "quatre" "cinq" "six" "sept" "huit" "neuf"
    "dix" "onze" "douze" "treize" "quatorze" "quinze" "seize"))
(define mul10s #("" "dix" "vingt" "trente" "quarante" "cinquante" "soixante"))

これをもとにして、単語から数を参照できるように、単語と数のペアのリスト(連想リスト)を作ります。

(define fst17s-inv
  (let recur ((i 0))
    (if (>= i (vector-length fst17s)) '()
      `((,(vector-ref fst17s i) . ,i) . ,(recur (+ i 1))))))

(define mul10s-inv
  (let recur ((i 0))
    (if (>= i (vector-length mul10s)) '()
      `((,(vector-ref mul10s i) . ,(* 10 i)) . ,(recur (+ i 1))))))

以下のような使い方をします。assoc は、リストの中に第1引数と一致する car を持つペアがあればそれを返し、なければ真理値の偽を表す #f を返します。

(assoc "six" fst17s-inv)
 => ("six" . 6)

(assoc "trente" mul10s-inv)
 => ("trente" . 30)

(assoc "quatre-vingt" mul10s-inv)
 => #f

で、やっと定義を始めるわけですが、80から上はややこしいので、まず80未満の数を表す言葉を変換する部分を作りましょう。

選択肢が増える予定なので (if (null? lis) …) ではなく cond を使っています*8。 文字列を単語リストに変換するときに "et" も取り除くつもりなので、ここには "et" を読んだ場合の処理はありません。

(define (convert-inv<100 lis)
  (cond ((null? lis) (values 0 '()))
        (else (let ((maybe-n*10 (assoc (car lis) mul10s-inv)))
                (if maybe-n*10
                  (receive (val rest) (convert-inv<100 (cdr lis))
                    (values (+ (cdr maybe-n*10) val) rest))
                  (let ((maybe-in-fst17s (assoc (car lis) fst17s-inv)))
                    (cond (maybe-in-fst17s
                           (values (cdr maybe-in-fst17s) (cdr lis)))
                          ((string=? (car lis) "une") (values 1 (cdr lis)))
                          (else (values 0 lis)))))))))

以下は上のコードの日本語訳です。

空リストの場合は70行くらい前*9に書いたように0を返します。

そうでない場合は、まず、10の倍数を表す単語を読み取ろうとします。
読み取れた場合は続けて、「10の倍数」が60のときは20未満の数を表す単語列を、それ以外は10未満の数を表す単語列を、リストの残りの部分から読み取ればよいのですが、面倒なのでまとめて convert-inv<100 の再帰呼び出しで読ませて(読もうとさせて)います。10の倍数を表す単語だけで、convert-inv<100 の対象となる単語列が終わることもあります。その場合は、「読もうとさせ」た結果は0です。10の倍数を表す単語を読み取れた場合の返り値の1つはここで読み取った「10の倍数を表す単語」を数値に変換したものと再帰呼び出しから返ってきた数値(「読もうとさせ」た結果)の和であり、もう1つは、再帰呼び出しから返ってきたリストです。
読み取れなかった場合は、17未満の数を表す単語を読み取ろうとします*10
17未満の数を表す単語が読み取れた場合は、convert-inv<100 の対象となる単語列はその単語で終わりなので、対応する値と、読み取った単語を取り除いたリストとを返します。
読み取れなかった場合は、0と、もとのままのリストを返します(再帰呼び出しで「読もうとさせ」た結果が0になるのもこの場合です)。
(日本語訳ここまで)

80以上を含む場合は、先頭が "quatre" のとき先頭部分の単語列*11が表す数が4である可能性と80以上である可能性があります。
次の単語が存在してそれが "vingt" か "vingts" なら80以上であり、そうでない場合は4です。
"vingt" のときは続いて20未満の数を表す単語列がある可能性があるので、(他の)10の倍数のときと同じように convert-inv<100 を再帰呼び出しします。
"vingts" のときは続きはないので、80と、先頭の2語("quatre"と"vingts")を除いたリストとを返します。
どちらでもない場合は、4と、先頭の"quatre" を除いたリストとを返します。

完成形です。

(define (convert-inv<100 lis)
  (cond ((null? lis) (values 0 '()))
        ((string=? (car lis) "quatre")
         (cond ((null? (cdr lis)) (values 4 '()))
               ((string=? (cadr lis) "vingts") (values 80 (cddr lis)))
               ((string=? (cadr lis) "vingt")
                (receive (val rest) (convert-inv<100 (cddr lis))
                  (values (+ 80 val) rest)))
               (else (values 4 (cdr lis)))))

        (else (let ((maybe-n*10 (assoc (car lis) mul10s-inv)))
                (if maybe-n*10
                  (receive (val rest) (convert-inv<100 (cdr lis))
                    (values (+ (cdr maybe-n*10) val) rest))
                  (let ((maybe-in-fst17s (assoc (car lis) fst17s-inv)))
                    (cond (maybe-in-fst17s
                           (values (cdr maybe-in-fst17s) (cdr lis)))
                          ((string=? (car lis) "une") (values 1 (cdr lis)))
                          (else (values 0 lis)))))))))

簡単なテストをします。5つのプロシージャ呼び出しのうち上の4つは正書法から生成し得るリストですが、最後のは違います。convert を使ってチェックすることが可能なのでこういうのまで読めてしまっても気にしません。

(convert-inv<100 '("douze"))
(convert-inv<100 '("cinquante" "une"))
(convert-inv<100 '("soixante" "dix" "huit"))
(convert-inv<100 '("quatre" "vingt" "seize"))
(convert-inv<100 '("dix" "trente" "neuf"))

12
()
51
()
78
()
96
()
49
()

長くなったので続きはまた今度。

*1:名前決める前に辞書くらいひけや。

*2:ということはなくて実数型だともっと大きくできたり多倍長という手もあったりしますけど。

*3:リンク先は SRFI ですが R7RS にももっと多機能なのがあります。

*4:これだと正書法と異なる入力も受け付けてしまいますが、最後に出てきた数を convert に渡せば比較してチェックできるのでここでは気にしません。

*5: ("cent" "trente") がそのまま渡されたときなども該当するのですが、この場合も0を返します。話がややこしくなるので説明は convert-inv<1000 を定義するときにします。

*6:どう便利なのかは convert-inv<1000 辺りの定義を見ればわかると思いますがこのエントリではまだ出てきません。

*7:JScheme で receive をこのように定義するのはまずい場合があるのですが、ここでの使い方なら問題ありません。

*8:実際には完成形から80以上を処理する部分を取り除いただけです。

*9:行数はてけとー

*10:女性名詞を修飾する数は、最後が1の場合 "un" ではなく "une" なので、単語が "une" であるという分岐も入れました。

*11:長さ1の場合も列と呼ぶことにします