プログラムで0.1+0.2が0.3にならない、と表示されて戸惑ったことはありませんか。
これはバグではなく、コンピュータが小数を2進数で近似して扱うしくみ(IEEE 754)の性質です。
この記事では、原因と実用的な対処法を初心者向けにやさしく解説します。
「なぜそうなるのか」と「どう避けるか」を順に理解していきます。
浮動小数点数(IEEE 754)の基本
小数はビットで近似する
コンピュータは0や1のビットで数を表現します。
小数も例外ではなく、浮動小数点数は「符号」「指数」「仮数(有効桁)」の3つに分けて、実数を近似して表現します。
直感的には、10進数でいうところの「1.23 × 10の何乗」を、2進数版の指数表記で持っているイメージです。
イメージ
- 符号: プラスかマイナスか。
- 指数: 桁をどのくらい動かすか。
- 仮数: 重要な桁(有効数字)を保持する部分。
この方式により、非常に大きい数から非常に小さい数まで幅広く扱えますが、仮数に割り当てられたビット数が有限であるため「ぴったり」表せない小数が多くあります。
固定ビット数と精度の限界
浮動小数点はビット数が決まっています。
例えば倍精度(64ビット)では、仮数部は52ビットで、約15〜17桁の10進有効桁を表現できます。
これは多くの処理には十分ですが、全ての小数を正確に格納できるわけではありません。
表せない値は「最も近い表現可能な値」に丸められます。
- ビット数が決まっているため、無限に続く小数はどこかで打ち切り(丸め)が必要です。
- 丸めた誤差が計算の途中や表示で「目に見える」ことがあります。
10進小数と2進小数の違い
10進では有限小数でも、2進では有限にならないことがよくあります。
0.5や0.25は2の累乗分の1なので2進で有限ですが、0.1や0.2は2進では無限に続く小数になります。
- 5(10進) = 0.1(2進) → 有限
- 25(10進) = 0.01(2進) → 有限
- 1(10進) ≈ 0.0001100110011…(2進) → 無限小数
- 2(10進) ≈ 0.001100110011…(2進) → 無限小数
つまり、0.1 も 0.2 も内部では「近いけれどズレた値」として格納されます。
倍精度(64ビット)が一般的
現在の多くの言語やランタイムでは、標準の数値型に倍精度(64ビット)を採用しています。
特にJavaScriptのnumber、Pythonのfloat、Goのfloat64(デフォルト推奨)などは64ビット倍精度です。
一方で、GPUやメモリ節約が重要な場面では単精度(32ビット)が使われることもあります。
以下は代表的な違いの目安です。
| 形式 | ビット数 | おおよその10進有効桁 | 主な用途 |
|---|---|---|---|
| 単精度(float) | 32 | 約6〜7桁 | グラフィックス、メモリ節約が重要な場面 |
| 倍精度(double) | 64 | 約15〜17桁 | 一般的なアプリケーション、科学技術計算 |
初心者の方は、まず倍精度前提で理解すると学習しやすいです。
なぜ0.1+0.2は0.3にならないのか
0.1は2進数で無限小数になる
すでに見たとおり、0.1や0.2は2進で無限に続く小数なので、有限ビットでは途中で切り上げや切り捨てが必要です。
保存されるのは「最も近い表現可能な値」ですが、そこには微小な誤差が含まれます。
実例(表示)
>>> 0.1 + 0.2
0.30000000000000004
console.log(0.1 + 0.2); // 0.30000000000000004
丸め誤差が加算で表に出る
丸め誤差は単体では目立ちませんが、加減乗除を重ねると誤差が表面化します。
0.1も0.2も「ややズレた値」なので、その和も「ややズレた値」になるのです。
イメージ
- 1 ≈ 0.10000000000000000555…
- 2 ≈ 0.20000000000000001110…
- 和 ≈ 0.30000000000000004440… → 0.30000000000000004 と表示
誤差はとても小さいのですが、表示桁数が多いと見えてしまいます。
0.30000000000000004と表示される理由
多くの言語は、数値を文字列に変換する際に「その数を再現できるだけの桁数」を出力しようとします。
そのため、内部のわずかなズレが見える形で「0.30000000000000004」と表示されます。
表示の問題に見えますが、根本は内部表現の丸めです。
==比較が失敗する理由
プログラムの等値比較(==など)は、ビットに格納された値が完全に同じかを見ます。
0.3(リテラル)の「最も近い値」と、0.1+0.2の「最も近い値」は、丸めの経路が違うため一致しません。
実例(比較)
0.1 + 0.2 === 0.3 // false
(0.1 + 0.2) == 0.3 # False
数値の等値比較に == を安易に使うのは危険です。
次のセクションで安全な比較方法を説明します。
初心者向けの対処法とベストプラクティス
許容誤差(epsilon)で比較する
最も基本的な方法は、「十分に近ければ同じとみなす」ことです。
差が小さな閾値(イプシロン)以下なら等しいと判断します。
桁スケールが大きく変わる場合は、相対誤差も考慮します。
実例
function isClose(a, b, eps = 1e-12) {
return Math.abs(a - b) <= eps * Math.max(1, Math.abs(a), Math.abs(b));
}
isClose(0.1 + 0.2, 0.3); // true
import math
math.isclose(0.1 + 0.2, 0.3) # True (デフォルトの相対/絶対許容誤差)
標準関数がある言語では、それを使うのが安全です。
金額は整数で扱う
金額のように「誤差が許されない」領域では、最小単位で整数として扱うのが基本です。
円ならそのまま整数、ドルならセント(100倍)で整数化します。
金額を浮動小数点で直接計算しないのが鉄則です。
実例
1000円 + 250円
const totalYen = 1000 + 250; // 1250 (整数)
// $10.99 + $0.10
const totalCents = 1099 + 10; // 1109 cents -> $11.09
税計算など丸め規則がある場合は、どのタイミングで四捨五入するか(毎行・小計・総計)を仕様として統一します。
Decimal(任意精度)を使う
金融や高精度が必要な場面では、10進の任意精度(Decimal/BigDecimal)を使います。
これは10進の有理数を正確に扱いやすく、0.1のような値を「正確に」表現できます。
実例
from decimal import Decimal, getcontext
getcontext().prec = 28
Decimal("0.1") + Decimal("0.2") == Decimal("0.3") # True
import java.math.BigDecimal;
new BigDecimal("0.1").add(new BigDecimal("0.2"))
.compareTo(new BigDecimal("0.3")) == 0; // true
const Decimal = require('decimal.js');
new Decimal('0.1').plus('0.2').equals(new Decimal('0.3')); // true
文字列から生成するのがポイントです。
0.1のような浮動小数点から直接変換すると誤差を引き継ぎます。
出力は丸めてフォーマットする
画面やレポートに出すときは、必要な小数点以下桁数に丸めてフォーマットします。
内部の誤差が見えないようにしながら、表記を統一できます。
実例
(0.1 + 0.2).toFixed(2); // "0.30"
format(0.1 + 0.2, ".2f") # '0.30'
丸め方(四捨五入、銀行丸めなど)は要件に合わせて選びます。
テストは近似比較にする
単体テストで浮動小数点を直接 == 比較すると不安定になります。
「近似比較」を使うのが実務的です。
実例
from pytest import approx
assert (0.1 + 0.2) == approx(0.3, rel=1e-12, abs=1e-12)
expect(0.1 + 0.2).toBeCloseTo(0.3, 12);
つまずきやすいポイントと確認方法
足し算の順序で結果がわずかに変わる
浮動小数点では、(a + b) + c と a + (b + c) が異なる結果になる場合があります。
これは丸めが各ステップで起きるためです。
特に、非常に大きな数と非常に小さな数の足し算で差が出やすいです。
実例
const a = 1e16, b = 1, c = -1e16;
console.log((a + b) + c); // 0 (bが消える)
console.log(a + (b + c)); // 1
対策として、小さい値同士・大きい値同士から集計する(ペア和)やKahan法などの手法が知られていますが、初心者はまず「順序によって誤差が変わる」点を覚えておくと良いです。
JSONや文字列化で桁が増える
JSONやログに数値を出力すると、内部のわずかなズレが露出して桁が増えて見えることがあります。
これは不具合ではなく、復元可能な表現を優先するための仕様です。
実例
JSON.stringify(0.1 + 0.2); // "0.30000000000000004"
(0.1 + 0.2).toPrecision(12); // "0.300000000000"
画面表示用には toFixed やフォーマット関数を使い、「見せる値」と「内部値」を分けると混乱が減ります。
入力のパースと丸めに注意
文字列から数値に変換する際、parseの時点で浮動小数点にすると誤差が入り得ます。
金額や桁が重要な値は、Decimalでパースするか、整数(最小単位)に変換するのが安全です。
また、ロケール(小数点がカンマの地域)にも注意が必要です。
ヒント
- 文字列 → Decimal/BigDecimal へ直接。
- 文字列 → 整数(最小単位にスケール) → 計算 → 最後に表示用フォーマット。
言語別の注意点
各言語の標準的な数値型と、おすすめの回避策をざっくりまとめます。
迷ったら「金額は整数かDecimal」「比較は近似」を合言葉にしましょう。
| 言語 | 標準の実数型 | 金額向け | 近似比較 |
|---|---|---|---|
| JavaScript | number(倍精度) | 整数(セント)または decimal.js 等 | 独自isClose/テストでtoBeCloseTo |
| Python | float(倍精度) | Decimal | math.isclose/pytest approx |
| Java | double(倍精度) | BigDecimal | 誤差許容の自前比較 |
| C# | double(倍精度) | decimal 型 | 誤差許容の自前比較 |
| Go | float64(推奨) | 整数スケール/shopspring/decimal 等 | 誤差許容の自前比較 |
| Ruby | Float(倍精度) | BigDecimal | (a – b).abs < eps |
| Swift | Double(倍精度) | Decimal/NSDecimalNumber | (a – b).magnitude < eps |
どの言語でも「文字列からDecimal/BigDecimalを生成」するのが重要です。
まとめ
0.1+0.2が0.3にならないのは、コンピュータが小数を2進数で有限ビット近似する(IEEE 754)ためです。
0.1や0.2は2進で無限小数になり、丸め誤差が加算で表面化して「0.30000000000000004」と表示されます。
等値比較(==)が失敗するのも、この微小な差が原因です。
実務では、(1) 許容誤差で比較する、(2) 金額は整数やDecimalで扱う、(3) 表示は丸めてフォーマット、(4) テストは近似比較、といった定番のベストプラクティスで安全に取り扱えます。
これらを押さえておけば、浮動小数点の「不思議」は予測可能な挙動に変わり、安心してプログラミングが進められます。
