search
LoginSignup
201

More than 5 years have passed since last update.

posted at

updated at

Organization

[Ruby]消費税計算にはBigDecimalを使いましょう

消費税計算でありがちな浮動小数点問題

ちょっと電卓で 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%になったら不具合が出始めた!」なんていうケースもあるんじゃないでしょうか?

まとめ

「プログラマだったら浮動小数点問題なんて常識だよ!」という方も多いと思いますが、常識だとわかっていても意外とうっかり間違えたりしてしまうものです。

はい、何を隠そう、僕自身がうっかりこの不具合を出してしまいました・・・。

僕のうっかりミスを他山の石として、みなさんも消費税計算や小数が絡む計算をする場合には浮動小数点の扱いを間違えないように注意してくださいね~。

参考

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
201