JS的浮點數計算精度丟失問題解決方案

前言:

近期在做項目的時候,遇到了一些JS浮點數精度的問題。這個問題,其實說大不大,說小不小。但是這次因爲涉及到一些財務和結算的問題,然後突然發現這個小問題處理起來還是挺麻煩的。這裏把相關的原因的問題的解決方案整理一下,也希望給各位提供一些參考。


案例分析:

近期的項目,由於在H5頁面上需要進行動態的金額計算,且金額涉及到了小數,因而隨之產生了JS浮點數計算的精度丟失問題。
剛開始的時候,測試給提了一個金額計算誤差的問題,剛開始我還沒怎麼重視,然後瞅了瞅代碼,隨便改了改做了些異常處理,然後就給提交了。
接着,測試又提了一個bug,“6.8-0.9=5.8”。然後頓時我就蒙逼了,隨後突然意識到,JS作爲解釋性語言,直接計算會有浮點數精度丟失問題。接下來,在網上找了一些資料,然後也根據具體的原理自己做了一些修改,最終解決了問題。下面就把整個問題解決的思路整理一下。


浮點數的二進制表示:

IEEE 754 標準是IEEE二進位浮點數算術標準(IEEE Standard for Floating-Point Arithmetic)的標準編號,等同於國際標準ISO/IEC/IEEE 60559。該標準由美國電氣電子工程師學會(IEEE)計算機學會旗下的微處理器標準委員會(Microprocessor Standards Committee, MSC)發佈。這個標準定義了表示浮點數的格式(包括負零-0)與反常值(denormal number),一些特殊數值(無窮(Inf)與非數值(NaN)),以及這些數值的「浮點數運算子」;它也指明瞭四種數值修約規則和五種例外狀況(包括例外發生的時機與處理方式)。

JS的浮點數實現也是遵循IEEE 754標準,採用雙精度存儲(double precision),進行了相關的實現。其中1位用來表示符號位,11位用來表示指數,52位表示尾數。如下圖:

浮點數的實現定義

由於無論是採用了哪種表達方式進行怎樣的計算,到了計算機的最底層,都是通過1和0的機器碼來對具體的數據和操作進行具體的實現。由於底層實現機制的原因,浮點數在轉換爲二進制表示的時候,無法精確表示這種包含小數點的數據,其本質是將浮點數轉換成了用二進制表示的最接近的近似值。下面一個例子可以用來簡單說明浮點數在轉換爲二進制時候的計算方法。如下圖:

浮點數轉換二進制
0.02625=0.000001101(二進制),無法精確求出二進制表示,因此採用“四捨五入法”(逢1進,逢0舍)。

以上就是問題產生的原理,很多編譯型語言如Java,c#等都對浮點數的處理進行了封裝。因此平時大多數情況下,並不會出現明顯的可見的問題。js本身作爲解釋性語言,好像這點上有着天生的劣勢。隨後本人在網上找了很多解決方式,也大多數是通過自定義的方法去解決這個問題,原理基本都大同小異。


解決方案:

本質上在處理這類問題的時候,基本的思路就是通過將浮點數轉換成整數進行計算,然後再將整數的小數點位調整,轉回正確的浮點數結果。

原生計算

console.log(6.8-0.9);
console.log(6.8-0.8);
console.log(6.8-0.4);
console.log(6.8-0.3);
//結果
5.8999999999999995
6
6.3999999999999995
6.5

第一步,定義一個自定義的轉換和處理函數:

Math.formatFloat = function (f, digit) {
    var m = Math.pow(10, digit);
    return parseInt(f * m, 10) / m;
}

此時調用這個自定義的函數,來處理上面的原生計算:

console.log(Math.formatFloat(6.8-0.9,1));
console.log(Math.formatFloat(6.8-0.8,1));
console.log(Math.formatFloat(6.8-0.4,1));
console.log(Math.formatFloat(6.8-0.3,1));
//此時結果
5.8
6
6.3
6.5

仔細看輸出的結果,會發現,6.8-0.9應該輸出結果5.9,6.8-0.4應該輸出6.4,這裏轉換結果還是不正確。可以將自定義方法內部的結果進行打印,進行對比。

console.log(6.8-0.9);
console.log((6.8-0.9)*10);//
console.log(parseInt((6.8-0.9)*10,10));
//在轉換結果的時候出現了問題,轉換爲整數時,小數點後直接被截斷了
console.log(((6.8-0.9)*10)/10);
//結果
5.8999999999999995
58.99999999999999
58
5.8999999999999995

這裏自定義的函數,做一下具體的處理。在浮點數計算的時候,很多時候產生的都是這種極限數據,如果要精確進行整數轉換,要放大的倍數過大。這裏我們可以用四捨五入對轉換的過程進行優化:

Math.ceil((6.8-0.9)*10);//向上取整
59
Math.floor((6.8-0.9)*10);//想下取整
58
Math.round((6.8-0.9)*10);//四捨五入
59

優化之後的自定義函數:

Math.formatFloat = function (f, digit) {
    var m = Math.pow(10, digit);
    return Math.round(f * m, 10) / m;
}

此時重新調用函數對計算結果進行打印:

console.log(Math.formatFloat(6.8-0.9,2));
console.log(Math.formatFloat(6.8-0.8,2));
console.log(Math.formatFloat(6.8-0.4,2));
console.log(Math.formatFloat(6.8-0.3,2));
//打印結果
5.9
6
6.4
6.5

寫在最後:

近些日子,發現js的火爆程度實在有點出乎意料。而且隨着node等技術的出現,讓js這樣的原來以慢著稱的語言有了和服務端開發語言的能力。之前微軟,蘋果,oracle等公司都嘗試用自己的語言和生態體系,統治整個開發者的世界。然而,我覺得說不定js能完成,這些巨頭沒有完成的工作。也許開發語言的大一統,會在不遠的未來實現。


希望我的文字能給你帶來幫助:

碼字不易,與君共勉!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章