preface

In recent projects, changes in monetary units were designed, such as conversions between cents and dollars.

But when 4.35 * 100 is calculated, the result is 434.99999999999994 instead of 435 as expected. Realized that you might encounter the classic JavaScript question — is 0.1 + 0.2 equal to 0.3?

Cause analysis,

0.1The conversion to binary is an infinite loop

We know that data in a computer is stored in binary, and the decimal is converted into binary data, multiplied by two, arranged in order.

Specific approach is: with 2 times the decimal fraction, you can get the product, take out the integral part of the product, and then multiply the remaining decimal part with 2, and get a product, and then take out the integral part of the product, so on, until the decimal part of the product is zero, or achieve the required precision.

For example, for example convert 0.8125 to a binary decimal:

So, based on the above, the steps to convert 0.1 to a binary decimal are as follows:

0.1 * 2 = 0.2
0.2 * 2 = 0.4 // Notice here
0.4 * 2 = 0.8
0.8 * 2 = 1.6
0.6 * 2 = 1.2
0.2 * 2 = 0.4 // Notice here, the loop starts
0.4 * 2 = 0.8
0.8 * 2 = 1.6
0.6 * 2 = 1.2.Copy the code

As you can see, since the last bit does not end in a 5 (only 0.5 * 2 is needed to get an integer), the resulting binary data is an infinite binary decimal 0.00011001100… .

The accuracy of Javascript

In Javascript, both integers and decimals follow the IEEE 754 standard (the standard double double floating-point number) and are represented as 64-bit fixed lengths.

In this rule, data is stored in the computer as follows:

  • Sign: accounting for 1 bit, indicating positive and negative;
  • Exponent: takes up 11 bits, indicating a range;
  • Mantissa (mantissa): account for 52 bits, indicating precision, if the extra end is 1 need to carry;

According to the following formula, we calculate the binary data V:

Here, we above 0.1 as an example, corresponding to binary data 0.00011001100… , expressed by scientific notation as 1.100110011… X 2^(-4), according to the above formula, S is 0(1 bit), E is -4 + 1023, the corresponding binary is 01111111011(11 bit), M for 1001100110011001100110011001100110011001100110011010.

Here we can see that the accuracy of 0.1 is lost in JavaScript.

In the JavaScript in the same way, 0.2 to 0.0011001100110011001100110011001100110011001100110011010, the condition of the missing accuracy.

So 0.1+0.2 yields the binary data as follows, and the result is 0.30000000000000004 when converted to base 10:

// The calculation process
0.00011001100110011001100110011001100110011001100110011010
0.0011001100110011001100110011001100110011001100110011010

/ / added
0.01001100110011001100110011001100110011001100110011001110
Copy the code

The solution

When something goes wrong with the project, it has to be fixed. Through communication with other team members, a third-party library called Number-Precision was found to solve this problem.

import NP from 'number-precision'

NP.strip(0.09999999999999998); / / = 0.1
NP.times(3.0.3);              // 3 * 0.3 = 0.9, not 0.8999999999999999
NP.divide(1.21.1.1);          // 1.21/1.1 = 1.1, not 1.0999999999999999
NP.plus(0.1.0.2);             // 0.1 + 0.2 = 0.3, not 0.30000000000000004
NP.minus(1.0.0.9);            // 1.0-0.9 = 0.1, not 0.09999999999999998
Copy the code

The following simple analysis of the source code.

NP.strip

/* strip(0.099999999999998)=0.1 */
function strip(num: numType, precision = 15) :number {
  return +parseFloat(Number(num).toPrecision(precision));
}
Copy the code

As you can see, the toPrecision method is used here.

ToPrecision is a string representation of a numeric object in fixed-point or exponential notation, rounded to the number of digits displayed as specified by the precision argument.

Take 0.09999999999999998 as an example:

0.09999999999999998.toPrecision(15) // Output string: "0.100000000000000"
Copy the code

The parseFloat method converts the returned string to the corresponding floating point number.

parseFloat(0.09999999999999998.toPrecision(15)) // Output number: 0.1
Copy the code

NP.times

