Loading post
Jul 23, 2026

ブラウザの開発者ツールでも、Pythonの対話環境でも、なんでもいいので開いて、これを打ってみてください。
0.1 + 0.2
// → 0.30000000000000004
0.3になりません。バグではありません。ほぼ全ての言語で、同じ結果になります。
僕は決済を本業にしていて、PayPalでも今のスタートアップでも「お金を1円もずらさずに動かす」のが仕事です。その世界で一番最初に叩き込まれるルールが、お金をfloat(浮動小数点数)で持つな、です。この記事では、なぜ0.1 + 0.2が0.3にならないのか、なぜそれが決済で致命的なのか、正しくはどう持てばいいのか、そしてTypeScript / Java / Python / C / Go のそれぞれで具体的にどう書くかまで、一気に書きます。長いですが、これ一本で「お金の持ち方」が分かるようにしたつもりです。コードをあまり書かない人でも、前半だけで「へえ、コンピュータってそうなってるんだ」と思ってもらえると思います。
0.1 + 0.2だけじゃありません。お金っぽい計算をさせると、あちこちで小数のゴミが出てきます。実際にJavaScriptで動かした結果です。
0.1 + 0.2 // → 0.30000000000000004
0.7 * 100 // → 70 (これはたまたま無事)
1.1 * 3 // → 3.3000000000000003
Math.round(1.005 * 100) // → 100 ← 101 を期待したのに!
(2.675).toFixed(2) // → "2.67" ← 2.68 のはずが!(標準の toFixed すら間違える)
(0.1 + 0.2) === 0.3 // → false
4つ目と5つ目が地味に怖い。1.005を「小数第2位で四捨五入して整数の銭にしよう」と思って* 100してroundしたのに、100。本来101銭になるはずが1銭ずれます。そして5つ目、言語標準のtoFixed(2)ですら2.675を2.68ではなく2.67にする。これは「自分の書き方が悪い」のではなく、浮動小数点そのものの性質です。
1件なら誤差ですが、これが何百万件と積み重なる決済システムだと、月末の突合(帳簿合わせ)で「1円合わない」という地獄が始まります。
理由はシンプルで、コンピュータは数を2進数(0と1)で持っているからです。
僕たちが普段使う10進数でも、割り切れない数があります。1 ÷ 3 = 0.3333...。10進数では1/3をきれいに書けなくて、どこかで打ち切った近似値しか持てません。
同じことが2進数でも起きます。人間には0.1はきれいな数に見えますが、これを2進数で書こうとすると0.0001100110011...と無限に続いてしまう。2進数の世界では0.1が「割り切れない数」なんです。 だからコンピュータは0.1そのものではなく、限界まで近づけた近似値を保存します。
10進数の世界: 1/3 = 0.3333... ← きれいに書けない
2進数の世界: 0.1 = 0.00011001... ← きれいに書けない(!)
その近似値を2つ足す(0.1 + 0.2)と、近似の誤差も一緒に足されて、0.30000000000000004という「本物の0.3のすぐ隣」に着地します。この方式には正式名称があって、IEEE 754という「浮動小数点数」の規格です。floatやdoubleと呼ばれるやつの正体がこれです。
大事なのは、**浮動小数点数は「速くて、そこそこ正確」な代わりに「厳密ではない」**という点です。物理シミュレーションや機械学習では、そこそこ正確なら十分なので大活躍します。でもお金は違う。お金は「そこそこ」では困る。1円は1円で、0.9999999円ではないんです。
理由は3つあります。
999.9999999円になっていたら、それはもうバグです。つまり決済では「厳密さ」が機能要件そのものなんです。じゃあどう持てばいいのか。原則は3つです。
決済でお金を持つ3原則
① 基本は「整数の最小単位」で持つ(ドルはセント、円は円)
② 小数がどうしても要るときだけ Decimal 型(専用の10進数型)を使う
③ float / double は絶対に使わない
以下、順に見ていきます。
一番シンプルで、業界標準の答えがこれです。小数を使わず、通貨の最小単位を整数で持つ。
ドルなら「セント」で持ちます。$10.50は10.5(小数)ではなく1050(整数のセント)として保存する。整数の足し算・引き算に小数の誤差は一切ないので、1050 + 2075 = 3125は永遠に正確です。
// ❌ 悪い: 小数で持つ
let priceDollars = 10.50;
let total = priceDollars * 3; // 31.499999999999996 になりうる
// ✅ 良い: 最小単位(セント)を整数で持つ
let priceCents = 1050; // $10.50 を 1050 セントとして持つ
let totalCents = priceCents * 3; // 3150。永遠に正確
// 表示する瞬間だけ、10進数に戻す
const display = `$${(totalCents / 100).toFixed(2)}`; // "$31.50"
Stripeのような決済APIが金額をamount: 1050(セント)で受け渡しするのは、まさにこの理由です。保存と計算は整数の最小単位、10進数に戻すのは画面に出す一瞬だけ、というのが鉄則です。
ここで多くの人が踏む落とし穴があります。「最小単位ってことは、全部100で割ればいいんでしょ?」——違います。通貨によって小数点以下の桁数がバラバラなんです。
通貨 最小単位 例
────────────────────────────────────────────
円 (JPY) 1円(小数なし) ¥1000 → 1000
ドル (USD) 1セント = 1/100 $10.50 → 1050
ディナール(BHD) 1/1000 1.234 → 1234
一部の暗号資産 1/100000000 0.00000001 まで
────────────────────────────────────────────
日本円には実用上「銭」という下位単位がありません。だから円の1000は「1000銭」ではなく「1000円」そのもの。逆にバーレーン・ディナールは小数第3位まである。「金額 × 100」というコードは、円では多すぎ、ディナールでは足りないわけです。
正しくは、通貨ごとに「小数点以下の桁数」を持っておきます(ISO 4217という国際規格で決まっています)。
// 通貨ごとの小数桁数(minor unit exponent)
const CURRENCY_DECIMALS: Record<string, number> = {
JPY: 0, // 円: 最小単位そのものが1円
USD: 2, // ドル: 1/100
EUR: 2,
BHD: 3, // ディナール: 1/1000
};
ユーザーが打った文字列"10.50"を、整数の最小単位1050に変える。ここに「簡易版」と「堅牢版」の2通りがあって、どっちを使うべきかがこの記事で一番実務的な話です。
function toMinorUnitsSimple(amount: string, currency: string): number {
const decimals = CURRENCY_DECIMALS[currency] ?? 2;
return Math.round(parseFloat(amount) * 10 ** decimals);
}
toMinorUnitsSimple('10.50', 'USD'); // 1050
toMinorUnitsSimple('1000', 'JPY'); // 1000
parseFloat(amount) * 10 ** decimalsした時点では4.35 * 100 = 434.99999...のようなゴミが出ます。でも入力が元々その桁数までなら、誤差は0.5より遥かに小さく、最後のMath.roundが正しい整数に戻してくれます。普段のアプリなら、正直これで十分です。
ただ、簡易版は「誤差が0.5未満に収まっている」ことに賭けているだけなので、次のケースで破れます。これが「なぜ簡易版に頼りきってはいけないか」の具体例です。
① 入力より細かい桁を丸めると、round が裏目に出る。
(1.005).toPrecision(20); // "1.0049999999999998..." ← 1.005 より少し下に保存されている
Math.round(1.005 * 100); // → 100 (101 を期待したのに!)
(2.675).toFixed(2); // → "2.67" (2.68 のはずが。標準の toFixed すら間違える)
1.005も2.675も、2進数では少しだけ下にズレて保存されるので、四捨五入すると切り下がります。「掛けてroundすればいい」が効かない典型です。金利や割引の計算で、入力より細かい桁が出た瞬間にこれが起きます。
② 計算を重ねると誤差が育って、いずれ round でも救えなくなる。
let s = 0;
for (let i = 0; i < 10; i++) s += 0.1;
s; // → 0.9999999999999999
このくらいならMath.round(s * 100)でまだ100に救えます。でも掛け算・割り算が絡む長い計算では、誤差が0.5を超えた瞬間にroundでも救えなくなる。「最後にroundすればいい」は、誤差が0.5未満に収まっていることに賭けているだけなんです。
③ 桁が大きいと float の整数精度そのものが足りなくなる。(後述のJSの罠)
つまり簡易版は「小さくてきれいな入力の1回変換」なら大丈夫だけど、それ以外では静かに間違える。だから決済コードでは次の堅牢版を使います。
厳密さが命の決済コードは、parseFloatで一瞬でもfloatを作らず、文字列のまま整数へ変換します。「誤差が0.5未満」という前提に一切依存しないためです。
function toMinorUnits(amount: string, currency: string): number {
const decimals = CURRENCY_DECIMALS[currency] ?? 2;
const [intPart, fracPart = ''] = amount.split('.'); // "10.5" → ["10","5"]
// 小数部を「ちょうど decimals 桁」に揃える。
// 足りなければ後ろに0を足し、多ければ切り捨てる、を1式で両対応:
// "5" → "5"+"00"="500" → 先頭2桁 "50" (0で埋めた)
// "50" → "50"+"00"="5000" → 先頭2桁 "50" (足した0は捨てられるだけ)
// "999" → "999"+"00"=… → 先頭2桁 "99" (切り捨て)
const frac = (fracPart + '0'.repeat(decimals)).slice(0, decimals);
return Number(intPart + frac); // "10" + "50" = "1050" → 1050
}
'0'.repeat(decimals)は「桁が足りない入力のための保険」です。"10.50"のように元から足りている入力では、足した0はsliceに捨てられて何もしません(だから一見ムダに見える)。でも"10.5"や"10"では、この0が埋めてくれる。おかげで"10.5"も"10.50"も同じ1050になります。
とはいえ、実務でこれを手書きすることは稀で、**本当の業界標準は「金額・10進数専用ライブラリを使う」**ことです。中でやっているのは、まさにこの「floatを作らず文字列/10進数のまま扱う」処理。各言語の標準ライブラリは後半でまとめます。
上の①②で見た通り、答えは「1回のきれいな変換なら効くが、一般には効かない」です。roundは便利ですが、floatを使う言い訳にはできない。正しい方針は「roundで後から救う」ではなく、そもそも整数の最小単位で持って、浮動小数点の誤差を1ミリも発生させないこと。そうすればroundに祈る必要がなくなります。
整数の最小単位で足りるのは「決まった額のやり取り」までです。でも、金利計算や、1個あたりの単価に個数を掛ける、為替で換算する、といった場面では小数が避けられません。
そのときはfloatではなく、Decimal型(10進数を正確に扱う専用の型)を使います。これらは「人間の10進数」をそのまま正確に持つので、0.1 + 0.2がちゃんと0.3になります。代わりにfloatより遅くてメモリを食う。「速いけど不正確なfloat」と「遅いけど正確なDecimal」のトレードオフで、お金では常に正確さを取る、というだけの話です。実際にPythonで動かすとこうなります。
from decimal import Decimal
Decimal("0.1") + Decimal("0.2") # → Decimal('0.3') ← ちゃんと 0.3!
Decimal(0.1) # → Decimal('0.1000000000000000055511...') ← float から作ると罠
最後の行が重要で、Decimalをfloatから作ると、floatの誤差ごと取り込んでしまう。必ず「文字列から」作ります。これは全言語共通の落とし穴です。
整数で正確に持っていても、割り算だけは避けられない場面があります。割り勘です。¥1000を3人で割ると333.33...円。誰かが1円を余分に食うか、1円を得します。
素朴にMath.round(1000 / 3)を3人分足すと、333 × 3 = 999。1円が消えます。逆に切り上げると334 × 3 = 1002で、2円が湧いて出る。合計が元の金額と合わない——台帳としては失格です。
正しくは、「最大剰余法」のように割った端数を決定的に誰かへ配るやり方をします。
// ¥1000 を3人に、合計が必ず1000になるように割る
function splitEvenly(total: number, n: number): number[] {
const base = Math.floor(total / n); // 333
const remainder = total - base * n; // 1000 - 999 = 1(余りの1円)
// 余りの1円を、先頭の1人に配る(誰に配るかは決定的なルールで決める)
return Array.from({ length: n }, (_, i) => base + (i < remainder ? 1 : 0));
// → [334, 333, 333] 合計はちゃんと 1000
}
僕が作った割り勘アプリSettliでも、この「端数を誰が食うか」は明示的なポリシーとして実装しています。友達同士なら「50銭未満は誰も食わない(黙って消す)」で許されますが、本番の台帳では消えた端数を丸め勘定で追跡して決定的に配賦します。同じ算数でも、社会的な契約が違うわけです。
ここからが実装の本編です。TypeScript / Java / Python / C / Go の5つで、「ダメな書き方 → 整数の最小単位 → 小数が要るときの標準ライブラリ」を並べます。どの言語でも結論は同じ——整数の最小単位を基本に、小数はfloatではなくDecimalで、floatは経由しない。
JSには整数型がなく、numberは全部64bit浮動小数点(double)です。ここが最大の注意点。
// ❌ ダメ: number は全部 double
const total = 0.1 + 0.2; // 0.30000000000000004
// ✅ 整数の最小単位。number は 2^53(約9007兆)まで整数を正確に持てる
let priceCents = 1050;
let totalCents = priceCents * 3; // 3150
// 2^53 を超えうるなら BigInt(末尾 n)
let huge = 9_007_199_254_740_993n; // bigint
// ✅ 小数が要るなら decimal.js、お金の型が欲しいなら dinero.js
import Decimal from 'decimal.js';
new Decimal('0.1').plus('0.2').toString(); // "0.3"
new Decimal('1.005').times(100).toString(); // "100.5"(ずれない)
Javaはdoubleと、正確なBigDecimalの両方を持っています。罠は「BigDecimalをdoubleから作ってしまう」こと。
// ❌ ダメ: double は不正確
double total = 0.1 + 0.2; // 0.30000000000000004
// ❌ さらにダメ: BigDecimal を double から作ると誤差ごと取り込む
new BigDecimal(0.1); // 0.1000000000000000055511151231257827021181...
// ✅ 整数の最小単位は long
long priceCents = 1050L;
long totalCents = priceCents * 3; // 3150
// ✅ 小数は BigDecimal を「文字列から」作る
BigDecimal a = new BigDecimal("0.1");
BigDecimal b = new BigDecimal("0.2");
a.add(b); // 0.3
new BigDecimal("1.005")
.setScale(2, RoundingMode.HALF_UP); // 1.01(正しく丸まる)
// お金専用の型が欲しいなら JSR-354(javax.money / Moneta)や Joda-Money
Pythonのintは多倍長(桁あふれの心配なし)、そして標準ライブラリにdecimal.Decimalがあります。
# ❌ ダメ: float は不正確。Decimal を float から作るのも罠
0.1 + 0.2 # 0.30000000000000004
Decimal(0.1) # Decimal('0.1000000000000000055511151231257827...')
# ✅ 整数の最小単位(int は多倍長なので桁あふれしない)
price_cents = 1050
total_cents = price_cents * 3 # 3150
# ✅ 小数は Decimal を「文字列から」作る
from decimal import Decimal, ROUND_HALF_UP
Decimal("0.1") + Decimal("0.2") # Decimal('0.3')
Decimal("1.005").quantize(Decimal("0.01"), ROUND_HALF_UP) # Decimal('1.01')
# お金専用の型が欲しいなら py-moneyed
C/C++の標準には長らく10進小数型が無く、固定小数点(64bit整数のセント)で持つのが定番です。
#include <stdint.h>
#include <stdio.h>
// ❌ ダメ: double は不正確
double total = 0.1 + 0.2; // 0.30000000000000004...
// ✅ 整数の最小単位。64bit 整数で持つ
long long price_cents = 1050;
long long total_cents = price_cents * 3; // 3150
// 表示するときだけ 10進に戻す(整数の商と剰余で組み立てる)
printf("$%lld.%02lld\n", total_cents / 100, total_cents % 100); // $31.50
// ※ 負の金額を扱うなら符号の扱いに注意(剰余が負になる)
小数がどうしても要るなら、C23の_Decimal64(10進浮動小数点、IEEE 754-2008)やGCCのdecimal-float拡張、C++ならBoost.Multiprecisionのcpp_dec_floatといった選択肢があります。ただ、決済ロジックの大半は整数のセントで十分回ります。
Goにも組み込みの10進型は無く、**整数の最小単位(int64)か、デファクト標準のshopspring/decimal**を使います。
// ❌ ダメ: float64 は不正確
total := 0.1 + 0.2 // 0.30000000000000004
// ✅ 整数の最小単位は int64
priceCents := int64(1050)
totalCents := priceCents * 3 // 3150
// ✅ 小数が要るなら shopspring/decimal(Goのデファクト標準)
import "github.com/shopspring/decimal"
a, _ := decimal.NewFromString("0.1")
b, _ := decimal.NewFromString("0.2")
a.Add(b) // 0.3
// お金専用の型が欲しいなら rhymond/go-money
5言語に共通するパターンを並べるとこうです。
言語 整数の最小単位 小数の標準 float から作る罠
──────────────────────────────────────────────────────────────
TS/JS number / bigint decimal.js / dinero new Decimal(0.1)
Java long BigDecimal new BigDecimal(0.1) ← 有名な罠
Python int(多倍長) decimal.Decimal Decimal(0.1)
C/C++ int64_t _Decimal64 / Boost (そもそも double を使わない)
Go int64 shopspring/decimal decimal.NewFromFloat(0.1)
──────────────────────────────────────────────────────────────
共通ルール: 基本は整数の最小単位、小数は Decimal を「文字列から」、float は経由しない
最後に、言語をまたいで踏むやつを3つ。
① 入力パースは「変換したら即整数」まで一気にやる。 parseFloatしたfloatのまま保存したり足し引きに使ったりしない。整数の最小単位にしてしまえば、以降は誤差ゼロです。
② JavaScriptの数値は全部 double。 前述の通り、約9000兆(2^53)を超える整数は整数ですら正確に持てません。円は桁が大きいので要注意。実際にこうなります:
Number.MAX_SAFE_INTEGER; // 9007199254740991
Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2; // → true(!)
③ データベースの型に注意。 僕のスタックはFirebase/Firestoreですが、Firestoreの数値は内部的に全部doubleです(専用のdecimal型がない)。だから「Firestoreに0.1を保存する」時点で、もう浮動小数点の世界に入っています。金額は最小単位の整数か、文字列で保存するのが安全です。使っているDBが金額をどう持つか、一度確認する価値があります(PostgreSQLのNUMERIC、MySQLのDECIMALのように正確な10進型を持つDBもあります)。
0.1 + 0.2が0.3にならないのは、2進数では0.1が割り切れない数だから。 1/3が10進数で割り切れないのと同じ現象で、バグではなく仕様floatは「速いけどそこそこ正確」。お金は「そこそこ」が許されないので、floatで持ってはいけない1.005や2.675、桁の大きい値、計算の積み重ねで静かに間違える。 決済ではfloatを一度も作らない堅牢版を使う「お金をfloatで持つな」は、決済エンジニアが最初に覚えて、最後まで忘れないルールです。そして0.1 + 0.2という小さなパーティー芸の裏には、コンピュータが数をどう見ているかという、けっこう深い話が隠れています。