8

The default implementation of javascript's "Number.toFixed" appears to be a bit broken.

console.log((8.555).toFixed(2));    // returns 8.56
console.log((8.565).toFixed(2));    // returns 8.57
console.log((8.575).toFixed(2));    // returns 8.57
console.log((8.585).toFixed(2));    // returns 8.59

I need a rounding method that is more consistent than that.

In the range between 8.500 and 8.660 the following numbers don't round up correctly.

8.575
8.635
8.645
8.655

I've tried to fix the prototype implementation as follows, but it's only half way there. Can anyone suggest any change that would make it work more consistently?

Number.prototype.toFixed = function(decimalPlaces) {
    var factor = Math.pow(10, decimalPlaces || 0);
    var v = (Math.round(this * factor) / factor).toString();
    if (v.indexOf('.') >= 0) {
        return v + factor.toString().substr(v.length - v.indexOf('.'));
    }
    return v + '.' + factor.toString().substr(1);
};
Joshua
  • 1,913
  • 1
  • 19
  • 31
  • 1
    I can imagine this is a floating point precision error. – Håvard Mar 30 '11 at 18:46
  • [http://stackoverflow.com/questions/566564/javascript-functions-math-round0-vs-tofixed0-and-browser-inconsistencies](http://stackoverflow.com/questions/566564/javascript-functions-math-round0-vs-tofixed0-and-browser-inconsistencies) similar? – Prescott Mar 30 '11 at 18:55
  • I just tried your examples above on Chromium V12.0 and received the following results : 8.55 8.56 8.57 8.59. So your mileage may vary depending on your JavaScript implementation. – HBP Mar 30 '11 at 19:10
  • Why not to use Mozilla's implementation modified by [AngularJS](https://github.com/angular/angular.js/blob/master/src/ng/filter/filters.js#L180)? It's a one-liner and was actually suggested in one of the answers. – cprn Jul 06 '15 at 14:21
  • It's probably related to floating point problems, see https://stackoverflow.com/questions/1458633/elegant-workaround-for-javascript-floating-point-number-problem – wildcard Mar 30 '11 at 18:56

5 Answers5

8

This is because of floating-point errors.

Compare (8.575).toFixed(20) with (8.575).toFixed(3) and imagine this proposition: 8.575 < real("8.575"), where real is an imaginary function that creates a real number with infinite precision.

That is, the original number is not as expected and the inaccuracy has already been introduced.

One quick "workabout" I can think of is: Multiply by 1000 (or as appropriate), get the toFixed(0) of that (still has a limit, but it's absurd), then shove back in the decimal form.

Happy coding.

  • naturally, I can't use "toFixed" in my override of the same function. I had to stick to using "Math.round", but your answer pointed me in the right direction. Ty – Joshua Mar 30 '11 at 19:24
  • @Joshua: Yes you can. Just store a copy of the toFixed` in a variable, and `apply()` it to the number. – Eric Mar 30 '11 at 19:29
8

Thanks for the answer pst. My implementation almost worked, but didn't in some cases because of floating point errors.

this line in my function is the culprit: Math.round(this * factor)

(it's on the Number.prototype, so "this" is the number); 8.575 * 100 comes out to 857.4999999999999, which in turn rounds down. this is corrected by changing the line to read as follows: Math.round(Math.round(this * factor * 100) / 100)

My entire workaround is now changed to:

Number.prototype.toFixed = function(decimalPlaces) {
    var factor = Math.pow(10, decimalPlaces || 0);
    var v = (Math.round(Math.round(this * factor * 100) / 100) / factor).toString();
    if (v.indexOf('.') >= 0) {
        return v + factor.toString().substr(v.length - v.indexOf('.'));
    }
    return v + '.' + factor.toString().substr(1);
};
Nick
  • 589
  • 7
  • 18
Joshua
  • 1,913
  • 1
  • 19
  • 31
4

A consistent solution would be to add a fixed tolerance (epsilon) to each number before rounding. It should be small, but not too small.

For example, with an eps = 1e-9, this:

console.log((8.555).toFixed(2));    // returns 8.56
console.log((8.565).toFixed(2));    // returns 8.57
console.log((8.575).toFixed(2));    // returns 8.57
console.log((8.585).toFixed(2));    // returns 8.59

Becomes this:

console.log((8.555 + eps).toFixed(2));    // returns 8.56
console.log((8.565 + eps).toFixed(2));    // returns 8.57
console.log((8.575 + eps).toFixed(2));    // returns 8.58
console.log((8.585 + eps).toFixed(2));    // returns 8.59
William
  • 2,695
  • 1
  • 21
  • 33
1

Maybe it will help someone, this is fixed popular formatMoney() function, but with correct roundings.

Number.prototype.formatMoney = function() {
  var n = this,
  decPlaces = 2,
  decSeparator = ",",
  thouSeparator = " ",
  sign = n < 0 ? "-" : "",
  i = parseInt(n = Math.abs(+n || 0)) + "",
  j = (j = i.length) > 3 ? j % 3 : 0,
  decimals = Number(Math.round(n +'e'+ decPlaces) +'e-'+ decPlaces).toFixed(decPlaces),
  result = sign + (j ? i.substr(0, j) + thouSeparator : "") + i.substr(j).replace(/(\d{3})(?=\d)/g, "$1" + thouSeparator) + (decPlaces ? decSeparator + Math.abs(decimals-i).toFixed(decPlaces).slice(2) : "");
  return result;
};

(9.245).formatMoney(); // returns 9,25
(7.5).formatMoney();   // returns 7,50
(8.575).formatMoney(); // returns 8,58
Pawel
  • 360
  • 2
  • 10
0

Check my answer

function toFixed( num, precision ) {
    return (+(Math.round(+(num + 'e' + precision)) + 'e' + -precision)).toFixed(precision);
}
user2823670
  • 537
  • 4
  • 6