消費税計算でありがちな浮動小数点問題
ちょっと電卓で 1800 x 1.08 を計算してみてください。いくつになりましたか?
はい、 1944 ですね。
ではターミナルからirbを開いて、 1800 * 1.08
と打ち込んでください。
> 1800 * 1.08
=> 1944.0000000000002
あれ?1944にならない!?
はい、これはコンピュータを使った計算でよくありがちな、 浮動小数点問題 というやつです。
つまり、ざっくりいえばコンピュータは小数点を人間が行う計算と同じように扱えない、というわけです。
もし、「消費税計算は端数切り上げとする」という仕様だった場合、この浮動小数点問題はバグの原因となります。
> (1800 * 1.08).ceil
=> 1945
お金の計算は一円でもズレると致命的です。
どのように回避すれば良いでしょうか?
小数を含む金額の計算はBigDecimalを使いましょう
この問題を解決する方法には選択肢がいくつかあると思いますが、ここではRuby標準のBigDecimal
クラスを使った計算方法を紹介します。
先ほどの計算はこうすれば期待する結果が得られます。
> require 'bigdecimal'
=> true
> (BigDecimal("1800") * BigDecimal("1.08")).ceil
=> 1944
ちなみに数値リテラル(Float
型)をそのままBigDecimal
に渡すと、ArgumentError: can't omit precision for a Float.
というエラーが出ます。
> (BigDecimal(1800) * BigDecimal(1.08)).ceil
ArgumentError: can't omit precision for a Float.
これは、
- もし小数がそのまま渡せてしまう場合、
1.08
という小数リテラルが評価された段階で、Floatクラスのインスタンスができる - しかしFloatクラスなので、その時点で
1.08
との誤差が生じてしまい、意図した結果にならない
ためです。なので、上で書いたように文字列として渡してください。
RSpecで検証
ついでにRSpecで実行結果を確認しておきましょう。
require 'bigdecimal'
describe 'tax calculation' do
describe 'with Float' do
it 'cannot get valid result' do
expect((1800 * 1.08).ceil).to_not eq 1944
end
end
describe 'with BigDecimal' do
it 'gets valid result' do
expect((BigDecimal("1800") * BigDecimal("1.08")).ceil).to eq 1944
end
end
end
余談: 消費税が5%のときはバグにならない
ちなみに上の計算は消費税が5%だと運良く(?)端数が出ないので、不具合になりません。
> 1800 * 1.05
=> 1890.0
なので、もしかすると「今まで問題なかったのに、なぜか8%になったら不具合が出始めた!」なんていうケースもあるんじゃないでしょうか?
2023-08-10追記
2023年現在の消費税は10%ですが、こんな計算式だと10%でも端数は発生します。
100000000000000 * 1.1
#=> 110000000000000.02
まあこんなに大きな金額を計算することは滅多にないと思いますが😅
まとめ
「プログラマだったら浮動小数点問題なんて常識だよ!」という方も多いと思いますが、常識だとわかっていても意外とうっかり間違えたりしてしまうものです。
はい、何を隠そう、僕自身がうっかりこの不具合を出してしまいました・・・。
僕のうっかりミスを他山の石として、みなさんも消費税計算や小数が絡む計算をする場合には浮動小数点の扱いを間違えないように注意してくださいね~。