Home >Web Front-end >JS Tutorial >Number().toFixed() Rounding Errors: Broken But Fixable

Number().toFixed() Rounding Errors: Broken But Fixable

Jennifer Aniston
Jennifer AnistonOriginal
2025-02-15 10:02:12180browse

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...

Key Points

  • JavaScript's Number().toFixed() function has rounding errors in multiple environments (Chrome, Firefox, Internet Explorer, Brave, and Node.js). This error is inconsistent, and occurs when the last significant number of the decimal part is 5, and the trim length is only reduced by one by one in the decimal part, and a different integer value is applied.
  • When the value contains a decimal, this error can be fixed by appending "1" to the end of the string version of the value to be modified. This solution does not require a reexamination of lower-level binary or floating-point operations.
  • The toFixed() function can be overridden using polyfill until all implementations are modified. However, not everyone is satisfied with this approach. polyfill involves the prototype.toFixed function and checks whether the result length is zero.

Warm up

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:

  • Error Alert
  • Compare with Intl.NumberFormat
  • Summary
  • Polyfill

Error Report

There is a long history of error reporting when using toFixed().

  • Chrome
  • Firefox
  • Firefox, see also
  • Internet Explorer

Here are some short examples of StackOverflow issues about this issue:

  • Example 1
  • Example 2

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.

Find pattern

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.

Test function

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>

Usage

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>

Output

For this situation, there are 6 unexpected results.

<code class="language-javascript">test({ fraction: .015, maxValue: 128 })
  .forEach(function(item) {
    console.log(item)
  })</code>

Discover

I found this error contains three parts:

  1. The last significant digit of the decimal part must be 5 (.015 and .01500 produce the same result).
  2. The trim length must be reduced by only one decimal part.
  3. With different integer values ​​applied, errors occur inconsistently.

Inconsistent?

For example, for integers 1 to 128, (value).toFixed(2) uses different 3 decimal places ending in 5, resulting in the following results:

  • Twisted numbers ending in .005 always fail (!!)
  • Twisted numbers ending in .015 fail for 1, then 4 to 7, then 128
  • Twisted numbers ending in .025 fail for 1, 2, 3, and then 16 to 63
  • Twisted numbers ending with .035 fail for 1, then 32 to 128
  • Twisted numbers ending with .045 fail for 1 to 15, then 128
  • Twisted numbers ending with .055 fail for 1, then 4 to 63
  • Twisted numbers ending in .065 fail for 1, 2, 3, then 8 to 15, then 32 to 128
  • Twisted numbers ending in .075 fail for 1, then 8 to 31, then 128
  • Twisted numbers ending in .085 fail for 1 to 7, then 64 to 127 (!!)
  • Twisted numbers ending in .095 fail for 1, then 4 to 7, then 16 to 128

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 toFixed()

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.

Polyfill

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!

FAQs (FAQ) on JavaScript's Number.toFixed() method

What is the purpose of the Number.toFixed() method in JavaScript?

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.

Why does the Number.toFixed() method sometimes give inaccurate results?

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.

How to fix rounding error in 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.

Can I use the Number.toFixed() method with non-numeric values?

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.

Is there a performance difference between the Number.toFixed() method and other rounding methods?

The performance difference between the

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.

Can I use the Number.toFixed() method to round to more than 20 decimal places?

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.

How to deal with negative numbers?

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.

Can I use the Number.toFixed() method in all JavaScript environments?

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.

What happens if I don't pass any arguments to the Number.toFixed() method?

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.

Can I use the Number.toFixed() method with large numbers?

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!

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn