Precision Issues and Solutions in JavaScript
Category Programming Techniques
Numbers in JavaScript are represented using the 64-bit double-precision floating-point format as per the IEEE 754 standard. The sign bit S, exponent bit E, and significand bit M occupy 1, 11, and 52 bits respectively, and the ES5 specification indicates that the exponent bit E's value range is [-1074, 971]
.
Summary of Precision Issues
It is obviously impossible to represent infinite numbers with a finite number of bits, hence a series of precision issues arise:
Floating-point precision issues, such as
0.1 + 0.2 !== 0.3
Large number precision issues, such as
9999 9999 9999 9999 == 1000 0000 0000 0000 1
Inaccurate rounding results from toFixed, such as
1.335.toFixed(2) == 1.33
Floating-point precision and toFixed actually belong to the same category of issues, both caused by the inability of floating-point numbers to be represented precisely, as follows:
(1.335).toPrecision(20); // "1.3349999999999999645"
Regarding the large number precision issue, we can first look at the following code snippet:
// The upper limit of the integer range that can be accurately represented, with S as 1 zero, E as 11 zeros, and S as 52 ones
Math.pow(2, 53) - 1 === Number.MAX_SAFE_INTEGER // true
// The lower limit of the integer range that can be accurately represented, with S as 1 one, E as 11 zeros, and S as 52 ones
-(Math.pow(2, 53) - 1) === Number.MIN_SAFE_INTEGER // true
// The maximum number that can be represented, with S as 1 zero, E as 971, and S as 52 ones
(Math.pow(2, 53) - 1) * Math.pow(2, 971) === Number.MAX_VALUE // true
// The closest positive number to 0 that can be represented, with S as 1 zero, E as -1074, and S as 0
Math.pow(2, -1074) === Number.MIN_VALUE // true
From the above, it is clear that integers within the range [MIN_SAFE_INTEGER, MAX_SAFE_INTEGER]
can all be accurately represented, but integers beyond this range may not be accurately representable. This leads to the so-called large number precision loss issue.
Solution Approaches
The first consideration is how to solve the precision issues of floating-point arithmetic, with 3 approaches:
Considering that the deviation of each floating-point operation is very small (which is not the case), the result can be rounded to a specified precision, such as
parseFloat(result.toFixed(12))
;Convert floating-point numbers to integer operations and then divide the result. For example, 0.1 + 0.2 can be transformed into
(1*2)/3
.Convert floating-point numbers to strings and simulate the actual calculation process.
Let's first look at the first solution. In most cases, it can yield the correct result, but for some extreme cases, toFixed to 12 is not enough, such as:
210000 * 10000 * 1000 * 8.2 // 17219999999999.998
parseFloat(17219999999999.998.toFixed(12)); // 17219999999999.998, while the correct result is 17220000000000
In the above case, if you want the correct result, you need toFixed(2)
, which is obviously unacceptable.
Looking at the second solution, for example, the library number-precision uses this approach, but it also has problems, such as:
// These two floating-point numbers, when converted to integers, the result of multiplication exceeds MAX_SAFE_INTEGER
123456.789 * 123456.789 // Converted to (123456789 * 123456789)/1000000, the result is 15241578750.19052
So, the third solution is ultimately considered, and there are already many mature libraries available, such as bignumber.js, decimal.js, and big.js, etc