JS浮點計算精度問題分析與解決
問題描述
在JS計算四則運算時會遇到精度丟失的問題,會引起諸多問題,看看以下例子:
例如:在chrome控制臺輸入 0.1 + 0.7
輸出結果是 0.7999999999999999
例如:0.1+0.2
輸出結果:0.30000000000000004
例如:0.1277*1000000000000
輸出結果:127700000000.00002
問題代碼
bMsg.expectRate的值為0.1277
bMsg.expectRate * 1000000000000 / 10000000000
如問題描述的輸出結果可以看到,結果並不是預期的12.77,而是127700000000.00002
問題原因分析
問題原因是Javascript采用了IEEE-745浮點數表示法(幾乎所有的編程語言都采用),這是一種二進制表示法,可以精確地表示分數,比如1/2,1/8,1/1024。遺憾的是,我們常用的分數(特別是在金融的計算方面)都是十進制分數1/10,1/100等。二進制浮點數表示法並不能精確的表示類似0.1這樣 的簡單的數字。
二進制浮點數表示
0.1 + 0.2計算過程會轉化成二進制,但由於換算為二進制表達時是無窮的。例如:
0.1 -> 0.0001100110011001...(無限)
0.2 -> 0.0011001100110011...(無限)
IEEE 754 標準的 64 位雙精度浮點數的小數部分最多支持 53 位二進制位,所以兩者相加之後得到二進制為
0.0100110011001100110011001100110011001100110011001100
因浮點數小數位的限制而截斷的二進制數字,再轉換為十進制,就成了 0.30000000000000004。所以在進行算術計算時會產生誤差。
解決方案
為了解決浮點數運算精度丟失問題,一般有以下的方案:
使用類庫
- math.js
- decimal.js
toFixed方法
在IE6、7、8版本會存在兼容性問題,返回值不準確。
小數部分轉化為整數,計算後再轉化成小數(推薦使用)
以下是一些封裝好的方法
加法函數
/** ** 加法函數,用來得到精確的加法結果 ** 說明:javascript的加法結果會有誤差,在兩個浮點數相加的時候會比較明顯。這個函數返回較為精確的加法結果。 ** 調用:accAdd(arg1,arg2) ** 返回值:arg1加上arg2的精確結果 **/ function accAdd(arg1, arg2) { var r1, r2, m, c; try { r1 = arg1.toString().split(".")[1].length; } catch (e) { r1 = 0; } try { r2 = arg2.toString().split(".")[1].length; } catch (e) { r2 = 0; } c = Math.abs(r1 - r2); m = Math.pow(10, Math.max(r1, r2)); if (c > 0) { var cm = Math.pow(10, c); if (r1 > r2) { arg1 = Number(arg1.toString().replace(".", "")); arg2 = Number(arg2.toString().replace(".", "")) * cm; } else { arg1 = Number(arg1.toString().replace(".", "")) * cm; arg2 = Number(arg2.toString().replace(".", "")); } } else { arg1 = Number(arg1.toString().replace(".", "")); arg2 = Number(arg2.toString().replace(".", "")); } return (arg1 + arg2) / m; } //給Number類型增加一個add方法,調用起來更加方便。 Number.prototype.add = function (arg) { return accAdd(arg, this); };
減法函數
/**
** 減法函數,用來得到精確的減法結果
** 說明:javascript的減法結果會有誤差,在兩個浮點數相減的時候會比較明顯。這個函數返回較為精確的減法結果。
** 調用:accSub(arg1,arg2)
** 返回值:arg1加上arg2的精確結果
**/
function accSub(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)); //last modify by deeka //動態控制精度長度
n = (r1 >= r2) ? r1 : r2;
return ((arg1 * m - arg2 * m) / m).toFixed(n);
}
// 給Number類型增加一個mul方法,調用起來更加方便。
Number.prototype.sub = function (arg) {
return accMul(arg, this);
};
乘法函數
/**
** 乘法函數,用來得到精確的乘法結果
** 說明:javascript的乘法結果會有誤差,在兩個浮點數相乘的時候會比較明顯。這個函數返回較為精確的乘法結果。
** 調用:accMul(arg1,arg2)
** 返回值:arg1乘以 arg2的精確結果
**/
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);
}
// 給Number類型增加一個mul方法,調用起來更加方便。
Number.prototype.mul = function (arg) {
return accMul(arg, this);
};
除法函數
/**
** 除法函數,用來得到精確的除法結果
** 說明:javascript的除法結果會有誤差,在兩個浮點數相除的時候會比較明顯。這個函數返回較為精確的除法結果。
** 調用:accDiv(arg1,arg2)
** 返回值:arg1除以arg2的精確結果
**/
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);
}
}
//給Number類型增加一個div方法,調用起來更加方便。
Number.prototype.div = function (arg) {
return accDiv(this, arg);
};
更多參考
- 浮點數的二進制表示
- 浮點數前世今生
- IEEE754
- 浮點數運算問題
- 格式浮點數
JS浮點計算精度問題分析與解決