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

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

前回は、100未満の数を表す言葉を構成する単語のリストから "et" を取り除いた*2もの*3を、もとの言葉が表す数に変換する convert-inv<100 というプロシージャを定義しました。

convert-inv<100 の定義と convert-inv<100 を実行するのに必要な定義を再掲します。

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

(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))))))

(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)))))))))

convert-inv<100 には、("soixante" "onze" "mille" "cent" "vingt" "huit") のようなリストも渡すことができ、このリストの場合は71と("mille" "cent" "vingt" "huit")の2つの値が返ってきます。
このように、convert-inv<100 は単体で100未満となる数だけでなく、より大きな数を表す言葉の、100未満を表す一部分も変換することができます*4(このリストは71128を表す "soixante et onze mille cent vingt-huit" を「構成する単語のリストから "et" を取り除いたもの」です)。

100未満の数は、直接には、1000未満の数の一部となります。従って、convert-inv<100 は convert-inv<1000 から(直接)呼び出されます。
convert-inv<1000 を定義しましょう。convert-inv<1000 に渡されるリストを次の2つに分類して定義を考えます。

  1. 「100以上1000未満の数を表す言葉を構成する単語のリストから "et" を除いたもの」の末尾に0個以上の単語を付け加えることによって作れるリスト。
  2. 「100以上1000未満の数を表す言葉を構成する単語のリストから "et" を除いたもの」の末尾に0個以上の単語を付け加えることによって作れないリスト。

1 は例えば ("neuf" "cent" "quatre" "vingt" "dix" "neuf" "mille" "soixante") のようなリストです。(ちなみに999060です)
2 は ("quatre" "vingt" "dix" "neuf" "mille" "soixante") や ("mille" "soixante") や () のようなリストです。

convert-inv<1000 がまともに仕事をするのは 1 の場合です。"cent" の左側(上の例では "neuf")が表す数を100倍し、右側(上の例では "quatre" "vingt" "dix" "neuf")が表す数を足すのが基本的な仕事です*5。2 の場合は、渡されたリストをそのまま convert-inv<100 に渡し、返ってきた結果をそのまま返せばよいです。

1 か 2 かは、リストを convert-inv<100 に渡すと簡単にわかります。convert-inv<100 から返ってきたリストの先頭が "cent" か "cents" なら 1、それ以外のときは 2 です。

ほかにも細かい場合分けはありますがあとはコード*6で。

(define (convert-inv<1000 lis)
  (receive (val rest) (convert-inv<100 lis)
    (cond ((null? rest) (values val '()))
          ((string=? (car rest) "cents") (values (* val 100) (cdr rest)))
          ((string=? (car rest) "cent")
           (receive (val2 rest) (convert-inv<100 (cdr rest))
             (values (+ (if (= val 0) 100 (* val 100)) val2)
                     rest)))
          (else (values val rest)))))

convert-inv<100 から返ってきたリストの先頭が "cent" で、返ってきた数値が0のとき、lis は "cent" から始まるリストです。つまり100から199のどれかの数を表す言葉を構成する単語の列が先頭にあるようなリストです。
このとき、0と100をかけるわけには行かないので数値が0かどうかで場合分けしています*7

convert-inv<10^6 を定義しましょう。convert-inv<10^6 に渡されるリストを次の2つに分類して定義を考えます。

  1. 「1000以上106未満の数を表す言葉を構成する単語のリストから "et" を除いたもの」の末尾に0個以上の単語を付け加えることによって作れるリスト。
  2. 「1000以上106未満の数を表す言葉を構成する単語のリストから "et" を除いたもの」の末尾に0個以上の単語を付け加えることによって作れないリスト。

convert-inv<1000 を定義したときと同じで 1 の場合は "mille" の左側を1000倍して右側と足せばよく 2 は convert-inv<1000 にリストをそのまま渡して結果をそのまま返せばよくこの2つはリストを convert-inv<1000 に渡すことによって簡単に区別できるようになりコードは以下のように書けます。

(define (convert-inv<10^6 lis)
  (receive (val rest) (convert-inv<1000 lis)
    (if (and (pair? rest) (string=? (car rest) "mille"))
      (receive (val2 rest) (convert-inv<1000 (cdr rest))
        (values (+ (if (= val 0) 1000 (* val 1000)) val2)
                rest))
      (values val rest))))

簡単にテストしましょう。
ここまでの3つのコード(前回の再掲分、convert-inv<1000、convert-inv<10^6)をいつも通り Schemoid に貼りつけて Eval した上で、
"trois cent quatorze mille cent cinquante-neuf" を構成する単語のリストを convert-inv<10^6 で変換してみます。

(convert-inv<10^6 '("trois" "cent" "quatorze" "mille" "cent" "cinquante" "neuf"))

314159
()

うまくいってますね。

最後まで行く予定でしたが長くなりそうなのでここで一旦切ります。

*1:この記事もプロシージャ名最初は invert<x でした。

*2:"et" をあらかじめ取り除いておくのは、イレギュラーな入力に convert-inv<x の中で対処したくないからです。正しい入力であれば、"et" の後ろに必ず "un" か "une" か "onze" があります。なので、正しい入力なら "et" を読んだらそのまま次の単語を読むことができるわけです。しかし、実際には、"et" で終わるリストなんてものも考慮しなければなりません。そうすると、null? 等でチェックしてからでないと次の単語が読めません。そういうことをしたくない(したくないだけで、しちゃいけない理由が何かあるわけではないです)ので先に "et" を取り除いておくわけです。(こういう理由だから "une" は "un" にせずそのままなんですが普通に考えたらわかりにくいですね)

*3:後ろに関係ない単語たちが続いていてもよい

*4:そうできるように作ったので。

*5:"cent" の左や右に言葉がない場合もあります。

*6:convert を用いたチェックを詳しくやりたい場合—例えばどこを間違えてるかメッセージを出すとかそういう場合は、 「"cents" に数詞を続けるべきではないのに続けてしまう間違い」よりも「"cent" と "cents" の間違い」のほうがありそうなので、"cent" と "cents" を区別しないほうが、的確なメッセージを出せるかもしれませんがそこまでやる予定はありません。"vingt" と "vingts" も分けちゃってますし。

*7:(どうでもいい話だと思いますが、説明すると書いてしまったので説明します)convert-inv<100 で、渡されたリストの先頭が "cent" だったときに1を返すなんて仕様を考えたりもしました。そうすると ("mille" …) でも1を返さないとちぐはぐな感じがします(convert-inv<100 からの復帰先の convert-inv<1000 からの復帰先の convert-inv<10^6 内で1を1000倍して結果が1000になる)。しかし、これは convert-inv<10^6 から convert-inv<1000 に渡されたリストがそのまま convert-inv<100 に渡された場合でないと間違った結果を返してしまいます。例えば、("cent" "mille") だった場合も、convert-inv<100 には、convert-inv<1000 によって "cent" が取り除かれて、("mille") が渡されます。このときに1(と ("mille"))を返してしまうと、convert-inv<10^6 は、101000 を返してしまいます。正しい結果は100000です。なので、("mille" …) の場合は1を返すわけにはいきません。"cent" も "mille" も左に何もないときは、1つの100や1つの1000を意味するというルールは同じなのに、「1を返すのは "cent" のときだけ」という仕様はわかりにくいので、常に0を返すようにしました。(convert-inv<x の中で x 以上の数のことを気にするのがそもそもおかしい気もします)