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

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

前回は、0–999999の範囲の数を表す言葉(を構成する単語のリストから "et" を取り除いたもの)を(もとの)言葉が表す数に変換する convert-inv<10^6 というプロシージャを定義しました。

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

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

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

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

これまでと同じように、convert-inv<10^6 を使って109-1までの数を表す言葉をその言葉が表す数に変換するプロシージャを定義し、それを使って1012-1までの数を表す言葉をその言葉が表す数に変換するプロシージャを定義し、... と続けてもよいのですが、この段階で、1024-1までの数を表す言葉をその言葉が表す数に変換するプロシージャを定義することもできます*1

(define %10^6 1000000)
(define %10^9 1000000000L)
(define %10^12 1000000000000L)
(define %10^18 1000000000000000000L)

(define (convert-inv<10^24 lis)
  (receive (val rest) (convert-inv<10^6 lis)
    (cond ((null? rest) (values val '()))
          ((member (car rest) '("million" "millions"))
           (receive (val2 rest) (convert-inv<10^6 (cdr rest))
             (values (+ (* val %10^6) val2) rest)))
          ((member (car rest) '("milliard" "milliards"))
           (receive (val2 rest) (convert-inv<10^24 (cdr rest))
             (values (+ (* val %10^9) val2) rest)))
          ((member (car rest) '("billion" "billions"))
           (receive (val2 rest) (convert-inv<10^24 (cdr rest))
             (values (+ (* val %10^12) val2) rest)))
          ((member (car rest) '("trillion" "trillions"))
           (receive (val2 rest) (convert-inv<10^24 (cdr rest))
             (values (+ (* val %10^18) val2) rest)))
          (else (values val rest)))))

リストを次のように分類します。

  1. 106未満の数を表す言葉から作られたリスト
  2. 106以上109未満の数を表す言葉から作られたリスト
  3. 109以上1012未満の数を表す言葉から作られたリスト
  4. 1012以上1018未満の数を表す言葉から作られたリスト
  5. 1018以上1024未満の数を表す言葉から作られたリスト

(後ろに数と関係ない言葉が続いていても構いません(euro とか ans とか*2*3

リストがどれに該当するかは、convert<10^6 に渡して返ってきたリストの先頭要素を見ればわかります。 ただし 1 の場合は空リストになることがあります。
2 から 5 の場合は、返ってきた値と返ってきたリストの先頭要素が表す数をかけ合わせて、先頭要素を除いた部分の変換結果を足せば変換完了です。先頭要素を除いた部分は上の分類のどれかに該当する(または空リスト)ので「先頭要素を除いた部分の変換結果」は、convert<10^6 または再帰呼び出しによって求められます。

ちなみに、%10^6と%10^12と%10^18はそれぞれ「数をフランス語の文字列表現に変換する (0-9223372036854775807)」で、convert<10^9、convert<10^18、convert<10^24 を定義する際に定義したのと同じものですが、%10^9 だけは接尾辞つきに変えています。この数は最大999倍される可能性があって、接尾辞がないと桁溢れを起こすことがあるのです。(逆に convert のほうは接尾辞があっても困らないのでそれで統一します)

先頭部分が数を表す文字列をその部分が表す数に変換する convert-inv と、その部分が正書法の場合のみに対応する convert-inv-strict を定義しましょう。

(use-module "elf/iterate.scm")
(define (convert-inv+ s)
  (convert-inv<10^24
    (filter (lambda (s) (not (string=? s "et")))
            (crack (list->string (map* char-downcase s)) "- \n\t"))))
(define (convert-inv s) (receive (res rest) (convert-inv+ s) res))

(define (%convert-inv-strict s conv)
  (let* ((res (convert-inv s)) (s2 (conv res)) (len (string-length s2)))
    (if (and (<= len (string-length s))
             (char-ci=? (string-ref s 0) (string-ref s2 0))
             (string=? (substring s 1 len) (substring s2 1 len)))
      res
      #f)))

(define (convert-inv-strict     s) (%convert-inv-strict s convert))
(define (convert-inv-strict1990 s) (%convert-inv-strict s convert1990))

ここまでのコードは https://gist.github.com/brv00/1af994bc3a10ee3c8e03d4eb26195a65 にあります。

軽くテストします。

(convert-inv "soixante trente")
(convert-inv-strict "soixante trente")

90
#f

"soixante trente" は convert-inv によって90に変換されますが正書法ではそんな書き方はしない*4ので、convert-inv-strict では変換できません*5

convert-inv-strict は、convert-inv が返す値を convert によって変換した結果ともとの文字列の先頭部分が少しでも違うと #f を返します。それでは strict すぎる、という場合—例えば空白文字はいくつ続いていてもよく、ハイフンの前後に空白文字があってもよいと考える場合、次のような文字列変換プロシージャを通してから渡すという方法があります。

(define (unique-delimiter s)
  (do ((i (- (string-length s) 2) (- i 1))
       (dst `(,(string-ref s (- (string-length s) 1)))
            (let ((c (string-ref s i)))
              (cond ((char=? c #\-)
                     `(,c . ,(if (char-whitespace? (car dst)) (cdr dst) dst)))
                    ((char-whitespace? c)
                     (if (memv (car dst) '(#\- #\space)) dst `(#\space . ,dst)))
                    (else `(,c . ,dst))))))
      ((< i 0) (list->string (if (char-whitespace? (car dst)) (cdr dst) dst)))))

unique-delimiter は文字列の先頭およびハイフンの前後の空白文字をなくし、連続する空白文字を1つのスペースに置き換えます。

これを使えば次のようなファイルの中身*6も数に変換できます(/storage/emulated/0/scheme/ というフォルダに max-number.txt という名前で保存しています*7)。

                Neuf trillions     deux
            cent                vingt-
         trois                mille       trois
       cent soixante-      douze       billions
     trente       -six milliards      huit
    cent           cinquante-quatre millions sept
    cent          soixante        -quinze
      mille huit cent             sept
est le plus grand nombre des entiers signés 64 bits.

これを変換するためにファイルを読み込むので Scheme Droid を使います*8

以下は、Scheme Droid での変換の実行結果です。

">" の右から始まる式が入力した内容で、その下が返り値です。 ただし、1行目の > (load 'jscheme.init') はアプリ起動時に自動的に書き込まれます。

> (load 'jscheme.init')
File loaded successfully
> (load "storage/emulated/0/scheme/french-number.scm")
#t
> (define s
  (list->string (map* values
                      (lambda (f)
                        (call-with-input-file
                         "/storage/emulated/0/scheme/max-number.txt"
                         (lambda (p)
                           (do ((c (read-char p) (read-char p)))
                               ((eof-object? c))
                             (f c))))))))
"                Neuf trillions     deux\n            cent                vingt-\n         trois                mille       trois\n       cent soixante-      douze       billions\n     trente       -six milliards      huit\n    cent           cinquante-quatre millions sept\n    cent          soixante        -quinze\n      mille huit cent             sept\nest le plus grand nombre des entiers signés 64 bits.\n"
> (convert-inv s)
9223372036854775807L
> (convert-inv-strict s)
#f
> (convert-inv-strict (unique-delimiter s))
9223372036854775807L

french-number.scm の内容は https://gist.github.com/brv00/1af994bc3a10ee3c8e03d4eb26195a65 と同じです*9*10

map* は JScheme の elf で定義されている 総称関数で map とほとんど同じ機能を持ちますが、リスト以外にも適用できます。ただし、map のように複数のコレクションを受けとることはできません。「リスト以外」にはプロシージャも含まれます。このプロシージャは、プロシージャを引数として受け取ります。map* の第2引数にプロシージャが渡されると、map* は、このプロシージャに渡されるプロシージャ—ここでは f —に渡される値が渡される順番に並んだリストと同じように扱います。ここではファイル内の文字のリストのように扱うわけです。そのリストのように扱われるものが values によってマッピングされて文字の(通常の意味での)リストへと変換され、さらに文字列へと変換された結果がすぐ下の文字列であり、s がバインドする値です。

この文字列を convert-inv に渡すと est の直前までが普通に変換されます。しかし、単語間を自由に空けてしまったため、convert-inv-strict では変換できません。この問題を unique-delimiter で解消してから convert-inv-strict に渡すと、旧正書法に従うようになる*11ため、また変換できるようになります。

数と数を表すフランス語の文字列の変換の話は、とりあえずここまでにしておきます。

*1:実際には、桁溢れを起こすので1019弱程度までしか変換できません。

*2:年を表す場合、旧正書法では mille は mil でもよいのですが、対応はしてません。

*3:数を表すのに使われる言葉は続けないほうがよいです。変換プロシージャが雑すぎるので後ろに数を表すのに使われる言葉が来るとそこまで読み込んでしまって間違った結果を返すことがあるのです。例えば "un milliard et un billion" に対して1001000000000Lとか。実際のフランス語の文章でそういうことが起こるかどうかは知りませんが(もしかしたら起こるから新正書法ができたのかもしれません。後ろに "-" がないなら数はここまで、といえるように。完全にただの想像ですが)。こういう問題に対処するために、あとで convert で判定するのではなく、オートマトンのようなものを作って crack の前に正しい文字列を判定するほうがよいかもしれません。そのうちやるかもしれません。

*4:正書法でなくてもしないと思う。

*5:trente が直前に書かれている数の一部ではない場合はこれを誤りとされるのは理不尽です。こういう事態を避けるために、数を表すのに使われない単語に限って、あとに続けてもよいとしているわけです。実際にはあとに続く単語列の先頭が数を表すものでなければ大丈夫です。本当は条件はもっと緩いのですが正確に書こうとするとややこしくなります(仕様が決まっているわけではないので実装に基づいて条件を書いています)。

*6:est 以降が正しいフランス語かはわかりません。多分正しいと思うけど。

*7:はてなってファイル名添付できないのかな?

*8:コピペでも読めますが。

*9:フルパスで指定しないとだめっぽいです。

*10:use-module を使えば、URL も指定できるそうですが、Scheme Droid では無理っぽいです。

*11:多分最初から従ってたんだと思いますがよく知りません。