Common Lisp で漢数字プリンタをちまちまと作っていたのですが ( Common Lisp 漢数字プリンタの試みのメモ - y2q_actionman’s ゴミクズチラ裏 )、ついに終わらせてしまうことにしました。

y2q-actionman/jp-numeral github.com

くだらない機能を盛り込んだので個人的には満足です。

上は「無量大数」、下は「清浄」の桁まで出ちゃう。

分数を入れたら「○○分の△△」と出す。

大字や旧字体も使い放題

0.123 を「一割二分三厘」と出す機能

120.30 を「百二十円三十銭」と出す機能

quicklisp に「入れとくれ」と依頼もしたので、そのうち quicklisp でも使えるようになると思います。

というわけでここからは、実装時にあったことのメモを書きます。

浮動小数点数プリンタはやはり面倒だった

「0.1の桁で 分 を出そう」とか思い始めたのが運の尽きで、浮動小数点数をきちんと印字したいという気になりました。 「 rationalize とかあるから、そんな苦労することないかな・・」という浅い考えはあっさり破られて、すぐに誤差に悩まされ始め、やはり正しいアルゴリズムを実装しないといけないな、という結論に。

そして・・面倒になってしまったので、 Lisp の format で浮動小数点数を文字列に変えて、その文字列を読み直すという、安易な二度手間っぽい作戦で終わらせてしまいました。

format の ~F 指示子の第二引数

format を使うことにしたのはいいのですが、意外と細かい仕様を知らなくて困りました。

その一つが、 ~F 指示子の第二引数でした。 ~F 指示子は、固定小数点表現で書き出すための指示子であり( C の printf で言うところの f に相当)、この第二引数は「小数点の後に、いくつの数字を置くか」を指定するものです。

なんと、この引数に負の値を渡すことができて、その解釈は処理系によって異なるようなのです:

式 Allegro CL 9.0 SBCL 1.0.55 (format nil "~,v,F" 2 12.35) "12.35" "12.35" (format nil "~,v,F" 1 12.35) "12.4" "12.4" (format nil "~,v,F" 0 12.35) "12." "12." (format nil "~,v,F" -1 12.35) "10.0" "12.0" (format nil "~,v,F" -2 12.35) "0.0" "12.0" (format nil "~,v,F" -3 12.35) "00.0" "12.0" (format nil "~,v,F" -4 12.35) "000.0" "12.0"

当然ながら、 0 以上の値を与えたときの挙動は同一です。 一方、負の値を与えた場合、 SBCL では小数点で丸めているように見えるのに対して、Allegro CL は小数点より上の桁を刈り込むという挙動をします。

まあ・・今回は 0 以上の値の場合にしか興味がないので、負の場合については適当にあしらっています。

format の ~F 指示子の第三引数

format の知らなかった仕様その2です。

~F 指示子は第三引数を取ることが出来、これが指定されると、与えられた値の 10^(第三引数の値) 倍の値を代わりに印字します。 C の printf にはない機能です。

これは「割」の出力を実装するのに便利そうだ、と使ってみたはいいのですが、ここにも知らない仕様がありました。

~F 指示子は、第一引数(出力する文字数を指定する)と第二引数(前述。小数点の後の文字数を指定)の両方が省略された場合、 「 prin1 と同様の ordinary free-format output を使う」(CLHS: Section 22.3.3.1) とあります。

なんと Allegro CL では、この ordinary free-format に落ちると、この第三引数が効力を発揮しないのです。

;; 小数点以下2桁を出すよう指定。 CL-USER> (format nil "~,2,F" 12.35) "12.35" ;; scale 100 倍 CL-USER> (format nil "~,2,2F" 12.35) "1235.00" ;; 小数点以下の桁数の指定を外してみる。 CL-USER> (format nil "~,,F" 12.35) "12.35" ;; scale 100 倍・・？ CL-USER> (format nil "~,,2F" 12.35) "12.35"

SBCL は、なんか効いたりします:

;; 小数点以下2桁を出すよう指定。 * (format nil "~,2,F" 12.35) "12.35" ;; scale 100 倍 * (format nil "~,2,2F" 12.35) "1235.00" ;; 小数点以下の桁数の指定を外してみる。 * (format nil "~,,F" 12.35) "12.35" ;; scale 100 倍・・？ * (format nil "~,,2F" 12.35) "1235.0"

以上のように釈然としないのですが、まあとにかく第一引数か第二引数のいずれかでもあれば、予想通りの挙動をしそうなので、その辺を上手く指定するようにして誤魔化しています。

無限大とか非数とか

浮動小数点数が無限大の時に、文字通り「無限大」とか出力したかったのですが、 ANSI Common Lisp には、数が無限大や無限小、非数かどうかを判定する関数がありません。 infinityp とか nanp なんて、いかにもありそうなんですが、処理系ごとの拡張で用意されているのみなのです。

なんか調べて互換性を維持するのも面倒なので、あっさりと止めてしまいました。

「そんなの C 言語なら当たり前にできるのに・・」と思ったんですが、同様のことをする関数が導入されたのは C99 になってからだったということも今日知りました (Man page of FPCLASSIFY) 。

1994 年制定の ANSI Common Lisp にないのも仕方ないのかなとも思いますが、弱点なのは確かな気がします。 *1