Use Number.toFixed() with caution
Introduction
Recently, I encountered a hidden bug in the company's project. After debugging for a long time, I found that it was actually toFixedcaused by the function precision problem, which triggered a series of thoughts.
We all know that in the binary environment of computers, the calculation accuracy of floating-point numbers will be missing. The most classic example is why 0.1+0.2 is not equal to 0.3?
Encountering the above problems, we will naturally think of toFixed
ways to round up, but the results are not satisfactory!
Precision issues with toFixed()
Let's take a look toFixedat the different performances under chrome, Firefox, and IE browsers:
Rounding as you can see toFixed
is not accurate on chrome, firefox.
In toFixed
chrome and Firefox, it is not widely used on the Internet to use the banker's rounding method for rounding.
The rule of the banker's rounding method is "round up to five. If the number after five is non-zero, add one. If the number after five is zero, see the odd-even. If it is even before five, it should be rounded up. If it is odd before five, add one."
For example, banker's rounding does not work on (2.55).toFixed(1) = 2.5, (3.55).toFixed(1) = 3.5.
Flip through the ecmascript specification for toFixedthe following statement:
The above specification roughly means that if toFixed
the input parameter is less than 10 to the 21st power, then take an integer n, so that the exact value of n*10^f - x is as close to 0 as possible, if there are two such n, take the larger n. This passage may be a bit obscure, let's take an example such as 1.335.toFixed(2)
In the example above, 1.335.toFixed(2) should be 1.34 according to rounding to 50%, but the actual situation is indeed 1.33. This is because when n=133, n*10^f - x is closer to 0, so the final result is 1.33.
Solution
1. Rewrite toFixed()
toFixed
We can implement rounding by overriding the method:
Number.prototype.toFixed = function (length) {
var carry = 0 // store the carry flag
var num, multiple // num is the original floating-point number multiplied by multiple times, and multiple is 10 to the power of length
var str = this + '' // convert the number that calls this method to a string
var dot = str.indexOf('.') // find the position of the decimal point
if (str.substr(dot + length + 1, 1) >= 5) carry = 1 // Find the position of the number to be rounded, manually judge whether it is greater than or equal to 5, and set the carry flag to 1 if the condition is met
multiple = Math.pow(10, length) // Set the multiple to expand the floating point number
num = Math.floor(this * multiple) + carry // remove all numbers after rounding and add our manual carry
var result = num / multiple + '' // reduce the rounded integer to the original floating point number
/*
* handle no decimals after rounding
*/
dot = result.indexOf('.')
if (dot < 0) {
result += '.'
dot = result.indexOf('.')
}
/*
* handle multiple carry
*/
var len = result.length - (dot + 1)
if (len < length) {
for (var i = 0; i < length - len; i++) {
result += 0
}
}
return result
}
The general idea of this method is to first find the rounding digit, determine whether the position is greater than or equal to 5, manually add one digit if the condition is satisfied, and then enlarge the original floating-point number by the parameter exponent of 10 through the parameter size, and then include the rounded digit. The number of bits used is Math.floorall removed, and whether to carry or not is determined according to our previous manual carry.
2. high-precision-four-fundamental-rules
Find a high-precision basic four arithmetic npm package on GitHub to make up for the lack of method calculation precision in native JS toFixed. The author rewrote the modified method with a rounding algorithm and packaged it into an npm package!
// Install
$ npm install high-precision-four-fundamental-rules --save
// use
import {add, subtract, multiply, divide} from 'high-precision-four-fundamental-rules';
add(1, 2, 4); // '3.0000'
subtract(1, 2, 3); // '-1.000';
multiply(1, 2, 2); // '2.00';
divide(1, 3, 7); // '0.3333333';