記一次 JavaScript 浮點型數字偏差引起的問題

需求

車間的工人在生產出來產品後,須要完成初步的自檢,並經過手機上報。在實際生產中,用戶(工人)不方便進行數值的輸入,於是表單中的一些項設計成 picker 模式以供選取數值。數值的取值範圍,根據容許的偏差範圍生成。示例以下:javascript

示例一
// 偏差
0.01mm ~ 0.06mm
// picker 展現的數值
0.01, 0.02, 0.03, 0.04, 0.05, 0.06

示例二
// 偏差
15mm ~ 18mm
// picker 展現的數值
15, 16, 17, 18

示例三
// 偏差
1.05mm ~ 1.1mm
// picker 展現的數值
1.05, 1.06, 1.07, 1.08, 1.09, 1.1

由以上例子能夠得知,取值範圍的計算是根據偏差範圍的最小值的最小位數做爲基數,從最小值(包含)逐步累加至最大值(包含)。java

實現

首先,根據最小值獲取小數位的個數。數組

function getDecimalPlace(value) {
    // 先將 Number 轉換爲 String
    value = value + '';
    // 查找小數點的位置,加 1 是爲了方便計算小數點的位數。
    var floatIndex = value.indexOf('.') + 1;
    // 返回的結果是小數位的個數
    return floatIndex ? value.length - floatIndex : 0;
}

用幾個實際的數值,測試一下這個方法。測試

getDecimalPlace(1); //0
getDecimalPlace('1.0'); //0
getDecimalPlace('1.5'); //1
getDecimalPlace('1.23'); //2

而後,根據小數位的個數計算累加的基數。優化

var min = 0.01;
var max = 0.06;

var decimal = getDecimalPlace(min);
// 基數
var radixValue = Math.pow(10, -decimal);

最後,根據偏差範圍和基數,循環生成取值範圍。prototype

var value = min;
var range = [];

for (; value <= max; value += radixValue) {
    range.push(value);
}
console.log(range);
//結果:[0.01,0.02,0.03,0.04,0.05]

從結果來看,好像哪裏不對。沒錯,最大值 0.06 沒有出如今取值範圍中。設計

問題

JavaScript 採用了 IEEE-754 浮點數表示法。這是一種二進制表示法,二進制浮點數表示法並不能精確表示相似 0.1 這樣簡單的數字。

經過一個簡單的例子來驗證上面這段話。code

var num1 = 0.2 - 0.1;
var num2 = 0.3 - 0.2;
console.log(num1 === num2); //false
console.log(num1 === 0.1); //true
console.log(num2 === 0.1); //false

由此可知,前面計算取值範圍的方法中,遇到了相似的問題。ip

var max = 0.06;
var value = 0.05;
console.log(value + 0.01 === max); //false

由於從 0.05 + 0.01 的結果並不等於 0.06,因此循環只執行了 5 次(而非預期的 6 次)就結束了。ci

在嘗試修復此問題以前,先把前面的代碼封裝一下。

function getRange(min, max) {
    var decimal = getDecimalPlace(min);
    var radixValue = Math.pow(10, -decimal);

    var value = min;
    var range = [];

    for (; value <= max; value += radixValue) {
        range.push(value);
    }
    
    return range;
}

解決問題

最簡單粗暴的辦法,就是調整循環條件,在循環結束後再將最大值添加至數組。

function getRange(min, max) {
    var decimal = getDecimalPlace(min);
    var radixValue = Math.pow(10, -decimal);

    var value = min;
    var range = [];

    for (; value < max; value += radixValue) {
        range.push(value);
    }
    range.push(max);
    
    return range;
}

再次使用以前的數據測試:

getRange(0.01, 0.06);
//結果:[0.01,0.02,0.03,0.04,0.05,0.06]

運行結果與預期一致,問題解決。

新的問題

然而,後續的測試中又出現了意外。

getRange(1.55, 1.65);
// 結果:[1.55,1.56,1.57,1.58,1.59,1.6,1.61,1.62,1.6300000000000001,1.6400000000000001,1.65]

1.6300000000000001 這樣的數值,顯然不是咱們指望獲得的。出現此現象,與以前的問題緣由一致。

方案一

將參與計算的數值,先轉換爲整型,再進行計算。

function getRange(min, max) {
    var decimal = getDecimalPlace(min);
    var radixValue = Math.pow(10, -decimal);

    var multi = Math.pow(10, decimal)

    var value = min * multi;
    var range = [];

    for (; value < max * multi; value += radixValue * multi) {
        range.push(value / multi);
    }
    range.push(max);

    return range;
}

注意事項:

  • 向數組中添加數值時,須要再除以倍數,獲得最終的數值。

方案二

使用 toFixed() 方法,對浮點型進行格式化。

function getRange(min, max) {
    var decimal = getDecimalPlace(min);
    var radixValue = Math.pow(10, -decimal);

    var value = min;
    var range = [];

    for (; value < max || +value.toFixed(decimal) === max; value += radixValue) {
        range.push(+value.toFixed(decimal));
    }

    return range;
}

注意事項:

  • toFixed() 方法返回的值是 String 類型,所以須要再轉換爲 Number 類型。
  • 作了一點優化,調整循環條件後,移除了循環外 push() 最大值的語句。

最後

JavaScript 中浮點型精度的偏差,是很是基礎可是卻又常常不被重視的問題。文中分享的方案,足以覆蓋項目中的全部狀況,但若是用在其它地方或項目中,在一些極端狀況下可能會有問題。

參考文檔

相關文章
相關標籤/搜索