JS decimal operation precision loss problem

In your work, do you often encounter the calculation of some data indicators, such as percentage conversion, how many decimal places to keep, etc.? Then the calculation will be inaccurate and the data precision will be lost. Through this sharing, the problem of data accuracy loss can be easily solved with the help of third-party libraries.


1. Scene recurrence

Some common problems with JS digital precision loss
// Addition =====================
0.1 + 0.2 === 0.3 // false
0.1 + 0.2 = 0.30000000000000004
0.7 + 0.1 = 0.7999999999999999
0.2 + 0.4 = 0.6000000000000001

// Subtraction =====================
1.5 - 1.2 = 0.30000000000000004
0.3 - 0.2 = 0.09999999999999998
 
// Multiplication =====================
19.9 * 100 = 1989.9999999999998
0.8 * 3 = 2.4000000000000004
35.41 * 100 = 3540.9999999999995

// Division =====================
0.3 / 0.1 = 2.9999999999999996
0.69 / 10 = 0.06899999999999999

Why is 0.1 + 0.2 === 0.3 false?

Let’s look at the following metaphor first

For example, a number 1÷3=0.33333333…

3 will keep looping infinitely, which can be expressed in mathematics, but the computer needs to store it so that it can be taken out and used next time, but 0.333333… This number loops infinitely, and it cannot be stored in any large memory, so it cannot store a relative number. Mathematically speaking, only an approximate value can be stored. When the computer stores and then retrieves the value, there will be a problem of loss of accuracy.

Look again at the problem of incorrect carry when the last decimal digit is 5 in tofixed() in js
1.35.toFixed(1) // 1.4 correct
1.335.toFixed(2) // 1.33 error
1.3335.toFixed(3) // 1.333 error
1.33335.toFixed(4) // 1.3334 correct
1.333335.toFixed(5) // 1.33333 error
1.3333335.toFixed(6) // 1.333333 error

As you can see, when the number of decimal points is 2 and 5, the rounding is correct, and the others are wrong.

The root cause is still the problem of losing precision of floating point numbers in the computer

For example: 1.005.toFixed(2) returns 1.00 instead of 1.01.

Reason: The actual number corresponding to 1.005 is 1.00499999999999989, which is all discarded during rounding.

1.005.toPrecision(21) //1.004999999999999989342

2. Floating point numbers

“Floating point number” is a standard for representing numbers. Integers can also be stored in the format of floating point numbers. We can also understand that floating point numbers are decimals.

In JavaScript, the current mainstream numerical type is Number, and Number uses the 64-bit double-precision floating point encoding in the IEEE754 specification.

The advantage of such a storage structure is that it can normalize integers and decimals and save storage space.

For an integer, it can be easily converted into decimal or binary. But for a floating point number, because of the existence of the decimal point, the position of the decimal point is not fixed. The solution is to use scientific notation so that the decimal point position is fixed.

The computer can only be expressed in binary (0 or 1). The formula for converting binary to scientific notation is as follows:

Among them, the value of a is 0 or 1, and e is the position where the decimal point moves.

for example:

27.0 is converted into binary as 11011.0, and expressed in scientific notation as:

Among them, the value of a is 0 or 1, and e is the position where the decimal point moves.

for example:

27.0 is converted into binary as 11011.0, and expressed in scientific notation as:

As mentioned earlier, the storage method of javaScript is a double-precision floating point number, whose length is 8 bytes, that is, 64 bits.

The 64-bit bits can be divided into three parts:

Sign bit S: The first bit is the sign bit (sign) of positive and negative numbers, 0 represents a positive number, and 1 represents a negative number;
Exponent bit E: The middle 11 bits store the exponent, which is used to represent the power number and can be a positive or negative number. In double-precision floating point numbers, the exponent has a fixed offset of 1023;
Mantissa bit M: The last 52 bits are the mantissa (mantissa), and the excess part is automatically rounded to zero;
As shown below:

for example:

27.5 converted to binary 11011.1

11011.1 converted to scientific notation [formula]

The sign bit is 1 (positive number), the exponent bit is 4 +, 1023 + 4, which is 1027

Because it is decimal and needs to be converted to binary, that is, 10000000011, the decimal part is 10111, and 52 digits must be added: 1011 1000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000`

So 27.5 is stored in the computer’s binary standard form (sign bit + exponent bit + fractional part (order)), which is as follows:

0 + 10000000011 + 011 1000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000`

2. Problem analysis

Back to the question

0.1 + 0.2 === 0.3 // false
0.1 + 0.2 = 0.30000000000000004

Through the above study, we know that in the JavaScript language, both 0.1 and 0.2 need to be converted from decimal to binary before operation.

// 0.1 and 0.2 are converted into binary before operation
0.00011001100110011001100110011001100110011001100110011010 +
0.0011001100110011001100110011001100110011001100110011010 =
0.0100110011001100110011001100110011001100110011001100111

// Converted to decimal, it is exactly 0.30000000000000004

So the output is false

Another question, then why does x=0.1 get 0.1?

The main reason is that the maximum offset of the decimal point when storing binary is 52 digits. The maximum number of digits that can be expressed is 2^53=9007199254740992, and the corresponding scientific notation mantissa is 9.007199254740992, which is also the maximum precision that JS can express.

Its length is 16, so you can use toPrecision(16) to perform precision calculations, and any excess precision will be automatically rounded up.

.10000000000000000555.toPrecision(16)
// Returns 0.1000000000000000, which is exactly 0.1 after removing the zero at the end

But the 0.1 you see is not actually 0.1. If you don’t believe me, you can try it with higher accuracy:

0.1.toPrecision(21) = 0.100000000000000005551

Summary

To store double-precision floating-point numbers, the computer needs to first convert the decimal number into binary scientific notation, and then the computer stores the binary number according to its own rules {sign bit + (exponent bit + binary of exponent offset) + decimal part} Scientific notation.

Because there is a limit on the number of digits in storage (64 bits), and some decimal floating-point numbers will appear in infinite loops when converted to binary numbers, which will cause binary rounding operations (0 to 1), when converted to decimal This causes calculation errors.

3. Solution

Theoretically, it is impossible to store infinite decimals with limited space to ensure accuracy, but we can deal with it and get the results we expect.

When you get data like 1.4000000000000001 to display, it is recommended to use toPrecision to round it up and parseFloat to convert it into a number before displaying it, as follows:

parseFloat(1.4000000000000001.toPrecision(12)) === 1.4 // True

The encapsulation method is:

function strip(num, precision = 12) {
  return + parseFloat(num.toPrecision(precision));
}

For arithmetic operations, such as + -*/, toPrecision cannot be used. The correct way is to convert the decimal into an integer and then perform the operation (first expand and then reduce the method).

Take addition as an example:

/**
 * Exact addition
 */
function add(num1, num2) {
  const num1Digits = (num1.toString().split('.')[1] || '').length;
  const num2Digits = (num2.toString().split('.')[1] || '').length;
  const baseNum = Math.pow(10, Math.max(num1Digits, num2Digits));
  return (num1 * baseNum + num2 * baseNum) / baseNum;
}

Take toFixed() as an example

//Enlarge first and then reduce the method
function toFixed(num, s) {
    var times = Math.pow(10, s)
    // 0.5 for rounding
    var des = num * times + 0.5
    //Remove decimals
    des = parseInt(des, 10) / times
    return des + ''
}
console.log(toFixed(1.333332, 5))

Finally, you can also use third-party libraries, such as Math.js and BigDecimal.js

References

  • Value-Ruan Yifeng
  • BigInt – JavaScript | MDN