0.1+0.2が0.3にならない理由|PHPではfloor((0.1+0.7)*10)が8ではなく7になる
JavaScriptで0.1+0.2を計算すると0.30000000000000004になる。
原因は、小数を二進数で持つ仕組みにある。
比較・お金・表示の三つの場面で、どう書けばいいかをまとめた。
JavaScript で 0.1 + 0.2 を計算すると、0.3 ではなく 0.30000000000000004 になる。
PHP のマニュアルには、floor((0.1+0.7)*10) が 8 ではなく 7 になる例まで載っている。
原因はバグではなく、小数を二進数で持つ仕組みそのものにある。
この記事では、なぜそうなるのかと、比較・お金・表示の三つの場面でどう書けばいいかをまとめる。
最初にこれを見ると、コンピューターは計算が苦手なのかと思ってしまう。
でも計算を間違えているわけではない。
0.1 という数を、最初からぴったりには持てていないだけだ。
0.1 は、二進数では割り切れない
十進数で 1 ÷ 3 を書こうとすると、0.3333… と終わらなくなる。
どこかで切るしかないので、少しだけ誤差が残る。
二進数でも同じことが起きる。
二進数で割り切れるのは、2 で何回か割った形の数だけだ。
0.5 や 0.25 はぴったり書けるが、0.1 や 0.7 は二進数にすると終わらない小数になる。
PHP のマニュアルにも、0.1 や 0.7 のような数は二進数の浮動小数点数としては正確に表現できない、と書いてある。
仮数部をいくら大きくしても同じだ、とも書いてある。
桁を増やせば解決する話ではない、ということだ。
PHP も JavaScript も、同じ「倍精度」で持っている
PHP のマニュアルによると、PHP は通常 IEEE 754 倍精度フォーマットを使う。
丸めの誤差は 1.11e-16 のオーダーで、計算を重ねると誤差はもっと大きくなる。
JavaScript の Number も同じ倍精度の形式だ。
だから二つの言語で同じ現象が出る。
言語の癖ではなく、ほとんどの言語に共通する話だと思っておいたほうがいい。
// JavaScript
console.log(0.1 + 0.2); // 0.30000000000000004
console.log(0.1 + 0.2 === 0.3); // falsePHP の例は、中の値が 7.9999999999999991118… のようになっているから 7 になる。
floor は切り捨てなので、8 にほんの少し届かない値が 7 に落ちる。
切り捨てや比較のように「境目」で判定する処理ほど、この誤差が表に出やすい。
小数どうしを == で比べない
PHP のマニュアルは、小数を直接比較して等しいかどうかを調べてはいけない、と書いている。
代わりに、差がじゅうぶん小さければ同じとみなす。
JavaScript には Number.EPSILON という定数がある。
1 と、その次に表せる数との差で、およそ 2.22e-16 だ。
ただ、MDN はこの値をそのまま閾値に使わないように、と注意している。
1000.1 + 1000.2 のように数が大きくなると、誤差は 10 の -13 乗くらいまで膨らむ。
Number.EPSILON で比べると、同じはずの値が「違う」と判定されてしまう。
閾値は、扱う数の大きさと、何桁まで合っていればいいかで決める。
閾値を書く前に「この数字は何桁まで意味があるのか」を先に決めるのがいい、と僕は思っている。
温度なら小数第1位、距離なら小数第2位、というふうに、意味のある桁が決まれば閾値も自然に決まる。
お金は、そもそも小数で持たない
金額の計算に小数を使うと、この誤差が一円単位のずれとして出てくることがある。
僕の考えでは、いちばん確実なのは小数を使わないことだ。
円なら整数のまま扱う。
ドルのように小数点以下がある通貨なら、セント単位の整数にしてから計算する。
税や割引で端数が出るところだけ、どこで、どの向きに丸めるかを決めて一回だけ丸める。
どうしても小数のまま正確に計算したいなら、PHP のマニュアルは任意精度数学関数か gmp 関数を使うように案内している。
どちらも拡張機能なので、使う前にサーバーで有効になっているかを確かめておきたい。
表示のときは、桁を決めて丸める
計算結果を画面に出すだけなら、表示する桁数を決めて丸めればいい。
PHP なら round や number_format、JavaScript なら toFixed が使える。
ここで気をつけたいのは、丸めるのは「最後に一回」ということだ。
途中で何度も丸めると、丸めの誤差が積み重なって、かえって合計が合わなくなる。
計算は丸めずに進めて、人に見せる直前で丸める。
この順番を守るだけで、見た目のおかしな数字はかなり減る。
型の比較で似た落とし穴を踏んだ話は、PHPの「==」の罠に書いた。
数字の意味をコードに残す話は、マジックナンバーを定数に置き換えるにまとめている。
まとめ
0.1 + 0.2 が 0.3 にならないのは、0.1 を二進数でぴったり持てないからだ。
小数は == で比べず、差が小さいかで比べる。
お金は整数で持ち、表示の直前で一回だけ丸める。
この三つを押さえておけば、「計算が合わない」で半日つぶれることはだいぶ減ると思う。
出典
ほかにも書いています
受託・note・スタンプこういう仕組みを作る相談を受けています
毎日の更新・データ集め・集計の自動化/3万円〜収益がいくらだったか、どの施策が外れたか
note / 個人の視点で掘り下げていますスタンプラリーに挑戦する
行った場所は、現地チェックインで記録に残せます※受託の相談とnoteは、運営者が個人で受けている・書いているものです。
