如何处理JavaScript中的浮点数精度问题?

827

我有以下的测试脚本:

function test() {
  var x = 0.1 * 0.2;
  document.write(x);
}
test();

此代码将打印出结果0.020000000000000004,而实际上应该只打印出0.02(如果您使用计算器)。据我所知,这是由于浮点数乘法精度误差导致的。

有没有什么好的解决方案,以便在这种情况下我可以得到正确的结果0.02?我知道有像toFixed这样的函数,或者舍入可能是另一种可能性,但我想真正地将整个数字打印出来,不要进行任何切割和舍入。只是想知道您中的某个是否有一些不错、优雅的解决方案。

当然,否则我会四舍五入到大约10位。


155
实际上,这个错误是因为无法将0.1映射为有限的二进制浮点数。 - Aaron Digulla
18
大多数分数无法精确地转换为小数。这里有一个好的解释:http://docs.python.org/release/2.5.1/tut/node16.html。 - Nate Zaugg
4
(new Number(0.1)).valueOf() 的值为 0.1 - Salman A
63
你的JavaScript运行时隐藏了这个问题并不意味着我是错误的。 - Aaron Digulla
12
与Aaron的观点不同,有办法在二进制中完美无误地编码0.1。但是IEEE 754并不一定定义了这一点。想象一种表示方式,在其中一方面用二进制编码整数部分,在另一方面用二进制编码小数部分,直到n个小数位,就像正常的大于0的整数一样,并最后表示小数点的位置。这样,你将完美地表示0.1,没有任何错误。顺便说一句,由于JS内部使用有限数量的小数位,开发人员可能已经编写了要避免在最后几位上犯错的代码。 - Fabien Haddadi
显示剩余10条评论
47个回答

12

请注意,对于一般用途来说,这种行为很可能是可以接受的。
问题在于比较浮点数值以确定适当的操作时会出现问题。
随着ES6的出现,定义了一个新的常量Number.EPSILON来确定可接受的误差范围:
因此,我们可以通过以下方式执行比较:


```javascript if (Math.abs(x - y) < Number.EPSILON) { // perform action } ```
0.1 + 0.2 === 0.3 // which returns false

你可以定义一个自定义比较函数,像这样:

function epsEqu(x, y) {
    return Math.abs(x - y) < Number.EPSILON;
}
console.log(epsEqu(0.1+0.2, 0.3)); // true

来源: http://2ality.com/2015/04/numbers-math-es6.html#numberepsilon


在我的情况下,Number.EPSILON 太小了,导致例如 0.9 !== 0.8999999761581421 - Tom
Number.EPSILON是无用的,因为该值随数字而变化。如果数字足够小,则可以使用它。在非常大的浮点数中,epsilon甚至可能超过1。 - Cem Kalyoncu

11
你获得的结果是正确的,并且在不同语言、处理器和操作系统中的浮点数实现中是相当一致的,唯一改变的是当浮点数实际上是双精度时的不准确程度(或更高)。
在二进制浮点数中,0.1就像十进制中的1/3(即0.3333333333333……),只是没有精确处理它的方法。因此,如果你正在处理浮点数,请始终预期小的舍入误差,因此你还需要将显示的结果四舍五入到某个可接受的值。作为回报,你得到非常快速和强大的算术运算,因为所有计算都在处理器的本地二进制中进行。
大多数情况下,解决方案并不是转向定点算术,主要是因为它速度较慢,而且99%的时间你只需要大致的精度。如果你正在处理需要那种级别精度的东西(例如金融交易),JavaScript可能不是最好的工具(因为你想强制使用固定点类型,静态语言可能更好)。
如果你正在寻找优雅的解决方案,很遗憾,这就是它:浮点数很快但有小的舍入误差 - 在显示它们的结果时始终将其四舍五入到某个可接受的值。

11

2
按照惯例,以“5”结尾的数字应四舍五入到最接近的偶数(因为总是向上或向下舍入会引入结果偏差)。因此,将4.725四舍五入到小数点后两位应该是4.72。 - Mark A. Durham

11

decimal.jsbig.jsbignumber.js 可以在 JavaScript 中避免浮点数运算问题:

0.1 * 0.2                                // 0.020000000000000004
x = new Decimal(0.1)
y = x.times(0.2)                          // '0.2'
x.times(0.2).equals(0.2)                  // true

big.js:极简主义;易于使用;精度以小数位指定;仅适用于除法的精度。

