0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

0.1 + 0.2 が 0.3 にならない — コンピュータは小数を「2進数の分数」でしか持てない

0
Last updated at Posted at 2026-07-25

電卓アプリの合計金額が1円ズレた。テストは通っているのに本番の集計だけ合わない。原因が分からず2時間溶かした末に、0.1 + 0.20.30000000000000004 を返していると気づいた。バグじゃない、仕様だった。自分はここで一度ちゃんとハマったので、同じ落とし穴の地図を置いておく。

  • 現象: 0.1 + 0.2 === 0.3false。JavaScriptでもPythonでもJavaでも同じ。
  • 原因: 小数は内部で2進数の分数に変換される。0.1 は2進数だと循環小数になり、途中で打ち切られて誤差が残る。
  • 直し方: 等値比較をやめて誤差許容にするか、整数(最小単位)で持つか、十進数型(Decimal)を使う。用途で選ぶ。
読む前 読んだ後
「計算ミスするCPUなんてある?」と疑う 2進数で0.1を表せない理由を1行で説明できる
金額計算を float でやっている お金は整数(最小単位)で持つ判断ができる
`==` で小数を比較して落ちる イプシロン比較を書ける

想定読者: 小数計算で1回でも「あれ?」となった全ての人。言語不問。特に金額・座標・集計を扱う人。

目次

  1. 何が起きたか(3言語で再現)
  2. なぜ2進数だと0.1が表せないのか
  3. IEEE 754 という「箱の形」
  4. 誤差が牙をむく2つの瞬間
  5. 直し方その1: 誤差を許容して比較する
  6. 直し方その2: 整数に寄せる(お金編)
  7. 直し方その3: 十進数型を使う
  8. 言語別の落とし穴早見
  9. まとめとアクション

何が起きたか(3言語で再現)

まず手元で再現する。ブラウザのコンソールでもNode.jsでもいい。

// JavaScript (ブラウザのコンソールでそのまま実行できる)
console.log(0.1 + 0.2);          // 0.30000000000000004
console.log(0.1 + 0.2 === 0.3);  // false
console.log(0.3 - 0.1);          // 0.19999999999999998

Pythonでも一致する。言語の実装が悪いわけじゃない。

# Python 3
>>> 0.1 + 0.2
0.30000000000000004
>>> 0.1 + 0.2 == 0.3
False
>>> from decimal import Decimal
>>> Decimal(0.1)   # floatが内部で何を持っているか覗く
Decimal('0.1000000000000000055511151231257827021181583404541015625')

最後の行が核心。0.1 を代入した瞬間、メモリには 0.1 ではなく 0.1000...0555... が入っている。この余りの ...0555 が積み重なって 0.30000000000000004 になる。JavaScriptもJavaもRubyもGoも、同じ double(64ビット浮動小数点)を使うかぎり同じ数字が出る。

なぜ2進数だと0.1が表せないのか

10進数で 1/3 を書こうとすると 0.3333... と無限に続く。有限桁では正確に書けない。これと同じことが、2進数の 0.1 で起きる。

2進数の小数は「1/2、1/4、1/8、1/16 …」の足し算でしか作れない。0.1(10分の1)をこの足し算で表そうとすると、

0.1(10進) = 0.0001100110011001100110011...(2進)
                └─ 1001 が無限に循環する

0011 が延々と繰り返される循環小数になる。分母が2のべき乗じゃない小数(0.1、0.2、0.3、0.7 …)は、ほぼ全部この運命をたどる。逆に 0.5(=1/2)や 0.25(=1/4)や 0.75 はぴったり表せる。

>>> 0.5 + 0.25 == 0.75   # 分母が2のべき乗ならピタリ合う
True
>>> 0.1 + 0.1 + 0.1 == 0.3
False

0.1 を3回足すと誤差が3回分たまって、0.3 を1回変換した誤差と一致しない。「同じ0.3のはずなのに」ズレるのはこれが理由。

IEEE 754 という「箱の形」

無限に循環する2進数を、有限のメモリに詰めるルールが IEEE 754 という規格。ほぼ全ての言語の double がこれに従う。64ビットの内訳はこうなっている。

[符号 1bit][指数 11bit][仮数 52bit]
 └ プラス/マイナス  └ 桁の位置     └ 有効数字の本体

仮数部が52ビットしかないので、循環小数はこの52桁で強制的に打ち切られる。打ち切った時点で「本来より少しだけ大きい/小さい」値に丸められる。この丸め誤差が全ての元凶。

10進数に直すと、double が保証する有効数字はおよそ 15〜17桁。だから 0.30000000000000004...04 みたいな16桁目あたりでボロが出る。ここは「精度が足りない」というより「有限の箱に無限を入れた代償」と捉えるのが正しい。詳しい丸めの挙動はPython公式チュートリアルのFloating Point Arithmeticが2進変換つきで一番わかりやすい。

誤差が牙をむく2つの瞬間

普段は表示のときに勝手に丸めてくれるので気づかない。問題が表面化するのは主に2パターン。

1つ目は等値比較。 if (total === 0.3) のような書き方は、誤差1個で崩れる。

let total = 0;
for (let i = 0; i < 10; i++) total += 0.1;
console.log(total);          // 0.9999999999999999
console.log(total === 1.0);  // false — ループ集計で誤差が蓄積

実際にテストでこう書くと落ちる。CIログにこのエラーが出て、最初は自分のロジックを疑った。

# test_total.py 集計ロジックのテストが謎に失敗した
>>> assert sum([0.1] * 10) == 1.0
Traceback (most recent call last):
  File "test_total.py", line 3, in <module>
    assert sum([0.1] * 10) == 1.0
AssertionError   # 実際の値は 0.9999999999999999

