数学アドベントカレンダーと言いつつ、ふつうのノウハウっぽいタイトルの記事ですが、中身的には「浮動小数点演算とどう付き合うか」という題材の具体例として消費税を持ってきています。
JavaScript以外の言語の方へ
RubyのBigDecimal、JavaのBigDecimalなど、10進法のまま計算できる数値ライブラリが標準で存在する場合は、迷わずそれを使いましょう。
ただ、フロントエンドで使うようなJavaScriptの場合、そのためにライブラリを入れるのは大げさだったりもします。ということで、「JavaScriptのnumber
型で計算する」場合について考えていくこととします。
何も考えずに計算すると…?
例えば、「消費税8%、小数点以下切り上げ」という条件で考えてみましょう。ごくかんたんに、Math.ceil(untaxed * 1.08)
とやってしまうと、例えば225円の場合に、本来なら225*1.08=243となるはずが、244まで切り上がってしまいます。
これは、JavaScriptのnumber
が浮動小数点数であり、1の位より下も2進法となっているため、「1.08」を正確に表せないことが原因です。実際にコンソールで225*1.08
を打ち込むと、243.00000000000003
という値が返ってきます。
なお、この前に書いたような方法で、この「1.08」のつもりで入力した値の厳密な値を調べてみると、「1.0800000000000000710542735760100185871124267578125」と、1.08より僅かに大きくなっていました。ということで、切り捨ての場合はわずかに大きくなってもまとめて切り捨てられるだけなので、(端数だけで0.01オーダーになるくらい巨大な数を扱う場合以外は)問題ありません。
また、四捨五入の場合も、1.08を整数倍しても端数が0.5きっかりになることがないので、たまたま問題にはなりません。
とはいえ、これらの「問題ない」というのは値を厳密に検証してわかることなので、消費税率が変わればまた前提も変わります。たとえば、消費税率10%になれば、整数以下の端数が0.5になる事例は容易に発生します(もっとも、「1.1」が1.1より僅かに大きいので、0.5で切り上げる四捨五入の場合は影響せずに済みます)。
正確に計算するために
いちばん確実なのは、untaxed * 108 / 100
のように計算することです。$2^{53}$(≒83兆)以下の整数では誤差が出ませんので、掛け算までは正確に行われます。割り算も、結果が正確に浮動小数点数になるなら、正確な値を返します。
もともと1.08
と浮動小数点数で与えられている場合、対策としては「本来数字のあるはずがない桁であらかじめ四捨五入してから戻す」という方法が考えられます。整数 × 1.08であれば、正しい結果は小数点以下2位までしかないはずなので、100倍した後に小数点以下に残っているものは誤差ということになります。ということで、ここでいったん四捨五入してから100で割り戻せば、(浮動小数点数として表せる範囲内では)正確、ということになります。
このように、金額のような離散的な値について浮動小数点数の計算を正しく行おうとすれば、それぞれの値がどういう範囲を持つべきものなのか、厳密な吟味が必要となります。そして、僅かな誤差でも、整数に対してプラスであれば切り上げに、マイナスであれば切り捨てに、0.5に対してマイナスの誤差が出る場合は四捨五入に影響してきます。