小数計算で発生する誤差とRubyでの対応方法について

こんにちは。ラクマでサーバーサイドエンジニアをやっているYuです。

最近、久しぶりの外出で日焼け止めを忘れて肌が赤くなりました。 みなさんは気をつけてください。

今回は、小数点を含む計算で発生する誤差と、Rubyでの対応方法についてお話したいと思います。

数字の計算って大事ですよね。1でもずれてたら大問題になってしまうこともあります。 ラクマでも手数料などお金の計算が行われていますので、正しく計算しなければいけません。

小数を含む計算をしてみよう

例として72000 * 0.086を計算してみましょう。

  72 000
 x 0.086
 -------
 432 000
5760 00
--------
6192.000

小学生のころのように紙に書いて筆算してみると72000 * 0.086 = 6192 になりました。小数点が出ないぴったりの数になりました。

さて、この計算をrubyのrails consoleで実行してみました。

(以下ruby: v2.7.1およびrails: v6.0.2.2で確認しています)

irb(main):001:0> 72000 * 0.086
=> 6191.999999999999

あれ?

ほとんど6192ですが、ぴったりにはなりません。 これを整数に変換してみると、

irb(main):002:0> (72000*0.086).to_i
=> 6191

正しい結果と比べて1ずれてしまいました。

どうして結果がずれてしまったの?

どうしてこのような結果になったのでしょうか? それを知るためには、コンピュータが小数をどう扱っているかを知る必要があります。

コンピュータが数字を扱うとき、2進数として扱っていることはよく知られていると思います。 (以下簡単な説明のため、小数の2進数表現について、やや正確性に欠けるところがあります。)

10   (10進数) -> 1010 (2進数) (10=2+8なので)
0.25 (10進数) -> 0.01 (2進数) (0.25 = 1/4なので、1を右に2つシフトする)

さて、この方法で小数を表現することにすると、小数を表現するためには以下のステップが必要とわかります。

1. ある整数を用意して
2. それを何回か右にシフトして
3. 目的の小数を表現する

上記の0.25の場合は、以下のようになります。

1. 1を用意して(1)
2. それを右に2つシフトして(0.01)
3. 0.25 = 1/4が表現できる

同じように、2.5を表現する場合は、

1. 10を用意して(1010)
2. それを右に2つシフトして(10.10)
3. 2.5 = 10/4が表現できる

そして、これを一般化すると、ある数xを2進数で正確に表現するためには、ある整数nkが存在して

x = k / (2 ^ n)

と表現できることが必要だということがわかります。 逆にいうと、このように表現できない小数は、2進数で正確に表現できないのです。 0.086には、どんなに探してもこのような表現方法は見つかりません! なので、0.086ではなくてそれに一番近い小数を代わりに使っていて、ずれてしまうのです。

どうすればずれないの?

どうすればずれないのでしょうか? 小数のまま計算してしまうとずれてしまうので、整数の計算(有理数の計算)に帰着させることが必要になります。

有理数というのは、(整数)/(整数)の形で表現することができる数のこと。 0.51/2と表せるから有理数だが、π=3.1415...にはそのように表す方法はないから有理数じゃない。)

そこでRationalクラスというものを使ってみましょう。 0.086 = 86/1000ということをふまえると、以下のように計算ができます。

irb(main):003:0> (72000 * Rational(86, 1000)).to_i
=> 6192

整数だけで計算を完結させることができるので、正しい値を表示することができました!

まとめ

このように、小数を含む計算を行うときには誤差が生じる可能性があります。 誤差が許容できない場合は、Rationalクラスを使って整数の計算に帰着させるといった対応が必要になります。