目次
この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。
小数の計算が必要な場面というのは多々ある。Rubyでは、例えば「1.5」と書けばFloatクラスのオブジェクトになるが、Floatクラスで注意しなければならないのが、浮動小数点誤差である。
この浮動小数点誤差に関して、原因、注意点、対策方法を纏める。
■ 浮動小数点誤差が発生する必要条件
Floatクラスのオブジェクトは内部的に2進数にて表現される。
2進数で表現した場合に無限小数となる数の演算は全て、浮動小数点誤差が発生する可能性がある。
「2進数で表現した場合に有限小数となる」とは、既約数で表現した場合の分母に2以外の素因数がない事である。
以下に例を示す。
1.5 = 3/2 … 分母を素因数分解すると2^1なので浮動小数点誤差は発生しない。
0.75 = 3/4 … 分母を素因数分解すると2^2なので浮動小数点誤差は発生しない。
0.1 = 1/10 … 分母を素因数分解すると2^1 * 5^1 なので浮動小数点誤差が発生する可能性がある。
■ 浮動小数点誤差が発生しがちな計算の例
浮動小数点誤差が発生しがちな計算は、例えば消費税の計算がある。
浮動小数点誤差の知識はあったとしても、意識していないと、225円の商品の税込み価格計算で「225 * 1.08」の様に書いてしまいがちである。
この計算は「243.0」を期待しているが、実際は「243.00000000000003」となる。
この結果の問題点は、税込価格が端数切り上げで計算している場合に、本来の価格より1円高くなってしまう点である。
(225 * 1.08).ceil
=> 244
■ 浮動小数点誤差に対する対策
対策1 : 演算の順序を工夫する
小数は分数で表現できる。分数は除算で表現できる。
整数の集合は、加算・減算・乗算において閉じているので、
除算を最後の1回のみ行う様に式を置き換える。
(225 * 108 / 100.0).ceil
=> 243
デメリット
・有効になる場面が限られている。
・複雑な演算には向かない。
対策2 : BigDecimalクラスを使用する
人間が数値として表現できる数は殆どが10進法の有限小数または整数である。
10進法で表記した各桁の精度を落とさずに演算が可能なBigDecimalクラスを使用すれば、有効桁数がそれ程多くない数同士の加算・減算・乗算では誤差は発生しない。
(恐らく電卓と同じ様なロジックになっていると思われる)
(225 * BigDecimal(“1.08”)).ceil
=> 243
デメリット
・分子の素因数に2, 5以外の数を持つ既約有理数による除算は誤差が発生する可能性がある。
・String型に変換する必要がある。
・遅い。
対策3 : Rationalクラスを使用する
有理数の集合は四則演算において閉じている(0による除算を除く)ので、
既約有理数の分母分子の精度を落とさずに演算が可能なRationalクラスを使用すれば、有理数同士の四則演算では0による除算を除いて誤差は発生しない。
(225 * 1.08r).ceil
=> 243
デメリット
・数値的に求めるには型変換をしなければならない。
■ Rationalクラスを使用する場合の注意点
Rationalクラスは丸め誤差の影響を考えなくて良い便利なクラスであるが、RationalオブジェクトとFloatオブジェクトの四則演算を行う場合、RationalオブジェクトはFloatオブジェクトに暗黙型変換されて演算される為、注意が必要となる。
(例えば、「225.0 * 1.08r」は「225.0 * 1.08」と同じになってしまう)
つまり型の優先度としては、Rationalは、Integerより強く、Floatより弱い様である。
Rationalクラスの利点を活用するのであれば、Floatオブジェクトをto_rメソッドによってRationalオブジェクトにしてから演算を行う必要がある。