hnwの日記

PHPで巨大な整数をカンマ区切りするのにnumber_format関数は使えない

PHPには、数値を3桁区切りで表示するための組み込み関数としてnumber_format()が用意されています。

number_format — 数字を千位毎にグループ化してフォーマットする


string number_format ( float $number [, int $decimals = 0 ] )


http://php.net/manual/ja/function.number-format.php


この関数は金額を表示するような場合に便利です。

<?php
$money = 1000;
printf("財布の中に%s円あります。\n", number_format($money));
/*
財布の中に1,000円あります。
*/


このように金額がカンマ区切りで表示されていると見やすくていいですよね。


ところで、このnumber_format()の第一引数はfloat型ですから、float型以外の引数が渡された場合はfloat型にキャストされてから処理されます。そのため、float型の性質を知らないと意外に思える結果になることがあります。

<?php
$money = 9007199254740995; /* 2**53+3 */
printf("財布の中に%s円あります。\n%d円ですよ?\n", number_format($money), $money);
/*
財布の中に9,007,199,254,740,996円あります。
9007199254740995円ですよ?
*/


今度は$moneyを非常に大きい整数にしてみました。大きいといっても64bit整数の範囲内ですので、64bit環境では正しく表現できる数です。実際、出力結果の2行目では代入した数がそのまま表示できているのがわかります。


一方、number_format()を通した結果は1円多い金額になっています。これはnumber_format()のバグではなく、浮動小数点数の性質によるものです。IEEE754倍精度浮動小数点数では2の53乗(およそ9000兆)までの整数を正しく表現できますが、それより大きい整数をキャストすると精度が落ちることがあるのです。

まとめ

9000兆を超える金額を扱うシステムでnumber_format()を使うと危険ということがわかりました。お金持ちの人は要注意ですね!


number_format()はオプショナルな第2引数を指定することで小数点以下も表示できますので、第1引数がfloat型なのは自然なことだと言えます。とはいえ、PHPプログラムだけ見るとfloat型が関係するようには見えませんから、わかりにくいバグの元になりそうですよね。


呼び出す関数の型宣言によってはエッジケースで予想外の挙動になることがありますよ、というのが本稿でお伝えしたかったことです。number_format()の場合は冗談で済む程度の内容だと思いますし、他の場合もおそらく無害なことが多いはずですが、世界のどこかで大障害の原因になっているかもしれません。また、PHP7からはスカラ型のタイプヒントが導入されますので、このような問題が今よりクローズアップされるかもしれません。