2つ目は金額計算。 消費税や割引で小数が出ると、四捨五入の直前で1円ズレる。集計件数が100件→99件分の金額になる、みたいな事故につながる。10件足しただけでも 10→9 に化けることがある(上のassertが1.0でなく0.9999...で落ちたのがそれ)。

>>> price = 0.1 + 0.2   # 何かの計算結果が 0.30000000000000004
>>> round(price, 2)     # 見た目は 0.3 に見えるが…
0.3
>>> int(price * 100)    # 整数化すると 30 のはずが
30
>>> int((0.1 + 0.2 + 0.1) * 100)  # 積もると 40 が 39 に落ちる
40

集計件数が増えるほど誤差は積もる。「テストは通ったのに本番の大量データで合計が合わない」の典型パターンがこれ。

直し方その1: 誤差を許容して比較する

小数同士を比べるときは、完全一致ではなく「十分近いか」で判定する。差が極小の閾値(イプシロン)より小さければ等しいとみなす。

// 許容誤差つきの比較関数
function nearlyEqual(a, b, epsilon = Number.EPSILON) {
  return Math.abs(a - b) < epsilon;
}
console.log(nearlyEqual(0.1 + 0.2, 0.3));  // true

Number.EPSILON は「1.0の次に表現できる数との差」で、約 2.22e-16。JavaScriptの定数の意味はMDNのNumber.EPSILONに定義がある。Pythonなら標準ライブラリの math.isclose が同じ役割を果たす。

>>> import math
>>> math.isclose(0.1 + 0.2, 0.3)  # 相対誤差も考慮してくれる
True

閾値を絶対値で固定すると桁の大きい数で破綻するので、math.isclose のように相対誤差を見る実装が安全。

直し方その2: 整数に寄せる(お金編)

金額を扱うなら、そもそも小数を使わないのが一番堅い。円ではなく「銭」や最小単位の整数で持つ。 100円なら 100、消費税10%なら 110、と整数のまま計算して、表示のときだけ割る。

# NG: 円をfloatで持つ
total = 0.0
for _ in range(3):
    total += 108.9   # 税込みの端数
print(total)         # 326.70000000000005 ← 誤差混入

# OK: 最小単位(銭=1/100円)を整数で持つ
total_sen = 0
for _ in range(3):
    total_sen += 10890   # 108.90円を「10890銭」として整数化
print(total_sen / 100)   # 326.7 ← 誤差ゼロ

ぶっちゃけこれが一番事故らない。上の例で 326.70000000000005 → 326.7 と誤差が消えたのが分かる。整数の足し算・掛け算には丸め誤差が原理的に発生しないからだ。会計・決済・ポイント計算はこの方式が定石。割り算だけは端数処理のルール(切り捨て/四捨五入)を明示的に決めておく。自分が金額処理を書くときは、まず「この値は整数で持てないか?」から入るようにしている。

直し方その3: 十進数型を使う

「整数化は面倒、でも誤差は許せない」なら、10進数を10進数のまま扱う専用の型を使う。Pythonの decimal.Decimal、Javaの BigDecimal、JavaScriptなら decimal.js などのライブラリ。

from decimal import Decimal

# 文字列から作るのがコツ。float から作ると誤差を引き継ぐ
a = Decimal("0.1")
b = Decimal("0.2")
print(a + b)              # 0.3   ← ピタリ
print(a + b == Decimal("0.3"))  # True

注意点が1つ。Decimal(0.1)(floatから生成)だと、あの 0.1000...0555 をそのまま取り込んでしまう。必ず Decimal("0.1")文字列から 作る。速度は double より遅い(内部で桁を10進で持つため)ので、金額のように「速さより正確さ」の場面で使う。生成方法の違いはPython公式のdecimalドキュメントに明記されている。

言語別の落とし穴早見

同じ double を使う言語でも、表示の丸め方が違うので「言語のせい」と勘違いしやすい。

言語 0.1 + 0.2 の表示 メモ
JavaScript 0.30000000000000004 生の値をそのまま表示
Python 3 0.30000000000000004 repr は最短往復表現
Java 0.30000000000000004 System.out.println
Ruby 0.30000000000000004 puts
PHP 0.3 デフォルト精度14桁で丸めて隠す

PHPは初期設定の表示精度が低いので 0.3 に見えるが、内部の値は他と同じ。「PHPだけ正しい」わけじゃなく、誤差を見せていないだけ。この挙動の違いは各言語の実装ではなく 表示層のポリシーの差 で、中身のビットは全言語共通。丸めの再現は0.30000000000000004.comで各言語横断の実測値がまとまっている(外部・二次情報)。

まとめとアクション

0.1 + 0.2 がズレるのはCPUのバグでも言語のバグでもなく、「有限のビットに、2進数だと循環する小数を詰めた」代償。防ぎ方は3つ、用途で選ぶ。

  • 比較なら → イプシロン比較(math.isclose / Number.EPSILON
  • 金額なら → 最小単位の整数で持つ
  • 正確さ優先なら → Decimal / BigDecimal(文字列から生成)

今日やること(5分): 手元のNodeかPythonで 0.1 + 0.2Decimal("0.1") + Decimal("0.2") を打ち比べて、内部値を Decimal(0.1) で覗く。

今週やること(30分): 自分のコードを grep -rnE '==\s*[0-9]+\.[0-9]' src/ で洗い、小数の等値比較を洗い出してイプシロン比較へ置換する。

今月やること: 金額を扱う箇所を棚卸しして、float/number で持っている金額を「最小単位の整数」か Decimal に寄せる方針を1本決める。既存コードは新規計算処理から順に移す。

小数が絡む計算を書くたびに「これは2のべき乗で割り切れる数か?」と一度だけ自問する。それだけで踏み抜く回数が激減する。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?