【JavaScript】もちろん「0.1 + 0.2 ≠ 0.3」をちゃんと説明できますよね??

はじめに

こんにちは、サクミル開発チームの栃川です。

どうも世の中には、「0.1 + 0.2 ≠ 0.3」ということを知ってはいるけど なんでそうなるのかよくわかっていない不届き者がおるようです(はい、私のことです笑)。

ということで、今日はなぜJavaScriptにおいて「0.1 + 0.2 ≠ 0.3」となるのか説明してみたいと思います。

前提

  • 2進数について理解があることを前提にしています
  • JavaScriptの範囲に限定して記事を記載しています

0.1 + 0.2の答えは何になるの

実際にコンソール画面で「0.1 + 0.2 」を計算してみます。 答えはもちろん0.3!!と思いきや、なんと答えは「0.30000000000000004」となります。

一体なぜこのようなことが起きてしまうのでしょうか。

これを理解するには「2進数と浮動小数点数」について理解する必要があります。

なお、2進数についての説明は割愛いたします。

もしよくわかってないという方がいれば下記の記事がとてもわかりやすく説明しているので参考にしてみてください。 gmo-miyazaki-creators.com

浮動小数点数とは

浮動小数点数とは、実数をコンピュータで処理するために有限桁の小数で近似値として扱う方式のことです。 今日、最も広く使われている規格は、 IEEE(Institute of Electrical and Electronics Engineers)「米国電気電子技術者協会」が制定している IEEE754というものです。

IEEE754では、全ての数値(N)を、符号部(s)・固定長の指数部(e)・固定長の仮数部(m)の3つの部分を組み合わせにより表現します。

 N = -1^{s}×m×2^{e}

そして、上記の数式で表された値はコンピュータ上では下記のようなビットの並びとして表現されます。

JavaScriptにおいては、数値を表現する際はIEEE754の規格のうち、符号部 1 ビット ・ 指数部 11 ビット ・ 仮数部 52 ビットで表現する倍精度 64ビットバイナリー形式を利用します

例えば、10進数の「10.75」という数値をIEEE754の精度 64ビットバイナリー形式で表現してみます。

「10.75」は2進数で表現すると「1010.11」となります。

2進数に変換した「1010.11」を上述したIEEE754の精度 64ビットバイナリー形式の形に直すと下記のようになることがわかります。

 1010.11_{(2)} = -1^{0}×1.01011_{(2)}×2^{3}

倍精度 64ビットバイナリー形式では、仮数部(m)は小数点部分である「01011」となります。 また指数部は、指数にバイアス値1023を足した値を2進数で表現する決まりとなっています。これは、マイナスの数値を符号なしの2進数で表すために、所定の数(=バイアス値)を加えて表現するためです。よって、指数部は3 + 1023 = 1026を2進数変換して「10000000010」となります。

最後に余ったビットには0を埋めるので最終的には、

  • 符号ビット (s): 0
  • 指数部 (e): 10000000010
  • 仮数部 (m): 0101100000000000000000000000000000000000000000000000

となり、10進数「10.75」はコンピュータ上では「0 10000000010 0101100000000000000000000000000000000000000000000000」と表現されます。

なぜ計算を間違えるのか

浮動小数点数については理解できたと思います。

ここで今回のテーマである「0.1 + 0.2 」について考えてみます。

実は「0.1」 と「0.2」は2進数に変換すると循環小数になります。

  • 0.1 => 0.000110011....
  • 0.2 => 0.00110011....

つまり、0.1と0.2を浮動小数点数になおすと下記のようになります。

 0.1_{(10)} = -1^{0}×1.10011001100....×2^{-4}

 0.2_{(10)} = -1^{0}×1.10011001100....×2^{-3}

これでは仮数部が循環小数になってしまい、52ビットで表現できなくなってしまいます。

こうした場合、IEEE754規格では仮数部の52ビットよりも後の部分は切り捨てられます。切り捨てる際に「偶数丸め(round to even)」のルールが適用されます。つまり、52ビット目が奇数(1)であれば、上に丸めて偶数にします。一方で、52ビット目がすでに偶数(0)であれば、そのまま切り捨てます。

「0.1」も「0.2」もいずれの場合も52ビット目は 1 で、53ビット目も 1となるので繰り上げ処理をしますので、結果として仮数部は「10011001100....10」となります。

つまり、「0.1 + 0.2」をする際には0.1 と0.2は内部的には厳密には表現できないので、それぞれの近似値で加算が行われます。結果として「0.1 + 0.2 ≠ 0.3」となるというわけです。

正しく計算するためには?

では、JavaScriptを使う以上は正しい計算はできないのでしょうか?? もちろんそんなことはありません。

とても便利なライブラリがあるので代表的なものを3つ紹介いたします。

これら3つのライブラリは、いずれもJavaScript の標準メソッドである toExponential, toFixed, toPrecision をサポートしていて、使い慣れた APIで数値演算の正確性を実現してくれる優れものです。 また、ドキュメントも充実して、すぐに使い始めることができます。

それぞれの違いについては作者が丁寧に説明してくれていますので、用途に応じて使い分けると良さそうです。 github.com

お困りの方は、ぜひ選択肢の1つとして検討してみてください。

まとめ

今回は、JavaScriptで「0.1 + 0.2 ≠ 0.3」となる理由について簡単に説明しました。

私自身が小数点の扱いで苦労した経験がありますので、読者の皆さんにはぜひ同じ失敗を避けるために、浮動小数点数の仕組みについて詳しく理解することをおすすめします(笑)きっと計算誤差のトラブルを未然に防ぐことができるはずです!

最後にプレックスではエンジニアとプロダクトデザイナーを募集しております。この記事を読んで、一緒に働いてみたいと思った方がいましたら是非ご連絡をお待ちしています!