てもとツール
ガイド

0.1+0.2が0.3にならない理由と、小数の比べ方・直し方

0.1+0.2が0.30000000000000004になるのは、0.1も0.2も2進の小数ではちょうど持てず、近い値に置き換わるからです。ずれない小数の見分け方と、差で比べる・整数に直す・表示だけ丸める・10進の型を使う、の4つの直し方を、それぞれの落とし穴と一緒に説明します。

公開読了目安 約5分

この記事の要点

  • 0.1 も 0.2 も、2進の小数ではいちばん近い別の値に置き換わる
  • ずれずに入りうるのは、分母が2の累乗になる小数(0.5・0.25 など)だけ
  • 差で比べるとき、Number.EPSILON をそのまま許容幅にすると 1.1 + 2.2 で外れる
  • 整数に直すときは、掛けたあとに切り捨てず四捨五入する
  • 表示で丸めても、中の値はそのまま。丸めた文字を次の計算に使わない
目次

0.1 + 0.2 が 0.3 にならないのは、0.1 も 0.2 も、コンピュータの2進の小数ではちょうど持てないからです。 中には「いちばん近い別の値」が入るので、足した結果も 0.3 とわずかに違う値になります。

JavaScript の数(number)や Python の小数(float)は、どちらもこの2進の形です。 この記事では、ずれない小数の見分け方と、比べ方・直し方を4つ、それぞれの落とし穴と一緒に説明します。

0.1 + 0.2 の中で起きていること

ずれは1か所ではなく、3か所で入ります。 0.1 を読み込むとき、0.2 を読み込むとき、2つを足した結果を丸めるときです。

0.1 と 0.2 は少し大きめに入り、足した結果はさらに少し大きめに丸まります。 一方で 0.3 と書いた値は、少し小さめに入ります。

その結果、0.1 + 0.2 は 0.3 と書いた値の「1つ隣の値」になります。 それぞれの段の差は、浮動小数点数ビューアの「2つの数を計算」で確かめられます。

ずれない小数と、ずれる小数の見分け方

小数を分数にして約分し、分母に2以外の数が残れば、その小数は必ずずれます。 0.1 は 1/10 で、分母の10に5が入っているので、2進では終わらずに続きます。

分母が2の累乗(2・4・8・16…)だけなら、2進でも途中で終わる小数になります。 0.5 は 1/2、0.25 は 1/4、0.375 は 3/8 なので、どれもちょうど入ります。

ただし、並べられる2進の桁には限りがあります。 倍精度で使える有効な桁は53桁なので、それより長い小数は、分母が2の累乗でも区切られます。 たとえば 0.5 に2の-54乗を足した数は、2進で54桁になり、0.5 に丸まります。

小数分数ずれずに入るか
0.51/2入る
0.3753/8入る
0.11/10ずれる
0.33/10ずれる

整数も、大きすぎると飛び飛びになります。 9,007,199,254,740,992(2の53乗)を超えると、整数でも2おきにしか入りません。

直し方1: === ではなく、差が十分小さいかで比べる

答えが一致するかを確かめたいときは、=== で比べずに、2つの差が許容幅より小さいかで比べます。 許容幅の決め方には落とし穴があります。

よく見かけるのは、Number.EPSILON(約 2.22e-16)を許容幅にする書き方です。 0.1 + 0.2 と 0.3 の差は 5.55e-17 なので、これは通ります。

ところが、1.1 + 2.2 は 3.3000000000000003 になり、3.3 との差は 4.44e-16 です。 Number.EPSILON より大きいので、同じ書き方では「違う」と判定されます。

Number.EPSILON は、1 と、1 の隣の値との差です。 数が大きくなると隣の値との差も広がるので、許容幅は比べる数の大きさに合わせます。

// 差が、大きいほうの数に比例した幅に収まるか
function nearlyEqual(a, b, rel = Number.EPSILON * 4) {
  const scale = Math.max(Math.abs(a), Math.abs(b));
  return Math.abs(a - b) <= rel * scale;
}
nearlyEqual(1.1 + 2.2, 3.3); // true

Python なら math.isclose() が、大きさに合わせた幅(既定 1e-09)で比べます。 math.isclose(0.1 + 0.2, 0.3) は True ですが、0 と比べるときは abs_tol を指定しないと False になります。

直し方2: 整数に直して計算する

0.1 単位の値なら、10倍した整数で持って計算し、最後に10で割ります。 整数どうしの足し算・引き算・掛け算は、大きすぎなければずれません。

落とし穴は、小数を整数に直す一歩目です。 0.57 × 100 は 56.99999999999999 になるので、切り捨てると 56 になります。

整数に直すときは、Math.round() で四捨五入します。 Math.round(0.57 * 100) なら 57 になります。

もう1つの落とし穴は、桁の多い数です。 2の53乗を超える整数は飛び飛びになるので、桁が多い値は BigInt のような整数の型を使います。

直し方3: 表示のときだけ丸める

画面に出すだけなら、表示のときに桁数を決めて丸めれば足ります。 (0.1 + 0.2).toFixed(10) は "0.3000000000" になります。

ただし、丸めは中に入っている値で行われます。 1.005 は 1.00499999999999989… として入っているので、(1.005).toFixed(2) は "1.00" です。

Python の round(2.675, 2) も、同じ理由で 2.68 ではなく 2.67 になります。 Python の公式ドキュメントも、これはバグではないと説明しています。

書いた数のとおりに丸めたいときは、四捨五入・端数処理計算のように、10進の字面のまま丸める方法が要ります。 丸めた文字を、次の計算に使い回さないことも大切です。

直し方4: 10進のまま計算する型を使う

10進の小数をそのまま持つ型を使えば、0.1 もちょうど入ります。 Python の decimal で試すと、次の比べ方が True になります。

  • Decimal('0.1') + Decimal('0.2') == Decimal('0.3')

Decimal(0.1) のように数から作ると、2進に置き換わった後の値が入ってしまいます。 10進の型には、文字列で値を渡します。

JavaScript の数は2進の形だけなので、10進の小数を扱うライブラリを使うか、直し方2の整数に直す方法を選びます。

何度も足し算を重ねるとき

ずれは計算を重ねるたびに積み重なります。 JavaScript で 0.1 を1つずつ10回足すと、1 ではなく 0.9999999999999999 になります。

Python の math.fsum() は、途中で失われた分を補いながら足すので 1.0 を返します。 Python 3.12 からは sum() も、浮動小数点数の総和をより正確に求める方法に変わりました。

足す回数が多い集計では、こうした足し方を使うか、直し方2のように整数で集計します。

関連記事

あわせて読みたい

同じテーマ・関連ツールの記事です。