Home >Web Front-end >JS Tutorial >Number().toFixed() Rounding Errors: Broken But Fixable
This article was originally published on David Kaye's blog
I found rounding errors in Number().toFixed() in all JavaScript environments I tried (Chrome, Firefox, Internet Explorer, Brave, and Node.js). Surprisingly, the fix is very simple. Continue reading...
When modifying a digital formatting function that performs the same function as Intl.NumberFormat#format(), I found a rounding error in this version in toFixed().
<code class="language-javascript">(1.015).toFixed(2) // 返回 "1.01" 而不是 "1.02"</code>
The failed test is located in line 42 here. I didn't find it until December 2017, which prompted me to check for other issues.
See my tweet about this question:
There is a long history of error reporting when using toFixed().
Here are some short examples of StackOverflow issues about this issue:
Usually, these point out aa errors of a value, but no range or pattern of values that return the error result (at least I didn't find it, I might have missed something ). This allows programmers to focus on small issues and not see larger patterns. I don't blame them for this.
Unexpected results based on input must originate from the shared mode in the input. So instead of reviewing the specification of Number().toFixed(), I focused on testing with a series of values to determine where the errors appear in each series.
I created the following test function to perform a toFixed() operation on a series of integers from 1 to maxValue, adding a decimal of such as .005 to each integer. The fixed (number digit) parameter of toFixed() is calculated based on the length of the decimal value.
<code class="language-javascript">(1.015).toFixed(2) // 返回 "1.01" 而不是 "1.02"</code>
The following example performs an operation on integers 1 to 128, adds a decimal .015 for each integer, and returns an "unexpected" array of results. Each result contains given, expected, and actual fields. Here we use that array and print each item.
<code class="language-javascript">function test({fraction, maxValue}) { fraction = fraction.toString() var fixLength = fraction.split('.')[1].length - 1 var last = Number(fraction.charAt(fraction.length - 1)) var fixDigit = Number(fraction.charAt(fraction.length - 2)) last >= 5 && (fixDigit = fixDigit + 1) var expectedFraction = fraction.replace(/[\d]{2,2}$/, fixDigit) return Array(maxValue).fill(0) .map(function(ignoreValue, index) { return index + 1 }) .filter(function(integer) { var number = integer + Number(fraction) var actual = number.toFixed(fixLength) var expected = Number(number + '1').toFixed(fixLength) return expected != actual }) .map(function(integer) { var number = Number(integer) + Number(fraction) return { given: number.toString(), expected: (Number(integer.toFixed(0)) + Number(expectedFraction)).toString(), actual: number.toFixed(fixLength) } }) }</code>
For this situation, there are 6 unexpected results.
<code class="language-javascript">test({ fraction: .015, maxValue: 128 }) .forEach(function(item) { console.log(item) })</code>
I found this error contains three parts:
For example, for integers 1 to 128, (value).toFixed(2) uses different 3 decimal places ending in 5, resulting in the following results:
Those who know more about binary and floating point mathematics than I do may be able to infer the root cause. I leave this to the reader as an exercise.
Fixed by more than one decimal to fix the value is always rounded correctly; for example, (1.0151).toFixed(2) returns "1.02" as expected. Both the test and polyfill use this knowledge for correctness checks.
This means that all toFixed() implementations have a simple fix: if the value contains a decimal, append "1" to the end of the string version of the value to be modified. This may not be "compliant with specifications", but it means we will get the expected results without having to revisit the lower level of binary or floating point operations.
Before all implementations are modified, if you are willing to do this (not everyone wants it), you can override toFixed() with the following polyfill.
<code>Object { given: "1.015", expected: "1.02", actual: "1.01" } Object { given: "4.015", expected: "4.02", actual: "4.01" } Object { given: "5.015", expected: "5.02", actual: "5.01" } Object { given: "6.015", expected: "6.02", actual: "6.01" } Object { given: "7.015", expected: "7.02", actual: "7.01" } Object { given: "128.015", expected: "128.02", actual: "128.01" }</code>
Then run the test again and check if the result length is zero.
<code class="language-javascript">(1.005).toFixed(2) == "1.01" || (function(prototype) { var toFixed = prototype.toFixed prototype.toFixed = function(fractionDigits) { var split = this.toString().split('.') var number = +(!split[1] ? split[0] : split.join('.') + '1') return toFixed.call(number, fractionDigits) } }(Number.prototype));</code>
Or just run the initial conversion that starts this article.
<code class="language-javascript">test({ fraction: .0015, maxValue: 516 }) // Array [] test({ fraction: .0015, maxValue: 516 }).length // 0</code>
Thank you for reading!
The Number.toFixed() method in JavaScript is used to format numbers using fixed-point notation. It returns a string representation of a number that does not use exponential representation and has exactly the specified number of digits after the decimal point. If necessary, the number is rounded and the result string has a decimal point after the length.
Due to the way JavaScript handles binary floating point numbers, the Number.toFixed() method sometimes gives inaccurate results. JavaScript uses binary floating point numbers, which cannot accurately represent all decimal decimals. This inaccuracy can lead to unexpected results when the number is rounded to a specific decimal number using the Number.toFixed() method.
One way to fix rounding errors in Number.toFixed() method is to use a custom rounding function. This function can take into account the characteristics of JavaScript's digital processing and provide more accurate results. For example, you could use a function that multiplies the number by a power of 10, rounds it to the nearest integer, and divides it by the same power of 10.
No, the Number.toFixed() method can only be used with numeric values. If you try to use it with a non-numeric value, JavaScript will throw a TypeError. If you need to use this method with a value that may not be a number, you should first check if the value is a number.
Number.toFixed() method and other rounding methods is usually negligible. However, if you are doing a lot of operations, using a custom rounding function may be slightly faster than using the Number.toFixed() method.
No, the Number.toFixed() method can only round to a maximum of 20 decimal places. If you try to round to more than 20 decimal places, JavaScript will throw a RangeError.
Number.toFixed() method handles negative numbers the same way as dealing with positive numbers. It rounds the absolute value of the number to the specified number of decimal places and adds a negative sign.
Yes, the Number.toFixed() method is a standard part of JavaScript and should be available in all JavaScript environments. However, because different JavaScript engines handle numbers differently, the results may not be exactly the same in all environments.
If you do not pass any arguments to the Number.toFixed() method, it will round to 0 decimal places by default. This means it will round the number to the nearest integer.
Yes, you can use the Number.toFixed() method with large numbers. However, remember that JavaScript can only accurately represent numbers up to 2^53 – 1. If you try to use the Number.toFixed() method with numbers larger than this number, it may not give accurate results.
The above is the detailed content of Number().toFixed() Rounding Errors: Broken But Fixable. For more information, please follow other related articles on the PHP Chinese website!