Common Lisp 漢数字プリンタ 実装終了
Common Lisp で漢数字プリンタをちまちまと作っていたのですが ( Common Lisp 漢数字プリンタの試みのメモ - y2q_actionman’s ゴミクズチラ裏 )、ついに終わらせてしまうことにしました。
くだらない機能を盛り込んだので個人的には満足です。
- 上は「無量大数」、下は「清浄」の桁まで出ちゃう。
- 分数を入れたら「○○分の△△」と出す。
- 大字や旧字体も使い放題
- 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
*1:C99 の浮動小数点環境アクセス機能 (Man page of FENV)なんて、ANSI CL で簡単には使えないという状況ですし・・。