y2q_actionman’s ゴミクズチラ裏

内向きのメモ書きを置いてます

Common Lisp 漢数字プリンタ 実装終了

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

github.com

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

  • 上は「無量大数」、下は「清浄」の桁まで出ちゃう。
  • 分数を入れたら「○○分の△△」と出す。
  • 大字や旧字体も使い放題
  • 0.123 を「一割二分三厘」と出す機能
  • 120.30 を「百二十円三十銭」と出す機能

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

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

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

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

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

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 で簡単には使えないという状況ですし・・。