bignumber.js:支持2-64进制;配置选项;NaN;Infinity;精度以小数位指定;仅适用于除法的精度;基数前缀。

decimal.js:支持2-64进制;配置选项;NaN;Infinity;非整数幂、exp、ln、log;精度以有效数字指定;总是应用精度;随机数。

详细比较请参考此链接


“非整数幂”是一项特定功能吗?似乎原生的 Math.pow** 已经处理了这个问题? - Tchakabam

9

0.6乘以3太棒了!)) 对我来说,这个很好用:

function dec( num )
{
    var p = 100;
    return Math.round( num * p ) / p;
}

非常非常简单))
请注意,这里是一个HTML标签。

这个能处理像 8.22e-8 * 1.3 这样的东西吗? - Paul Carlton
0.6 x 3 = 1.8,你提供的代码却得出了2...这样不好。 - Zyo
@Zyo 在这个实例中返回了1.8。你是怎么运行它的? - Drenai
有趣。你可以交换乘法和除法运算符,它仍然有效。 - Andrew

8
为了避免这种情况,您应该使用整数值而不是浮点数。因此,当您想要精确到2个位置时,请使用值*100;对于3个位置,请使用1000。在显示时,您可以使用格式化程序来添加分隔符。
许多系统都不使用小数点进行计算。这就是为什么许多系统使用美分(作为整数)而不是美元/欧元(作为浮点数)的原因。

8

虽然不够优雅,但它能完成任务(去除尾部零)

var num = 0.1*0.2;
alert(parseFloat(num.toFixed(10))); // shows 0.02

8
toFixed 不总是有效:https://dev59.com/wXRB5IYBdhLWcg3wUVtd#661757 - Sam Hasler

8

首先将两个数字转换为整数,执行表达式,然后将结果除以原来的数以获取小数位:

function evalMathematicalExpression(a, b, op) {
    const smallest = String(a < b ? a : b);
    const factor = smallest.length - smallest.indexOf('.');

    for (let i = 0; i < factor; i++) {
        b *= 10;
        a *= 10;
    }

    a = Math.round(a);
    b = Math.round(b);
    const m = 10 ** factor;
    switch (op) {
        case '+':
            return (a + b) / m;
        case '-':
            return (a - b) / m;
        case '*':
            return (a * b) / (m ** 2);
        case '/':
            return a / b;
    }

    throw `Unknown operator ${op}`;
}

以下是几个操作的结果(排除的数字来自于eval函数):

0.1 + 0.002   = 0.102 (0.10200000000000001)
53 + 1000     = 1053 (1053)
0.1 - 0.3     = -0.2 (-0.19999999999999998)
53 - -1000    = 1053 (1053)
0.3 * 0.0003  = 0.00009 (0.00008999999999999999)
100 * 25      = 2500 (2500)
0.9 / 0.03    = 30 (30.000000000000004)
100 / 50      = 2 (2)

这个函数对我有效,修复了当 AMBUSDT 交易对由于0.00001*10000000返回100.00000000000001而无法正常工作的问题,谢谢Simon。 - jmp
这对我来说没问题,我用这个函数修复了TradingView轻量级图表,当AMBUSDT交易对由于0.00001*10000000返回100.00000000000001时无法正常工作,谢谢Simon。 - undefined
你知道为什么整数运算没有问题,而浮点数却存在精度问题的理论吗?我很想更好地理解这个问题。 - undefined
更新:在阅读了一些关于IEEE-754的资料之后(我的脑袋现在疼痛不已),我发现并非所有的整数都能被精确地表示,只有那些小于9007199254740993的整数可以(因为JS使用64位精度)。JS会将这个数字向下舍入1个单位。如果超过这个数,你只能以2的倍数递增。但对于99%的使用情况来说,这已经足够了。 - undefined

7

问题

浮点数无法精确地存储所有小数值。因此,在使用浮点格式时,输入值始终会存在舍入误差。 输入误差当然会导致输出误差。对于离散函数或运算符,输出在函数或运算符离散的位置周围可能会有很大的差异。

浮点值的输入和输出

