Hitu Bansal
Hitu Bansal

Reputation: 3137

Javascript toFixed() is not working as expected

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?

Upvotes: 3

Views: 745

Answers (5)

Chukwuemeka Maduekwe
Chukwuemeka Maduekwe

Reputation: 8526

This worked for me. source

export const preciseRound = (value, exp) => {
  /**
   * Decimal adjustment of a number.
   *
   * @param   {String}    type    The type of adjustment.
   * @param   {Number}    value   The number.
   * @param   {Integer}   exp     The exponent (the 10 logarithm of the adjustment base).
   * @returns {Number}            The adjusted value.
   */
  function decimalAdjust(type, value, exp) {
    // If the exp is undefined or zero...
    if (typeof exp === "undefined" || +exp === 0) {
      return Math[type](value);
    }
    value = +value;
    exp = +exp;
    // If the value is not a number or the exp is not an integer...
    if (isNaN(value) || !(typeof exp === "number" && exp % 1 === 0)) {
      return NaN;
    }
    // Shift
    value = value.toString().split("e");
    value = Math[type](+(value[0] + "e" + (value[1] ? +value[1] - exp : -exp)));
    // Shift back
    value = value.toString().split("e");
    return +(value[0] + "e" + (value[1] ? +value[1] + exp : exp));
  }

  // You can use floor, ceil or round
  return decimalAdjust("round", value, exp);
};
console.log(preciseRound(1.005, -2));  // <= 1.01
console.log(preciseRound(1.341, -2));  // <= 1.34
console.log(preciseRound(1.01, -2));   // <= 1.01
console.log(preciseRound(33.355, -2)); // <= 33.36

Upvotes: 0

TylerY86
TylerY86

Reputation: 3792

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

Upvotes: 0

Ryan Allen
Ryan Allen

Reputation: 81

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

Upvotes: 0

GOTO 0
GOTO 0

Reputation: 47642

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.

Upvotes: 1

Vahid Rahmani
Vahid Rahmani

Reputation: 182

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.

Upvotes: 6

Related Questions