Note a problem caused by JavaScript floating-point digital error

Keywords: Javascript Mobile

demand

After the workers in the workshop produce the products, they need to complete the preliminary self-test and report them through mobile phones. In actual production, users (workers) are not convenient to input numerical values, so some items in the form are designed as picker mode for selecting numerical values. The range of values is generated according to the allowable error range. Examples are as follows:

Example 1
 / / error
0.01mm ~ 0.06mm
 // picker Displays Numbers
0.01, 0.02, 0.03, 0.04, 0.05, 0.06

Example two
 / / error
15mm ~ 18mm
 // picker Displays Numbers
15, 16, 17, 18

Example three
 / / error
1.05mm ~ 1.1mm
 // picker Displays Numbers
1.05, 1.06, 1.07, 1.08, 1.09, 1.1

As can be seen from the above examples, the calculation of the range of values is based on the minimum digit of the error range as the cardinal number, which gradually accumulates from the minimum value (including) to the maximum value (including).

Realization

Firstly, the number of decimal digits is obtained according to the minimum value.

function getDecimalPlace(value) {
    // First convert Number to String
    value = value + '';
    // Find the location of the decimal point, add 1 to facilitate the calculation of the number of decimal points.
    var floatIndex = value.indexOf('.') + 1;
    // The result returned is the number of decimal places
    return floatIndex ? value.length - floatIndex : 0;
}

Test this method with several actual values.

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

Then, the cumulative cardinality is calculated according to the number of decimal digits.

var min = 0.01;
var max = 0.06;

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

Finally, according to the error range and cardinal number, the range of values is generated cyclically.

var value = min;
var range = [];

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

As a result, there seems to be something wrong. Yes, the maximum value of 0.06 does not appear in the range of values.

problem

JavaScript uses the floating-point representation of IEEE-754. This is a binary representation. The binary floating-point representation does not accurately represent simple numbers like 0.1.

A simple example is given to illustrate the above statement.

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

It can be seen that similar problems have been encountered in the previous method of calculating the range of values.

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

Because the result from 0.05 + 0.01 is not equal to 0.06, the loop ends after only five executions (instead of the expected six).

Before trying to fix this problem, encapsulate the previous code.

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;
}

Solve the problem

The simplest and crudest way is to adjust the loop conditions and add the maximum value to the array after the loop is over.

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;
}

Reuse previous data tests:

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

The operation results are consistent with expectations and the problems are solved.

New problems

However, there were accidents in the subsequent tests.

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

The value of 1.6300000000001 is obviously not what we expected. This phenomenon is consistent with the causes of previous problems.

Scheme 1

The numerical values involved in the calculation are first converted into integers and then calculated.

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;
}

Matters needing attention:

  • When adding values to an array, you need to divide by multiple to get the final value.

Option two

The toFixed() method is used to format the floating-point type.

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;
}

Matters needing attention:

  • The toFixed() method returns a value of String type, so it needs to be converted to Number type again.
  • After adjusting the loop conditions, the statement of maximum push() outside the loop is removed.

End

The error of floating-point precision in JavaScript is a very basic but often ignored problem. The scenarios shared in this article are sufficient to cover all situations in the project, but may be problematic in some extreme cases if used elsewhere or in the project.

Reference material

Posted by dannydefreak on Mon, 07 Oct 2019 19:33:06 -0700