多少なりともプログラミングをしている方であれば、「浮動小数点数には誤差がある」という話は聞いたことがあるかもしれません。
しかし、そのことを普段から意識できているでしょうか?
僕は意識しておらず案の定踏み抜きました。
ありがちな例
ある日、ユーザさんの預金残高に小数点以下切り捨てで 0.5% の利息をつける必要がありました。
かんたんな計算だと思って、僕はこんなコードを書きました。
Math.floor(value * 1.005)
……これ、間違いです。
試しに value
に 1000
を入れてみましょう。1000に 0.5% を足したら1005になるような気がしますが、実際には1004になってしまいます。上記の例は JavaScript ですが、大抵のメジャーな言語では同じ結果になります。手元で試してみましょう。
なぜこんなことになるのでしょうか。1000 * 1.005
をそのまま出力してみると、1004.9999999999999
となります。この値はわずかに1005に達していないので、切り捨てたら1004になってしまうのですね。浮動小数点数は1.005を正確に表現できないため、誤差が生じてしまったようです。
切り捨てと切り上げに気をつける
この誤差は本当に簡単に発生します。次の例は、すべて期待通りの結果にはなりません。
Math.floor(0.6 / 0.2) // 2
0.6 / 0.2 // 2.9999999999999996
// 消費税は小数点以下切り上げ
Math.ceil(900 * 1.08) // 973
900 * 1.08 // 972.0000000000001
let f = 0;
for (let i = 0; i < 10; i++) {
f += 0.1;
}
Math.floor(f) // 0
f // 0.9999999999999999
浮動小数点数の計算結果は整数にならないと考えてしまったほうが良いです。
回避策
計算結果を丸める前に、十分下位の桁で四捨五入すれば、この問題は回避できます。
// 小数点第4位で四捨五入
Math.floor((value * 1.005).toFixed(3))
Math.floor(String.format("%.3f", value * 1.005))
floor(round($value * 1.005, 3))
厳密な計算が求められる場面には向かないかもしれませんが、大抵のアプリケーションではこれで十分でしょう。Math クラスを拡張するなどして、アプリケーション全体から呼べるようにしておくと良いかもしれません。
17/2/25 追記
コメントで指摘を頂きました。Decimal 型を使うことで、変な実装をすることなく誤差のない計算を行うことができます。
Java であれば標準で BigDecimal クラスが使えますし、JavaScript にも decimal.js というライブラリがあります。自分で実装するよりも、こういったものを使うほうが良いでしょう。
(追記ここまで)
まとめ
小数の計算には気をつけましょう。