破损的 toFixed 实现

9
默认的javascript中的“Number.toFixed”实现似乎有些问题。
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

我需要一种比现在更加稳定的四舍五入方法。

在8.500和8.660之间,以下数字无法正确地进行四舍五入。

8.575
8.635
8.645
8.655

我已经尝试修复原型实现,但只完成了一半。有人能建议一些改变使其更加一致吗?

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);
};

1
我可以想象这是浮点精度误差。 - Håvard
类似于这个问题?https://dev59.com/J3RB5IYBdhLWcg3wn4jc - Prescott
可能与浮点数问题有关,请参见 https://dev59.com/tXM_5IYBdhLWcg3wQQpd - wildcard
我刚刚在Chromium V12.0上尝试了您上面的示例,并得到了以下结果:8.55 8.56 8.57 8.59。因此,您的JavaScript实现可能会有所不同。 - HBP
为什么不使用由AngularJS修改的Mozilla实现呢?这是一个一行代码的解决方案,而且实际上在其中一个答案中也被建议使用。 - cprn
5个回答

8
感谢pst的答案。我的实现几乎成功了,但在某些情况下由于浮点误差而失败。
这是我的函数中的问题所在: Math.round(this * factor)
(它在Number.prototype上,因此“this”是数字); 8.575 * 100变成了857.4999999999999,进而向下舍入。 通过将该行更改为以下内容来进行更正: Math.round(Math.round(this * factor * 100) / 100)
我的整个解决方法现在已更改为:
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);
};

8
这是由于浮点数误差造成的。
比较 (8.575).toFixed(20)(8.575).toFixed(3),并想象这个命题:8.575 < real("8.575"),其中real是一个创建具有无限精度的实数的虚构函数。
也就是说,原始数字与预期不符,并且已经引入了不准确性。
我能想到的一个快速“解决方法”是:乘以1000(或适当的倍数),获取其toFixed(0)(仍然有限,但荒谬),然后将其转换回小数形式。
祝编码愉快。

自然地,我不能在覆盖相同函数时使用“toFixed”。我不得不坚持使用“Math.round”,但是你的答案指引了我正确的方向。谢谢。 - Joshua
@Joshua:是的,你可以这样做。只需将 toFixed 的副本存储在一个变量中,并将其 apply() 到数字上即可。 - Eric

4

一种解决方法是在取整之前给每个数字增加一个固定的容差(epsilon)。这个值应该很小,但也不能太小。

例如,如果eps = 1e-9,那么这个数值:

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

变为:

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

1
也许这会对某些人有所帮助,这是固定的流行formatMoney()函数,但具有正确的四舍五入。
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

0

检查我的答案

function toFixed( num, precision ) {
    return (+(Math.round(+(num + 'e' + precision)) + 'e' + -precision)).toFixed(precision);
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接