解決JS浮點數運算結果不精確的Bug

       最近在作項目的時候,涉及到產品價格的計算,常常會出現JS浮點數精度問題,這個問題,對於財務管理系統的開發者來講,是個很是嚴重的問題(涉及到錢相關的問題都是嚴重的問題),這裏把相關的緣由和問題的解決方案整理一下,也但願給各位提供一些參考。javascript

一. 常見例子

// 加法
   0.1 + 0.2 = 0.30000000000000004
   0.1 + 0.7 = 0.7999999999999999
   0.2 + 0.4 = 0.6000000000000001

   // 減法
   0.3 - 0.2 = 0.09999999999999998
   1.5 - 1.2 = 0.30000000000000004

   // 乘法
   0.8 * 3 = 2.4000000000000004
   19.9 * 100 = 1989.9999999999998

   // 除法
   0.3 / 0.1 = 2.9999999999999996
   0.69 / 10 = 0.06899999999999999

   // 比較
   0.1 + 0.2 === 0.3 // false
   (0.3 - 0.2) === (0.2 - 0.1) // false複製代碼

二. 致使緣由

JavaScript 內部只有一種數字類型Number,也就是說,JavaScript 語言的底層根本沒有整數,全部數字都是以IEEE-754標準格式64位浮點數形式儲存,11.0是相同的。由於有些小數以二進制表示位數是無窮的。JavaScript會把超出53位以後的二進制捨棄,因此涉及小數的比較和運算要特別當心。java

三. IEEE二進制浮點數算術標準(IEEE 754)

IEEE二進制浮點數算術標準IEEE 754)是20世紀80年代以來最普遍使用的浮點數運算標準,爲許多CPU與浮點運算器所採用。這個標準定義了表示浮點數的格式(包括負零-0)與反常值(denormal number)),一些特殊數值(無窮(Inf)與非數值(NaN)),以及這些數值的「浮點數運算符」;它也指明瞭四種數值舍入規則和五種例外情況(包括例外發生的時機與處理方式)。git

四. 浮點數的存儲

JS的浮點數實現也是遵循IEEE 754標準,採用雙精度存儲(double precision),使用64位固定長度來表示,其中1位用來表示符號位,11位用來表示指數,52位表示尾數。以下圖:github

  • 符號位(sign):第1位是正負數符號位,0表明正數,1表明負數
  • 指數位(Exponent):中間11位存儲指數,用來表示次方數
  • 尾數位(mantissa):最後的52位是尾數,超出部分自動進一舍零

五. 浮點數的計算步驟(0.1+0.2)

【1】首先,十進制的0.1和0.2會轉換成二進制的,可是因爲浮點數用二進制表示是無窮的函數

0.1——>0.0001 1001 1001 1001 ...(1001循環)
 0.2——>0.0011 0011 0011 0011 ...(0011循環)複製代碼

【2】IEEE754標準的64位雙精度浮點數的小數部分最多支持53位二進制,多餘的二進制數字被截斷,因此二者相加以後的二進制之和是ui

0.0100110011001100110011001100110011001100110011001101複製代碼

【3】將截斷以後的二進制數字再轉換爲十進制,就成了0.30000000000000004,因此在計算時產生了偏差this

六. 解決辦法

【1】引用類庫spa

  1. Math.js
  2. decimal.js
  3. big.js

【2】思路一:在知道小數位個數的前提下,能夠考慮經過將浮點數放大倍數到整型(最後再除以相應倍數),再進行運算操做,這樣就能獲得正確的結果了prototype

0.1 + 0.2 ——> (0.1 * 10 + 0.2 * 10) / 10 // 0.3
   0.8 * 3 ——> ( 0.8 * 100 * 3) / 100         //2.4複製代碼

【3】自定義一個轉換和處理函數code

// f表明須要計算的表達式,digit表明小數位數
    Math.formatFloat = function (f, digit) {
      // Math.pow(指數,冪指數)
      var m = Math.pow(10, digit);
      // Math.round() 四捨五入
      return Math.round(f * m, 10) / m;
    }
    console.log(Math.formatFloat(0.3 * 8, 1));  // 2.4
    console.log(Math.formatFloat(0.35 * 8, 2));  // 2.8複製代碼

【4】加法函數

/** ** 加法函數,用來獲得精確的加法結果 ** 說明: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);
    };複製代碼

【5】減法函數

/** ** 減法函數,用來獲得精確的減法結果 ** 說明: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);
    };複製代碼

【6】乘法函數

/** ** 乘法函數,用來獲得精確的乘法結果 ** 說明: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);
    };複製代碼

【7】除法函數

/** ** 除法函數,用來獲得精確的除法結果 ** 說明: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);
    };複製代碼
相關文章
相關標籤/搜索