- 技術 💻
浮動小数点の落とし穴8選|0.1+0.2・丸め・金額計算
コンピュータは計算が得意。そう思っていた頃が、誰にでもあるんですよね。
でも小数の世界では、足し算の順番を変えただけで答えが変わったり、同じ値どうしの比較が偽になったりします。今回は、知らないと請求金額がズレるものを8つ集めました。1つでも知らなかったら儲けものです。
① 0.1 + 0.2 は、0.3 になりません
有名なやつですが、まずここからです。
0.1 + 0.2
// → 0.30000000000000004
バグではなく、仕様どおりの正しい動作です。
コンピュータは数を2進数で持ちます。ところが0.1は、2進数で表すと無限に続く循環小数になるんですよね。10進数で1/3が0.3333…と終わらないのと同じ理屈です。無限は入りきらないので、どこかで打ち切る。その誤差が、足したときに表に出てくるわけです。
「小数はほぼ全部、近い値であって正確な値ではない」——ここが出発点です。
② 足す順番を変えると、答えが変わります
数学では a+b+c は、どの順で足しても同じですよね。小数では、変わります。
0.1 + 0.2 + 0.3 # → 0.6000000000000001
0.3 + 0.2 + 0.1 # → 0.6
途中で丸められる位置が変わるからなんです。
だから大量の小数を合計するときは、順番によって結果がわずかに変わります。大きい値と小さい値を混ぜて足すと、小さい方が飲み込まれて消えることもあるんですよね。
集計バッチで「昨日と1円だけ合わない」の原因が、これだったりします。
③ 自分自身と等しくない値が、存在します
NaN(非数)という値があります。0÷0のような、答えが定義できない計算の結果ですね。このNaN、自分自身と比較しても false になります。
NaN === NaN // → false
「x が NaN かどうか」を == で判定できないので、専用の関数(isNaNなど)が用意されているんです。
逆にこの性質を使うと、「x !== x が true なら、x はNaN」という判定が書けます。理屈は通っているのに、初見だと二度見するコードです。
④ 0には、プラスとマイナスがあります
小数の世界には、-0(マイナスゼロ)が存在します。
-0 === 0 // → true(等しい)
1 / 0 // → Infinity
1 / -0 // → -Infinity(結果が違う!)
比較すると等しいのに、割り算に使うと結果が変わる。「等しいのに、同じではない」という不思議な状態なんですよね。
負の値の計算が0に丸まったとき、符号だけが残ったのが -0 です。どちら側から0に近づいたかの記録が残っている、と考えると腑に落ちます。
⑤ 金額を小数で持つと、いつか事故ります
ここが実務でいちばん大事なところです。
①〜②のとおり、小数の計算には誤差が出ます。お金の計算に、誤差は許されません。だから金額の扱いには、定石が2つあります。
- 最小単位の整数で持つ:円ならそのまま整数、ドルならセント単位の整数にする方法
- 10進数専用の型を使う:Javaの
BigDecimal、PythonのDecimal、SQLのDECIMAL型など
「金額に浮動小数点を使わない」——これは知識というより、業務システムの世界の作法なんです。
⑥ 0.5の丸めが、四捨五入とは限りません
0.5を四捨五入したら1。そう思いますよね。
ところが多くの言語やDBには、0.5のときは近い方の偶数に丸めるという方式があります。銀行家の丸めと呼ばれていて、浮動小数点の国際規格の既定もこれです。たとえばPythonのround()は、この方式で動きます。
round(0.5) # → 0(偶数の0へ)
round(1.5) # → 2(偶数の2へ)
round(2.5) # → 2(偶数の2へ!)
なぜこんなことをするのか。いつも切り上げると、合計が実際より大きく偏っていくからです。偶数側に交互に振ることで、大量に丸めたときの偏りを打ち消すんですよね。
「1件ずつは正しいのに、合計だけ合わない」の犯人になりやすいので、どの丸め方式かを仕様で決めるのが定石です。
⑦ 誤差が、迎撃ミサイルを外させたことがあります
小さな誤差が、実際に大惨事を招いた例が残っています。
1991年の湾岸戦争で、迎撃ミサイルシステムが飛来するミサイルを撃ち損ねた事故がありました。原因は、時間を小数で数えていたことによる、わずかな誤差の蓄積です。
システムを起動したまま長時間動かし続けると、誤差が少しずつ積み上がる。100時間後には、およそ0.34秒という無視できない時間のズレになっていました。
誤差は、1回では見えなくても、積み重ねると事故になる。今日の話が「豆知識」で終わらない理由が、ここにあります。
⑧ 大きすぎる整数も、静かに壊れます
小数だけの話ではありません。JavaScriptの数値は内部が浮動小数点なので、扱える整数にも上限があります。
9007199254740992 === 9007199254740993
// → true(別の数なのに等しい!)
上限を超えると、隣り合う整数を区別できなくなるんです。エラーは出ません。黙って別の値になるのがいちばん怖いところですよね。
だからIDや金額のような大きな整数をJSONで受け渡すときは、数値ではなく文字列で持つのが安全策です。SNSのAPIがIDを文字列で返すのは、この問題への対処なんです。
番外編:手元で確かめられる小ネタ
電卓アプリは、なぜ0.3と表示するのか
内部では同じ誤差が出ていますが、表示の直前に丸めているものが多いんです。「見えていない」だけで、無いわけではないんですよね。
0.1を3回足しても0.3にならないのに
0.1 + 0.1 + 0.1 // → 0.30000000000000004
0.1 * 3 // → 0.30000000000000004
やり方を変えても、誤差は誤差。逃げられません。
比較は「差が小さいか」で行う
Math.abs(a - b) < 1e-9
小数の等価比較は == ではなく、この形が定石です。許容誤差をいくつにするかは、扱う値の大きさで決めます。
RJCでも、数字と向き合っています
RJCが携わるのは、保険、金融、決済、交通。1円のズレが、そのまま障害報告になる世界です。
だから金額の型をどうするか、丸め方式をどこで決めるかは、設計レビューの定番論点なんですよね。
今日のような知識を、Techチームで共有しながら開発しています。「なぜ誤差が出るか」を説明できる人が、設計で信頼される領域なんです。
よくいただくご質問
Q. まずどれから押さえるべきですか?
A. ⑤の金額の扱いかなと思います。次の設計から効きますし、事故の大きさが桁違いなんですよね。
Q. 小数を使ってはいけない、ということですか?
A. いえ、適材適所です。科学技術計算やグラフィックスでは浮動小数点が正解です。誤差が許されない用途に使わない、という話なんです。
Q. 丸め方式は、どこで決めればいいですか?
A. 仕様書に明記するのが定石です。言語・DB・帳票ツールで既定の方式が違うことがあるので、揃っているかの確認までがセットですね。
Q. 未経験からでも、ここまで分かるようになりますか?
A. なります。今日の例は、ブラウザの開発者ツールに打ち込めば全部その場で再現できます。体験すると、一生忘れないんですよね。
「計算は正確」という思い込みを、まず外す
今日の8つに共通しているのは、これでした。
コンピュータの計算は速いけれど、小数においては正確ではなく「とても近い」。
その前提を知っているかどうかで、設計もデバッグも変わります。1円のズレを追う日が来たとき、真っ先に疑える人になれるかどうかなんですよね。
みなさんが小数の誤差で踏んだ事故、よかったら教えてください。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。