浮點數的加減
1.335.toFixed(2) // '1.33' 這真是一個尷尬的錯誤
情境描述
用 Javascript 來做加減乘除,如果是有小數點的數字的話,跑出來的數字會非常奇特,例如:
const number1 = 5.1;
const number2 = 100.0;
console.log(number1 * number2);
// 509.99999999999994
原因
發現主要問題在於 Javascript 是用二進制來計算數值,之後再轉換成十進制,舉個例子,0.1 + 0.2,在 Javascript 會是這樣子計算的。
// 0.1 和 0.2 都轉化成二進位制後再進行運算
0.00011001100110011001100110011001100110011001100110011010
+
0.0011001100110011001100110011001100110011001100110011010
=
0.0100110011001100110011001100110011001100110011001100111
// 轉成十進位制正好是 0.30000000000000004
精度遺失 (Loss of Precision)
由於位元數有限,浮點數只能提供一定範圍的有效數字(Significant Digits)。
- 單精度 (float): 大約 7 位有效數字。
- 雙精度 (double): 大約 15-16 位有效數字。
超過這個範圍的數字會被丟棄或變得不準確。
大數吃小數 (Absorption / Loss of Significance)
當一個「非常大的數」與一個「非常小的數」進行加減運算時,小的數可能會完全消失。
- 原理:浮點數運算時需要對齊指數(Exponent)。為了讓小數的指數與大數對齊,小數的尾數(Mantissa)會不斷右移,直到被移出有效位數範圍,變成 0。
- 例子: $$1,000,000,000 + 0.000000001 = 1,000,000,000$$ (那個 $0.000000001$ 在運算過程中被忽略了)
嚴重相消 (Catastrophic Cancellation)
當兩個「非常接近」的大數相減時,有效位數會大幅減少,剩下的結果可能只是「雜訊」。
- 例子:$A = 1.23456789$,$B = 1.23456788$ $A - B$ 的結果理應是 $0.00000001$。但在電腦內部,因為之前的捨入誤差,相減後的剩餘部分可能包含巨大的相對誤差。
結合律失效 (Non-Associativity)
在數學上,$(A + B) + C$ 等於 $A + (B + C)$。但在浮點數運算中,這不一定成立。
- 原因:因為「大數吃小數」的問題,運算的順序會影響結果。
- 建議:若要加總一系列數字,最好先將數字從小到大排序,先加總小的數字,最後再加大的數字(Kahan Summation Algorithm)。
note
永遠不要使用 == 來判斷兩個浮點數是否相等。 錯誤寫法:if (a == b) ... 原因:因為微小的捨入誤差,兩個理應相等的數字可能在二進位中差了最後一個 bit。
解決方法
-
外部套件
引用 Math.js 或者 BigDecimal.js 等,均可以順利解決浮點數運算的問題。
-
自訂四則運算程式碼
//除法
function accDiv(arg1, arg2) {
var t1 = 0,
t2 = 0,
r1,
r2;
try {
t1 = arg1.toString().split(".")[1].length;
} catch (e) {}
try {
t2 = arg2.toString().split(".")[1].length;
} catch (e) {}
with (Math) {
r1 = Number(arg1.toString().replace(".", ""));
r2 = Number(arg2.toString().replace(".", ""));
return (r1 / r2) * pow(10, t2 - t1);
}
}
//乘法
function accMul(arg1, arg2) {
var m = 0,
s1 = arg1.toString(),
s2 = arg2.toString();
try {
m += s1.split(".")[1].length;
} catch (e) {}
try {
m += s2.split(".")[1].length;
} catch (e) {}
return (Number(s1.replace(".", "")) * Number(s2.replace(".", ""))) / Math.pow(10, m);
}
//加法
function accAdd(arg1, arg2) {
var r1, r2, m;
try {
r1 = arg1.toString().split(".")[1].length;
} catch (e) {
r1 = 0;
}
try {
r2 = arg2.toString().split(".")[1].length;
} catch (e) {
r2 = 0;
}
m = Math.pow(10, Math.max(r1, r2));
return (arg1 * m + arg2 * m) / m;
}
//減法
function accSubtr(arg1, arg2) {
var r1, r2, m, n;
try {
r1 = arg1.toString().split(".")[1].length;
} catch (e) {
r1 = 0;
}
try {
r2 = arg2.toString().split(".")[1].length;
} catch (e) {
r2 = 0;
}
m = Math.pow(10, Math.max(r1, r2));
n = r1 >= r2 ? r1 : r2;
return ((arg1 * m - arg2 * m) / m).toFixed(n);
}
一、前言
Javascript 中,常見的與浮點數相關的操作(運算)有:+