2進数では 0.1 を正確に表せないためです。JavaScript 固有の問題ではありません。どこまで正確なのか、安全な比べ方、お金の扱い方までを実行して確かめます。
2 進数では 0.1 を正確に表せないからです。
1/3 を10 進数で書くと 0.333… と終わらないのと同じことが、0.1 に起きています。
example.js ▶ 実行↺ 戻す
console.log (0.1 + 0.2 );
console.log (0.1 + 0.2 === 0.3 );出力 書き換えて実行できます
0.30000000000000004
false
example.js ▶ 実行↺ 戻す
console.log ((0.1 ).toPrecision (20 ));
console.log ((0.2 ).toPrecision (20 ));
console.log ((0.3 ).toPrecision (20 ));出力 書き換えて実行できます
0.10000000000000000555
0.20000000000000001110
0.29999999999999998890
これは JavaScript の不具合ではありません。IEEE 754 の倍精度 を使う言語では
Python でも Java でも C でも同じ結果 になります。
実際に何が入っているのか
0.1 の中身をもっと桁数を出して見ます。
example.js ▶ 実行↺ 戻す
console.log (0.5 + 0.25 === 0.75 );
console.log ((0.5 ).toPrecision (20 ));
console.log ((0.1 ).toPrecision (20 ));出力 書き換えて実行できます
true
0.50000000000000000000
0.10000000000000000555
example.js ▶ 実行↺ 戻す
console.log (Number .MAX_SAFE_INTEGER );
console.log (Number .MAX_SAFE_INTEGER + 1 === Number .MAX_SAFE_INTEGER + 2 );
console.log (9007199254740993 );
console.log (Number .isSafeInteger (9007199254740993 ));出力 書き換えて実行できます
9007199254740991
true
9007199254740992
false
0.1 は少しだけ大きく 、0.3 は少しだけ小さく 保存されています。
0.1 + 0.2 はわずかに大きい側に寄るので、0.3 と一致しません。
画面に 0.1 と出るのは、そう見えるように丸めているから です。
中身は最初から 0.1 ではありません。
正確に表せる数もある
2 進数で書ける小数、つまり2の冪の和 で作れる数は正確です。
example.js ▶ 実行↺ 戻す
console.log (9007199254740991n + 2n );
console.log (typeof 9007199254740991n );出力 書き換えて実行できます
9007199254740993n
bigint
example.js ▶ 実行↺ 戻す
const a = 0.1 + 0.2 ;
console.log (a === 0.3 );
console.log (Math .abs (a - 0.3 ) < Number .EPSILON );出力 書き換えて実行できます
false
true
0.5 は 1/2、0.25 は 1/4 なのでぴったり入ります。
0.1 は 1/10 で、2 進数では割り切れません。
整数はどこまで正確か
小数と違い、整数はある大きさまで 正確です。
javascript
const a = 1e10 + 0.1 ; const b = 1e10 + 0.2 ; console .log ( Math .abs (a - b) < Number .EPSILON); console .log (a - b);
false
-0.10000038146972656
example.js ▶ 実行↺ 戻す
function nearlyEqual (x, y, rel = 1e-9 ) {
return Math .abs (x - y) <= rel * Math .max (Math .abs (x), Math .abs (y), 1 );
}
console.log (nearlyEqual (0.1 + 0.2 , 0.3 ));
console.log (nearlyEqual (1e10 + 0.1 , 1e10 + 0.1 ));
console.log (nearlyEqual (1 , 2 ));出力 書き換えて実行できます
true
true
false
9007199254740991(約9 千兆)を超えると、隣り合う整数を区別できなくなります 。
+1 と +2 が等しくなっているのがその証拠です。
3行目に注目してください。書いた数と、出てきた数が違います。
…993 は表せないので、リテラルを書いた時点で いちばん近い …992 に置き換わっています。
大きな整数を正確に扱うなら BigInt です。
example.js ▶ 実行↺ 戻す
console.log ((0.1 * 10 + 0.2 * 10 ) / 10 === 0.3 );
console.log (Math .round (0.1 * 10 ) + Math .round (0.2 * 10 ) === 3 );出力 書き換えて実行できます
true
true
example.js ▶ 実行↺ 戻す
let a = 0 ;
for (let i = 0 ; i < 10 ; i++) a += 19.99 ;
let b = 0 ;
for (let i = 0 ; i < 10 ; i++) b += 0.7 ;
console.log (a, a === 199.9 );
console.log (b, b === 7 );出力 書き換えて実行できます
199.9 true
7.000000000000001 false
どう比べればよいか
誤差を許して比べる
差が十分小さければ同じとみなします。Number.EPSILON は
1 の隣の数との差 で、この種の判定によく使われます。
example.js ▶ 実行↺ 戻す
let total = 0 ;
for (let i = 0 ; i < 10 ; i++) {
total += 1999 ;
}
console.log (total);
console.log (`${Math.floor(total / 100)}.${String(total % 100).padStart(2, '0')}` );出力 書き換えて実行できます
19990
199.90
javascript
console .log (( 1.005 ) .toFixed ( 2 ) , ( 1.005 ) .toPrecision ( 20 )); console .log (( 1.015 ) .toFixed ( 2 ) , ( 1.015 ) .toPrecision ( 20 )); console .log (( 1.025 ) .toFixed ( 2 ) , ( 1.025 ) .toPrecision ( 20 )); console .log (( 2.675 ) .toFixed ( 2 ) , ( 2.675 ) .toPrecision ( 20 ));
1.00 1.0049999999999998934
1.01 1.0149999999999999023
1.02 1.0249999999999999112
2.67 2.6749999999999998224
ただしこれは小さい数でしか通用しません。 数が大きくなると、誤差そのものが大きくなります。
example.js ▶ 実行↺ 戻す
function round2 (n) {
return Math .round (n * 100 + Number .EPSILON * n * 100 ) / 100 ;
}
console.log (round2 (1.005 ));
console.log (round2 (1.015 ));
console.log (round2 (1.025 ));出力 書き換えて実行できます
1.01
1.02
1.03
example.js ▶ 実行↺ 戻す
console.log (0.1 );
console.log (0.1 === 0.1000000000000000055511151231257827 );出力 書き換えて実行できます
0.1
true
大きさに合わせて許容量を変える必要があります。
example.js ▶ 実行↺ 戻す
console.log (0.1 + 0.1 );
console.log (0.1 + 0.1 + 0.1 );
console.log ((0.1 + 0.1 + 0.1 ).toPrecision (20 ));出力 書き換えて実行できます
0.2
0.30000000000000004
0.30000000000000004441
整数にしてから比べる
そもそも小数にしない方法もあります。これがいちばん確実です。
お金は小数で持たない
金額を 19.99 のような小数で持つと、足すたびに誤差が出る……とは限りません。
ここが厄介なところです。
19.99 を10 回足すとぴったり 合い、0.7 を10 回足すとずれます 。
どちらになるかは値によって変わり、見ただけでは分かりません。
「試したら合っていた」は何の保証にもなりません。手元で通ったものが、別の値でずれます。
だから最小単位の整数 で持ってください。円なら円、ドルならセントです。
toFixed は思ったとおりに丸まらない
「見せるときだけ丸めればいい」も、そのままでは通りません。
4つとも切り下がっています。 どれも中身が見た目よりわずかに小さい からです。
1.005 の実体は 1.00499999999999989… なので、
四捨五入して 1.00 になるのが正しい 動きです。
toFixed() が間違っているのではありません。渡している数が、もう 1.005 ではない のです。
「見た目の値」で四捨五入したいなら、誤差を足し戻してから丸めます。
これも万能ではありません。金額を扱うなら、最初から整数で持つのが確実です。
よくある誤解
JavaScript だけの問題だと思っている
IEEE 754 の倍精度 を使う言語すべてで起きます。
0.1 を正確に持てる言語は、10進数の型 を別に用意しています
(Java の BigDecimal、Python の decimal など)。
表示された値が中身だと思っている
console.log(0.1) が 0.1 と出るのは、
その値に戻る最短の10進表記 を選んで表示しているからです。
同じ値です。 見た目が違うだけです。
誤差は「たまに」出ると思っている
0.1 を足すたびに毎回ずれています。表示のときに隠れているだけです。
2 回足したときは表示上ちょうど 0.2 ですが、
3 回足すと隠しきれなくなって出てきます。
同じ IEEE 754 が決めている NaN の振る舞いは
なぜ NaN === NaN は false なのか にあります。
「等しい」をどう判定するかという話は、== と === の違いにも繋がります。
→ なぜ == ではなく === を使うのか