小数点の計算がずれてしまう仕組み
小数点の計算でずれるものがある
0.1 + 0.2 // 0.30000000000000004
原因を追いかけてみましょう。
数値の計算の仕組みについて
10進数の計算を次の例を考えてみます。
7 + 10 = 17
一般的にプログラミング言語が数値計算を行う場合2進数に変換します。
2進数とは
2進数は2になると次の位に上がります。
1 = 1
2 = 10
3 = 11
4 = 100
5 = 101
6 = 110
7 = 111
8 = 1000
9 = 1001
10 = 1010
...
14 = 1110
15 = 1111
16 = 10000
10進数から変換するときは 2のn乗で分割して変換します。
2のn乗の例:
2 ** 0 = 1
2 ** 1 = 2
2 ** 2 = 4
2 ** 3 = 8
7,10を2進数に分解すると
7 = 4 + 2 + 1 = (2**2) + (2**1) + (2**0) // 2の0乗は1です。
10 = 8 + 2 = (2**3) + (2**1)
n乗の部分が2進数の桁数になります。ただし右端桁がn=0になります。
- 7は 2進数で
111
- 10は 2進数で
1010
JavaScriptではNumber.toString(2) で2進数に変換することができます。
(7).toString(2) // 111
(10).toString(2) // 1010
足し算をしてみます。
111
+1010
------
10001
1001 = (2**4) + (2**0) = 16 + 1 = 17
10 + 7 = 17
がきちんと計算できました。
少数の計算
乗数の計算は乗数をかけるとn乗が+1になります。乗数で割るとn乗が-1になります。
2の0乗は1でした。2の-1乗は1/2です。
少数を2進数に変換する場合は、変換したい値を分数に変換した後に、分母・分子をそれぞれ
2進数に変換して、少数に変換するとわかりやすいと思います。
0.5
を2進数に変換してみましょう。
0.5 = 1/2
(↓ 分母分子を2進数に変換)
1/10 = 0.1
JavaScriptで確認してみると
(0.5).toString(2) // 0.1
0.625を2進数に表現してみましょう。
0.625 = 625/1000 = 5/8
101/1000 = 0.101
JavaScriptで確認してみると
(0.625).toString(2) // 0.101
これまでの例は分母が10, 100 といった割り切れる数でしたがそうではないケースもあります。
0.1
を 2進数で表現してみると
0.1 = 1/10
(↓ 分母分子を2進数に変換)
1/1010
2進数の割り算をすると、0.0001100110011...
と無限に続きます。
0.0001100110011...
-----------
101 ) 1000
101
----------
110
101
-------
1000
JavaScriptで確認してみましょう。
本来は無限に続くべき2進数は途中で値が切り捨てられています。
(0.1).toString(2) // 0.0001100110011001100110011001100110011001100110011001101
10進数の小数点を2進数に変換した後に、その2進数を使って10進数を復元したいと思います。
2進数 0.101
を10進数に直してみます。
0.101
(↓ 2進数を10進数に変換)
2 ** (-1) + 2 ** (-3) = 0.5 + 0.125 = 0.625
10進数の小数点を与えて2進数に変換した後、10進数の少数に再変換する関数を実装して
動きを確認してみます。
function convertFloatToDigitThenFloat(testFloat) {
const test = testFloat.toString(2).substring(2);
let value = 0.0;
for(let i = 0; i < test.length; i++) {
const digit = parseInt(test[i]);
const plus = digit * (2 ** (-i-1));
console.log("(before)value", value, "plus", plus);
value += plus;
console.log("(after)value", value);
}
return value;
}
0.1を入れた動きを見てみます。途中の出力は省略しています。
convertFloatToDigitThenFloat(0.1)
...(省略)
VM2850:7 (before)value 0 plus 0.0625
VM2850:9 (after)value 0.0625
VM2850:7 (before)value 0.0625 plus 0.03125
VM2850:9 (after)value 0.09375
...(省略)
VM2850:9 (after)value 0.09999999999999998
VM2850:7 (before)value 0.09999999999999998 plus 2.7755575615628914e-17
VM2850:9 (after)value 0.1
最終的には0.1になりますが、これはJavaScriptが15-17桁以上の少数のは丸め誤差を自動計算を行うためです。桁数が大きな少数は勝手に四捨五入されます。
0.09999999999999999 // 0.09999999999999999 を返す
0.099999999999999999 // 0.1 丸め誤差が発生して四捨五入した0.1を返す
0.1 + 0.2 = ?
計算結果がずれてしまう原因を見てきましょう。
// 0.1 + 0.2 = 0.3
0.1 + 0.2 // 0.30000000000000004
0.1
, 0.2
は 分母分子を変換するとそれぞれ2進数では表現できない値になることがわかります。
0.1 = 1/10
(↓ 分母分子を2進数に変換)
1/1010
0.2 = 2/10 = 1/5
(↓ 分母分子を2進数に変換)
1/101
JavaScriptで確認してみます。JavaScriptは無限を表現できないので、値が途中で切られています。
(0.1).toString(2); // 0.0001100110011001100110011001100110011001100110011001101
(0.2).toString(2); // 0.001100110011001100110011001100110011001100110011001101
二つの小数点を2進数に変換し、2進数の表現から合計を計算してみます。
function sum(a, b) {
const digitsA = a.toString(2).substring(2);
const digitsB = b.toString(2).substring(2);
let length = Math.max(digitsA.length, digitsB.length);
let value = 0.0;
for(let i = 0; i < length; i++) {
if(digitsA[i]) {
const digit = parseInt(digitsA[i]);
const plus = digit * (2 ** (-i-1));
console.log("(before)value", value, "plus", plus);
value += plus;
console.log("(after)value", value);
}
if(digitsB[i]) {
const digit = parseInt(digitsB[i]);
const plus = digit * (2 ** (-i-1));
console.log("(before)value", value, "plus", plus);
value += plus;
console.log("(after)value", value);
}
}
return value;
}
sum(0.1, 0.2);
(before)value 0 plus 0
...(省略)
VM3143:17 (before)value 0 plus 0.125
VM3143:19 (after)value 0.125
VM3143:10 (before)value 0.125 plus 0.0625
VM3143:12 (after)value 0.1875
VM3143:17 (before)value 0.1875 plus 0.0625
VM3143:19 (after)value 0.25
VM3143:10 (before)value 0.25 plus 0.03125
...(省略)
VM3143:17 (before)value 0.28125 plus 0.0078125
VM3143:19 (after)value 0.2890625
VM3143:10 (before)value 0.2890625 plus 0.00390625
VM3143:12 (after)value 0.29296875
VM3143:17 (before)value 0.29296875 plus 0.00390625
VM3143:19 (after)value 0.296875
VM3143:10 (before)value 0.296875 plus 0.001953125
...(省略)
(after)value 0.29999999999999993
VM3143:10 (before)value 0.29999999999999993 plus 0
VM3143:12 (after)value 0.29999999999999993
VM3143:17 (before)value 0.29999999999999993 plus 5.551115123125783e-17
VM3143:19 (after)value 0.3
VM3143:10 (before)value 0.3 plus 2.7755575615628914e-17
VM3143:12 (after)value 0.30000000000000004
0.1 + 0.2= 0.30000000000000004
の計算結果と一致しました。
- 0.1と0.2は2進数で表現するとJavaScriptの中では近似値(厳密には一緒の値ではない)
- 近似値の計算に加えて丸め誤差の四捨五入が発生するので、計算結果が微妙に合わない現象が発生する
Discussion