/** ** exact multiplication */
function times(num1: numType, num2: numType, ... others: numType[]) :number {
  if (others.length > 0) {
    return times(times(num1, num2), others[0], ...others.slice(1));
  }
  const num1Changed = float2Fixed(num1);
  const num2Changed = float2Fixed(num2);
  const baseNum = digitLength(num1) + digitLength(num2);
  const leftValue = num1Changed * num2Changed;

  checkBoundary(leftValue);

  return leftValue / Math.pow(10, baseNum);
}
Copy the code

The first half of the method is used to multiply more than two parameters by recursion. Let’s focus on the second half of the logic.

Here are a few leading functions:

/**
 * Return digits length of a number
 * @param {*number} num Input number
 */
function digitLength(num: numType) :number {
  // Get digit length of e
  const eSplit = num.toString().split(/[eE]/);
  const len = (eSplit[0].split('. ') [1] | |' ').length - +(eSplit[1] | |0);
  return len > 0 ? len : 0;
}

/** * Convert decimals to whole numbers, supporting scientific notation. If it is a decimal, it is enlarged to an integer *@param {*number} Num Indicates the number of inputs */
function float2Fixed(num: numType) :number {
  if (num.toString().indexOf('e') = = = -1) {
    return Number(num.toString().replace('. '.' '));
  }
  const dLen = digitLength(num);
  return dLen > 0 ? strip(Number(num) * Math.pow(10, dLen)) : Number(num);
}

/** * Checks if the number is out of bounds and gives a hint if it is@param {*number} Num Indicates the number of inputs */
function checkBoundary(num: number) {
  if (_boundaryCheckingState) {
    if (num > Number.MAX_SAFE_INTEGER || num < Number.MIN_SAFE_INTEGER) {
      console.warn(`${num} is beyond boundary when transfer to integer, the results may not be accurate`); }}}Copy the code

We can see that the logic of multiplication is to zoom in and then zoom out. The decimals are converted to integers, multiplied, and finally reduced by the sum of the decimals of all the multiplied data.

NP.divide

/** * exact division */
function divide(num1: numType, num2: numType, ... others: numType[]) :number {
  if (others.length > 0) {
    return divide(divide(num1, num2), others[0], ...others.slice(1));
  }
  const num1Changed = float2Fixed(num1);
  const num2Changed = float2Fixed(num2);
  checkBoundary(num1Changed);
  checkBoundary(num2Changed);
  // fix: similar to 10 ** -4 to 0.00009999999999999999, strip fixed
  return times(num1Changed / num2Changed, strip(Math.pow(10, digitLength(num2) - digitLength(num1))));
}
Copy the code

Division is an adjustment based on multiplication to convert the data to an integer and divide first. Then according to the decimal difference, zoom & zoom.

NP) plus and NP) minus

/** * exact addition */
function plus(num1: numType, num2: numType, ... others: numType[]) :number {
  if (others.length > 0) {
    return plus(plus(num1, num2), others[0], ...others.slice(1));
  }
  const baseNum = Math.pow(10.Math.max(digitLength(num1), digitLength(num2)));
  return (times(num1, baseNum) + times(num2, baseNum)) / baseNum;
}

/** * exact subtraction */
function minus(num1: numType, num2: numType, ... others: numType[]) :number {
  if (others.length > 0) {
    return minus(minus(num1, num2), others[0], ...others.slice(1));
  }
  const baseNum = Math.pow(10.Math.max(digitLength(num1), digitLength(num2)));
  return (times(num1, baseNum) - times(num2, baseNum)) / baseNum;
}
Copy the code

The logic of addition and subtraction, similar to multiplication, is to enlarge the data according to the largest number of decimals, then add/subtract, and finally reduce the data.

conclusion

  • There is a loss of precision in JavaScript when a decimal is converted to binary and there is an infinite loop
  • throughtoPrecisionMethod for precision loss repair
  • When the data is calculated, the decimal number is converted to a whole number to calculate, and then the result is enlarged or reduced

The resources

  • Explore JavaScript accuracy issues and solutions
  • JavaScript floating point traps and solutions