2017年1月9日 星期一

JavaScript 小數相加

因為浮點數在電腦中不是儲存準確的值,所以會有誤差。
原因參考:
The Floating-Point Guide - Basic Answers
[浮點數] IEEE754 , C/C++ 浮點數誤差 @ Edison.X. Blog :: 痞客邦 PIXNET ::


尤其在經過運算後,可能目視便可看到誤差,也可能多出許多非預期的小數位數。
網路上有幾種處理方式,以下用兩個浮點數相加為例。
測試瀏覽器:Chrome 55.0.2883.87 m

例如:
0.1+0.7 (預計結果為0.8)
=>0.7999999999999999


處理方式 1:
轉成整數運算後,再轉回小數。
(參考:http://www.w3schools.com/js/js_numbers.asphttp://www.cnblogs.com/konooo/archive/2010/01/23/1654617.html)
「先轉成整數運算後」-->「再轉回小數」
(0.1*10+0.7*10)/10
=>0.8

但發現一個特例
155.2+0.67 (預計結果為155.87)
=>155.86999999999998
(155.2*100+0.67*100)/100
=>155.86999999999998
=>「先轉成整數運算後」-->「再轉回小數」,這方法無效


處理方式 2:
轉成整數運算後,緊接著使用 round,再轉回小數。
(參考:http://stackoverflow.com/a/10474055)
「先轉成整數運算後」-->「round處理確保為整數」-->「再轉回小數」
Math.round((155.2*100+0.67*100))/100
=>155.87
參考連結範例,是直接乘上 1e12 倍,看似也正常
Math.round((155.2*1e12+0.67*1e12))/1e12
=>155.87


但 Math.round 直接乘上 1e12 倍再還原,也出現例外
(參考:http://stackoverflow.com/a/13388202)
1234563995.721+12345691212.718+1234568421.5891+12345677093.49284 (預計結果為27160500723.52094)
=>27160500723.520943 (IE11顯示 27160500723.520942)

Math.round(1234563995.721*100000+12345691212.718*100000+1234568421.5891*100000+12345677093.49284*100000)/100000
=>27160500723.52094 (IE11顯示 27160500723.52094)

Math.round(1234563995.721*1000000+12345691212.718*1000000+1234568421.5891*1000000+12345677093.49284*1000000)/1000000
=>27160500723.52094 (IE11顯示 27160500723.52094)

Math.round(1234563995.721*1e12+12345691212.718*1e12+1234568421.5891*1e12+12345677093.49284*1e12)/1e12
=>27160500723.520943  (IE11顯示 27160500723.520942) 
=> 小數原本只有5位,但chrome和IE11都變成6位
=> 直接乘上 1e12 倍再還原,無法完美得出 27160500723.52094


處理方式 3:
前面 http://stackoverflow.com/a/13388202 參考連結,建議用 toFixed
(1234563995.721+12345691212.718+1234568421.5891+12345677093.49284).toFixed(5)
=>"27160500723.52094" (但需注意 toFixed 回傳的結果是字串)

但 toFixed 在某些地方,也會出現不如預期的現象 (參考:http://stackoverflow.com/a/661757)
(1.695).toFixed(2)
=>"1.70"

(0.695).toFixed(2)  (預期結果"0.70")
=>"0.69" (但IE11正常顯示 "0.70")
=> Chrome無法正常四捨五入,但IE11可以正常四捨五入
其他特例
(1.2).toFixed(16)
=>"1.2000000000000000"
(1.2).toFixed(17)
=>"1.19999999999999996"  (但IE11正常顯示 "1.20000000000000000")



結論:如果著重於相加後,小數位數不會變多

解決方式一: 
「先乘上剛剛好能剛好將該小數最大位數變成整數的10倍數」-->「進行相加」-->「round處理確保為整數」-->「再轉回小數」
var floatAdd = function (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 Math.round((arg1*m+arg2*m))/m;
};


解決方式二: 
「小數直接相加」-->「toFixed小數最大位數」
var floatAdd = function (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.max(r1, r2);
    return parseFloat((arg1 + arg2).toFixed(m));
};


解決方式三:
使用The Floating-Point Guide提到的方式 「The Floating-Point Guide - Floating-point cheat sheet for JavaScript


沒有留言:

張貼留言