floatやらdecimalやらで沼にハマったので忘備録
なんかfloatって便利そうだから使ったらいいかなーという気楽な気持ちで使ったら面倒なことになりました。
もはやfloat
の使い所がわからなくなった...
floatだと精度が低い!
例えば、以下のような値をfloatした時に思っている挙動と違いました。
1.111111111111111111222222222.floor(20)
=> 1.1111111111111112 #小数点以下が16桁しかない...
他にも以下のような計算をしても誤差が出てしまいます。
pry(main)> 1 - 0.9
=> 0.09999999999999998
pry(main)> 0.9.class
=> Float
これらはfloat型であることに起因しています。
理由は以下の説明の通り。
Rubyにおいて、浮動小数点数の精度は倍精度浮動小数点数(double-precision floating point)によって制御されます。この倍精度浮動小数点数は通常64ビットで表現されます。
1.111111111111111111222222222という数値は、倍精度浮動小数点数では表現しきれない精度を持っています。
そのため、Rubyでは内部的に最も近い倍精度浮動小数点数に丸められることになります。
Rubyでは倍精度浮動小数点数の精度は約15桁程度であり、それを超えると丸められることによって誤差が生じます。このため、1.111111111111111111222222222.floor(20)を実行すると、1.1111111111111112という結果になります。
つまり、浮動小数点数の精度は低いということ。
BigDecimalを使おう
このような誤差を避けるためには、Rubyにおいては必要な精度を保つためにBigDecimalクラスを使用することが推奨されます。BigDecimalクラスを使うと、より高い精度で浮動小数点数を扱うことができます。
例えば、上記の数値をBigDecimalで処理する場合は以下のようになります:
pry(main)> number = BigDecimal('1.111111111111111111222222222')
=> 0.1111111111111111111222222222e1
pry(main)> number.floor(20)
=> 0.111111111111111111122e1
計算も同じです。
pry(main)> 1.to_d - 0.9.to_d
=> 0.1e0
BigDecimalを使うことで、より正確な計算が可能になりますねー。
計算の速度が倍精度浮動小数点数よりも遅くなることがあると言われているが、大規模じゃないと影響出ないのでは?とか思ってるがそれは未検証。
困ったこと(余談)
decimal型でDBに保存すると指定の桁数を上回る場合はデフォルトでは四捨五入される。
ただ、精度を高く保ちたい場合、この四捨五入は非常に厄介。
例えば、precision:4, scale:2
で設定しているところに1234.129
を保存すると1234.13
となってしまう。これは非常に困る!!!!!だって1234.12
という正確な値が欲しいのだから。
自分は以下のようにbefore_action
で対処しようとしたのですが、これがまた困ったことに
floor
メソッドの挙動にやられた。
どういうことかというと、floor
メソッドを使ったことがある人はわかると思うのだが、
正の値をfloorするには想定通りになるのだが負の値をfloorするとより小さい方に行ってしまう。
例えば以下を実行すると、より小さい方にまとめられる性質があるので、この場合は-1.3
となる。困った。
-1.23.floor(1)
=> -1.3
じゃあどうしたらいいかというと負の値にはceil
を使えば良い。
これだと大きい方に切り上げられるので想定の挙動になる。
-1.23.ceil(1)
=> -1.2
つまり、まとめると以下ということ。
- floorは小さい方に切り捨てる
- ceilは大きい方に切り上げる
上記を踏まえてbefore_actionを作成した。(例外処理等は割愛)
def hoge
self.foobar = foobar.positive? ? BigDecimal(foobar.floor(2).to_s): BigDecimal(foobar.ceil(2).to_s)
end
こんなことしなくてよかった...
でもbefore_actionは不要でした。というか、foobar
に入ってくる値が20桁とかだったらこの時点で精度が狂うし、ダメなやり方でした。
というかtruncateで良くない?
まだまだメソッドの知識が足りないな...実装前にもっと調べる癖をつけよう。
実はPostgres(他はよくわかっていない)では、decimal型でDBに保存すると指定の桁数を上回る場合はデフォルトでは四捨五入される。(前述)
実はこのデフォルトは変えられるというおちでした...
詳しくは公式に書いてありますが、以下のように記述することで溢れた小数点は切り捨てられるようにできます。
BigDecimal::mode(BigDecimal::ROUND_MODE, BigDecimal::ROUND_UP)