2

I am using toFixed but the method does not operate as expected

parseFloat(19373.315).toFixed(2);

//19373.31   Chrome 

Expected Output : 19373.32

parseFloat(9373.315).toFixed(2);
// 9373.32  Working fine

Why does the first example round down, whereas the second example round up?

Denis Tsoi
  • 9,428
  • 8
  • 37
  • 56
Hitu Bansal
  • 2,917
  • 10
  • 52
  • 87

4 Answers4

6

The problem is that binary floating point representation of most decimal fractions is not exact. The internal representation of 19373.315 may actually be something like 19373.314999999, so toFixed rounds down, while 19373.315 might be 19373.315000001, which rounds up.

  • Assuming toFixed casts to 32-bit float; https://www.h-schmidt.net/FloatConverter/IEEE754.html 19373.315 -> 19373.314453125 (error of -0.000546875) – TylerY86 May 23 '18 at 05:27
  • This is despite `(19373.315).toFixed(4)` = 19373.3150 ... Even if this is "expected" or "intended", I'd still report it as a bug. Should use a double and proper rounding during conversion to fixed string. I think the spec even says so. :\ – TylerY86 May 23 '18 at 05:30
  • I consolidated my comments and research into an answer and provided a "fixed" workaround. – TylerY86 May 23 '18 at 06:13
1

Why does the first example round down, whereas the second example round up?

Look at the binary representation of the two values in memory.

const farr = new Float64Array(2);
farr[0] = 19373.315;
farr[1] = 9373.315;
const uarr = new Uint32Array(farr.buffer);
console.log(farr[0], uarr[1].toString(2).padStart(32, 0) + uarr[0].toString(2).padStart(32, 0));
console.log(farr[1], uarr[3].toString(2).padStart(32, 0) + uarr[2].toString(2).padStart(32, 0));

Without diving into the details, we can see that the second value has an additional '1' at the end, which is lost in the first larger value when it is fit into 64 bits.

GOTO 0
  • 42,323
  • 22
  • 125
  • 158
  • Yet `(19373.315).toFixed(4)` yields `19373.3150`. The 1 at the end is why `(19373.3150).toFixed(39)` yields `19373.314999999998690327629446983337402343750`... Pretty sure there's a bug here where toFixed has a missed rounding case. – TylerY86 May 23 '18 at 05:54
  • 1
    @TylerY86 Gee, that reminds me of [another Chrome bug](https://stackoverflow.com/questions/25602799), now luckily fixed. – GOTO 0 May 23 '18 at 06:04
  • I think that bug is a significant order of magnitude (-0.998) worse in precision than this (-.000000000002) though. It still comes out `0.0022002200220022002200220022002201` though. lol – TylerY86 May 23 '18 at 06:12
  • 1
    Perhaps the spec might have been to specific with instructions for the toFixed function and included a bug? (FF does this too.) lol – TylerY86 May 23 '18 at 06:18
  • ^-0.998 should be 0.098 – TylerY86 May 23 '18 at 06:19
  • Wait, that's toString... `0.0022002200220022002200220022002201` is roughly correct in base 3? – TylerY86 May 23 '18 at 06:23
  • @TylerY86 Yes, this was eventually fixed in some release of V8 and now `toString` seems to work well again. – GOTO 0 May 23 '18 at 06:41
0

Assuming toFixed casts to 32-bit float; Check with this utility...

19373.315 is stored as 19373.314453125 (an error of -0.000546875) in 32-bit floating point format.

This is despite (19373.315).toFixed(4) coming out as 19373.3150.

Even if this is "expected" or "intended", I'd still report it as a bug.

It should use a double during the rounding check, and thus proper rounding during conversion to fixed string.

I think the spec even says so. :\

In the V8 javascript engine source, the Number.prototype.toFixed function invokes DoubleToFixedCString in this file ...

There's probably some inappropriate optimization in there... (Looking into it.)

I'd suggest submitting an additional test case for V8 with 19373.315 specifically.

(19373.3150).toFixed(39) yields 19373.314999999998690327629446983337402343750.

Rounding occurs once to bring it up to 19373.315 - which is correct - but not at the right digit when rounding to 2 digits.

I think this should have a second pass on rounding here to catch edge cases like this. I think it might have to round to n+1 digits, then again to n digits. Maybe there's some other clever way to fix it though.

function toFixedFixed(a,n) {
  return (a|0) + parseFloat((a % 1).toFixed(n+1)).toFixed(n).substr(1);
}

console.log(toFixedFixed(19373.315,2)); // "19373.32"
console.log(toFixedFixed(19373.315,3)); // "19373.315"
console.log(toFixedFixed(19373.315,4)); // "19373.3150"
console.log(toFixedFixed(19373.315,37)); // "19373.3149999999986903276294469833374023438"
console.log(toFixedFixed(19373.315,38)); // "19373.31499999999869032762944698333740234375"
console.log(toFixedFixed(19373.315,39)); // "19373.314999999998690327629446983337402343750"

(Adopted from my comments on Vahid Rahmani's answer, who is correct.)

TylerY86
  • 3,737
  • 16
  • 29
0

Other answers have explained why, I would suggest using a library like numeral.js which will round things as you would expect.

Ryan Allen
  • 71
  • 1
  • 5