さて、プログラミングの話題もたまには書いてみます。今回はPHPのround関数の挙動が変だ！という話題です。

round()は浮動小数点数を四捨五入する関数で、大抵の言語に同じ名前で実装されているかと思います。ではPHPのround関数の何が問題なのか、ちょっと試してみましょう。

$ uname -sro Linux 2.6.9-42.0.10.plus.c4smp GNU/Linux $ php --version PHP 5.1.6 (cli) (built: Feb 23 2007 06:56:38) Copyright (c) 1997-2006 The PHP Group Zend Engine v2.1.0, Copyright (c) 1998-2006 Zend Technologies $ php -r '$x1=0.49999999999;$x2=0.5;var_dump($x1,$x2,($x1===$x2),round($x1),round($x2));' float(0.49999999999) float(0.5) bool(false) float(1) float(1) $

上記の通り、0.49999999999を四捨五入したら1になってしまいました。ここでクイズです。上記のような結果になった理由は何でしょうか。

上の結果を見た瞬間に「ああ、よくある浮動小数点数の精度とか誤差とかの話題か」と思った方は一定以上経験のあるプログラマなのだろうと思いますが、残念ながらハズレです。0.5は2進で正確に表せる数ですから、丸め誤差が発生するわけでもありません。また、PHPの浮動小数点数はCでいうdoubleそのものですから、10進11桁程度であればそれなりの精度で格納できます。

さて、答え合わせの前に、他の言語でも試してみることにしましょう。Rubyだとどうなるでしょうか。

$ ruby -e '$x1=0.49999999999;$x2=0.5;p($x1,$x2,($x1==$x2),$x1.round(),$x2.round());' 0.49999999999 0.5 false 0 1 $

なるほど。これは直感通りの結果です。念のためCでも試してみましょうか。

$ cat /tmp/round-test.c #include<stdio.h> #include<math.h> int main() { double x1=0.49999999999, x2=0.5; printf("%.11f

%.11f

%d

%f

%f

", x1, x2, x1==x2, round(x1), round(x2)); return 0; } $ gcc -lm /tmp/round-test.c $ ./a.out 0.49999999999 0.50000000000 0 0.000000 1.000000 $

やはり普通はそうなるはずですよね。

PHPのソースコードを少し眺めていると、すぐに不穏な個所にぶつかりました。

#define PHP_ROUND_WITH_FUZZ(val, places) { \ double tmp_val=val, f = pow( 10.0 , ( double ) places); \ tmp_val *= f; \ if (tmp_val >= 0.0 ) { \ tmp_val = floor(tmp_val + PHP_ROUND_FUZZ); \ } else { \ tmp_val = ceil(tmp_val - PHP_ROUND_FUZZ); \ } \ tmp_val /= f; \ val = !zend_isnan(tmp_val) ? tmp_val : val; \ } \

どうやらこのマクロがPHPのround関数の実体のようです。不思議なことに、ライブラリ関数のround(3)を呼ばずに謎の定数PHP_ROUND_FUZZとfloor(3)とceil(3)を使ってround関数らしきものを実装しているようです。round(3)を避ける意味がわかりません。更に不安なことに、少なくとも僕の手元の環境では定数PHP_ROUND_FUZZは0.50000000001なんていう不思議な数に定義されているみたいですよ！こんな見事なマジックナンバーは久々に見ました！ステキすぎてクラクラしちゃいますね！

皮肉はさておき下記のURLを読んでみると、こんな実装になっている理由がわかったような気がします。

この一連のバグ報告を斜め読みで要約すると、「紙とペンで計算すると5.045になるはずの値（実際にはコンピュータ上では約5.04499999999999992894573）を小数点以下第二位までで四捨五入してるのになぜか5.04になった！バグだ！」って騒いでいるプログラマがバグ報告をしてきて、これに対処するために四捨五入の境界値付近（0.00000000001くらいの差）だったら全部0から遠い方に切り上げるようなコード修正をした、ということかと思います。他の言語なら無知なバグ報告者を罵倒して終わるはずのところを、バグ修正として対応してしまうところがPHPらしいのかもしれません。もっとも、これは想像なので実際どうだか知りませんけどね。もし詳しい人がいらしたら教えてください。

というわけで、このエントリの解答としては「どうやらPHPの仕様」ということになります。実は僕が調べ始めた時点での予想は「PHPのバグ」でしたので、これは僕自身にとっても意外な結果です。

蛇足になりますが、http://jp.php.net/manual/ja/function.round.php の先頭には英語版には付いていない注意書き

(訳注：内部的な 2 進数表現と 10 進数表現の差により生じる丸め誤差の影響により 必ずしも小数点以下を四捨五入した結果を返さないことに注意してください。)

が書いてあります。これは訳者の優しさが表れている文章だと思います。一方で、今回のroundの妙な挙動が本当にPHPの仕様だとしたら、PHPの中の人は優しさが無いと思うんですよね。それとも、これはこれで優しさなんでしょうかね？