PHPの浮動小数点数を理解していなかったために、消費税計算の結果が正しくなく本番障害になった話です。障害連絡をもらって調査を開始したさい、電卓叩いてもその結果にならず頭を悩ませました。
障害内容
インボイス対応で消費税対象額と消費税額の内訳を請求書に表示する対応を行いました。障害連絡があった請求書の内容は以下の通りでした。税率は10%です。
金額 | 404800 |
消費税対象額 | 368001 |
消費税額 | 36799 |
消費税計算の処理を抜粋すると以下のようなコードになります。
<?php
$rate = 0.1;
$amount = 404800;
$tax = floor(($amount / (1 + $rate)) * $rate);
echo $tax;
?>
結果: 36799
(金額 ÷ 1.1) × 0.1
の結果を切り捨てて消費税額を求めていました。今回の金額で計算すると (404800 ÷ 1.1) × 0.1 = 36800
となります。電卓叩いてもExcelで計算しても36800になりますが、画面上には36799と表示されてしまっています。
原因
そもそも 404800 ÷ 1.1 = 368000
で端数が出ないので丸め処理の問題ではないと思い込んでいました。しかし、原因はPHPの内部の浮動小数点数による丸めの問題でした。
PHPマニュアルの言語リファレンスに以下の警告が記載されています。内部的には(404800 ÷ 1.1) × 0.1 = 36799.999…
となっており、小数点以下を切り捨てることで36799となっていたようです。
警告
浮動小数点数の精度
浮動小数点数の精度は有限です。 システムに依存しますが、PHP は通常 IEEE 754 倍精度フォーマットを使います。 この形式は、1.11e-16 のオーダーでの丸め処理で誤差が発生します。 複雑な算術演算をすると、誤差はさらに大きくなるでしょう。そしてもちろん、 いくつかの演算を組み合わせる場合にも誤差を考慮しなければなりません。
さらに、十進数では正確な小数で表せる有理数、たとえば
0.1
や0.7
は、 二進数の浮動小数点数としては正確に表現できません。 これは、仮数部をいくら大きくしても同じです。 したがって、それを内部的な二進数表現に変換する際には、どうしても多少精度が落ちてしまいます。 その結果、不思議な結果を引き起こすことがあります。たとえば、floor((0.1+0.7)*10)
の結果はたいてい7
となるでしょう。おそらくは8
を想定していらっしゃるでしょうが、そのようにはなりません。 これは、(この計算結果の) 内部的な値が7.9999999999999991118...
のようになっているからです。よって、小数の最後の桁を信用してはいけませんし、 小数を直接比較して等しいかどうかを調べてはいけません。より高い精度が必要な場合には、 任意精度数学関数または gmp 関数を代わりに使用してください。
もっと「シンプルな」説明が欲しければ、» floating point guide を見るといいでしょう。”Why don’t my numbers add up? (なんで数字が足されないの?)” というタイトルが付いています。
https://www.php.net/manual/ja/language.types.float.php
対応方法
PHPマニュアルの言語リファレンスに記載されている通り、任意精度数学関数を使用することで対応できます。BC Math 関数という任意の精度で掛け算や割り算を行う関数が用意されています。これを用いて以下のように修正することで正しく計算されるようになりました。
<?php
// Your code here!
$rate = 0.1;
$amount = 404800;
$bc_tax = bcmul(bcdiv($amount, (1 + $rate), 0), $rate, 0);
echo $bc_tax;
?>
結果: 36800
まとめ
コンピューターの内部では2進数で管理しています。そのため、人間が認識する整数とコンピューターが内部で管理している浮動小数点数にはギャップがあります。最近の言語ではそのあたりを人間に意識させることなく処理できるようになっていますが、PHPのfloorやroundでは浮動小数点数に対して丸めが行われるために今回の問題が発生しました。PHPで精度の求められる計算を行う場合はBC Math 関数を利用するのが安全です。
コメント