8000
Skip to content

fix: avoid native float overflow when scaling large values in toFixed - #777

Open
spokodev wants to merge 1 commit into
BenjaminVanRyseghem:developfrom
spokodev:fix/large-value-mantissa-precision
Open

fix: avoid native float overflow when scaling large values in toFixed#777
spokodev wants to merge 1 commit into
BenjaminVanRyseghem:developfrom
spokodev:fix/large-value-mantissa-precision

Conversation

@spokodev
Copy link
Copy Markdown

Problem

The integer part of a large integral value changes depending only on the requested mantissa length:

numbro(9.999999e20).format({mantissa: 0, trimMantissa: false}) // '999999900000000000000'        correct
numbro(9.999999e20).format({mantissa: 2, trimMantissa: false}) // '999999900000000000000.00'     correct
numbro(9.999999e20).format({mantissa: 3, trimMantissa: false}) // '999999899999999900000.000'    corrupted
numbro(9.999999e20).format({mantissa: 4, trimMantissa: false}) // '999999899999999900000.0000'   corrupted

An integral value's integer part must be invariant to the requested mantissa length, so the 999999899999999900000 results are wrong.

Root cause

toFixed in src/formatting.js scales, rounds and divides in native float:

const n = new BigNumber(roundingFunction(+`${value}e+${precision}`) / (Math.pow(10, precision)));

For an integral value above 2^53, scaling by 10^precision and dividing back in native float loses precision. The result flips between correct and corrupted purely as a function of the requested decimal count.

Fix

bignumber.js is already imported in this module. The scale and divide now run through BigNumber, so large values keep their digits. The user-supplied roundingFunction is still applied, so rounding behavior is unchanged.

cons
9FFB
t power = new BigNumber(10).pow(precision);
const rounded = roundingFunction(new BigNumber(value).times(power).toNumber());
const n = new BigNumber(rounded).div(power);
return n.toFixed(precision);

Test

Added a regression test in the toFixed block asserting the integer part of 9.999999e20 stays 999999900000000000000 for mantissa 0..6. It fails on the current code (mantissa 3, 4, 6) and passes with the fix. Full unit suite stays green (463 specs, 0 failures); integration suite green (2 specs, 0 failures).

`toFixed` scaled, rounded and divided the value in native float
(`roundingFunction(+`${value}e+${precision}`) / Math.pow(10, precision)`).
For integral values above 2^53 this loses precision, so the integer part
of the output changed depending only on the requested mantissa length.

Example with 9.999999e20:

  format({mantissa: 0}) -> 999999900000000000000      (correct)
  format({mantissa: 2}) -> 999999900000000000000.00   (correct)
  format({mantissa: 3}) -> 999999899999999900000.000  (corrupted)
  format({mantissa: 4}) -> 999999899999999900000.0000 (corrupted)

An integral value's integer part must be invariant to the requested
mantissa length. bignumber.js is already imported in this module, so the
scale and divide now run through BigNumber and large values keep their
digits. Adds a regression test asserting the integer part stays constant
for mantissa 0..6.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

0