因此,在使用浮点变量时,您应该始终注意这一点。并且,任何你想要从浮点计算中得到的输出都应该在显示之前进行格式化 / 调整,以考虑这一点。
当仅使用连续函数和运算符时,通常将其舍入到所需的精度即可(不要截断)。用于将浮点数转换为字符串的标准格式功能通常会为您完成此操作。
由于舍入会添加一个误差,这可能会导致总误差超过所需精度的一半,因此应根据预期输入精度和所需输出精度对输出进行校正。你应该

  • 将输入舍入到预期精度,或确保没有可以输入更高精度的值。
  • 在舍入/格式化输出之前添加一个小值,该值小于或等于所需精度的1/4,并大于舍入输入和计算期间引起的最大预期误差。如果不可能,则使用的数据类型的精度组合不足以为计算提供所需的输出精度。

这两件事通常不会做,对于大多数用户来说,由于未进行这些更正而引起的差异太小,以至于不重要,但我曾经有过一个项目,其中没有进行这些更正而导致用户拒绝接受输出。

离散函数或运算符(如模运算)

当涉及到离散运算符或函数时,可能需要额外的更正以确保输出正确。舍入和在舍入之前添加小的更正无法解决问题。
可能需要对中间计算结果进行特殊的检查 / 更正,即在应用离散函数或运算符之后立即进行。 对于特定情况(模运算符),请参见我的答案:Why does modulus operator return fractional number in javascript?

最好避免出现问题

通过使用可以存储预期输入而没有舍入误差的数据类型(整数或固定点格式)进行此类计算,通常更为高效。财务计算中永远不应使用浮点值作为例子。


6

优雅、可预测和可重用

让我们以一种优雅且可重用的方式来解决问题。只需在数字、公式或内置的Math函数结尾处添加.decimal,即可让您轻松地获得所需的浮点精度。

// First extend the native Number object to handle precision. This populates
// the functionality to all math operations.

Object.defineProperty(Number.prototype, "decimal", {
  get: function decimal() {
    Number.precision = "precision" in Number ? Number.precision : 3;
    var f = Math.pow(10, Number.precision);
    return Math.round( this * f ) / f;
  }
});


// Now lets see how it works by adjusting our global precision level and 
// checking our results.

console.log("'1/3 + 1/3 + 1/3 = 1' Right?");
console.log((0.3333 + 0.3333 + 0.3333).decimal == 1); // true

console.log(0.3333.decimal); // 0.333 - A raw 4 digit decimal, trimmed to 3...

Number.precision = 3;
console.log("Precision: 3");
console.log((0.8 + 0.2).decimal); // 1
console.log((0.08 + 0.02).decimal); // 0.1
console.log((0.008 + 0.002).decimal); // 0.01
console.log((0.0008 + 0.0002).decimal); // 0.001

Number.precision = 2;
console.log("Precision: 2");
console.log((0.8 + 0.2).decimal); // 1
console.log((0.08 + 0.02).decimal); // 0.1
console.log((0.008 + 0.002).decimal); // 0.01
console.log((0.0008 + 0.0002).decimal); // 0

Number.precision = 1;
console.log("Precision: 1");
console.log((0.8 + 0.2).decimal); // 1
console.log((0.08 + 0.02).decimal); // 0.1
console.log((0.008 + 0.002).decimal); // 0
console.log((0.0008 + 0.0002).decimal); // 0

Number.precision = 0;
console.log("Precision: 0");
console.log((0.8 + 0.2).decimal); // 1
console.log((0.08 + 0.02).decimal); // 0
console.log((0.008 + 0.002).decimal); // 0
console.log((0.0008 + 0.0002).decimal); // 0

干杯!


6
如果你选择给负评,至少提供一个原因。 - Bernesto
5
我没有点踩,但是虽然这段代码优美且可重用,但对 JavaScript 基本类型对象进行的猴子补丁很可能是不可预测的。其中一些问题似乎也会适用。 - BobRodes
尝试:((0.1*3)*1e14).decimal - trincot
@BobRodes 我完全同意这是一个猴子补丁, 并且由于相关原因它并不适合一些项目。但对于许多项目来说,这个解决方案是两害相权取其轻的理想选择。 - Bernesto
1
@Bernesto 这是两个罪恶中更大的一个,正是因为指定的原因。当页面上的任何脚本都是由另一个开发人员编写的,他认为使用像 decimalprecision 这样的常见属性名称来满足自己的需求是一个好主意时,问题就出现了。在模块化 JS 的时代,甚至考虑这种选项都很奇怪。decimal 可以是一个帮助函数,并在需要时导入,这种方法将是正确的,并且不会收到任何反对票。解决方案本身看起来非常可靠,除了它是浮点而不是定点精度,并且没有在更大的数字上进行测试。 - Estus Flask
显示剩余2条评论

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