避免JavaScript中奇怪的小数计算问题

63

刚看了MDN上的一篇文章,其中提到由于一切均为“双精度64位格式IEEE 754值”,JS处理数字的一个特点是当你执行类似.2 + .1这样的操作时,你得到的结果是0.30000000000000004(这是该文章中的说法,但我在Firefox中得到的结果是0.29999999999999993)。因此:

(.2 + .1) * 10 == 3

评估结果为false

这似乎会引起很大问题。那么有什么方法可以避免由于JS中不精确的十进制计算而导致的错误?

我注意到如果你执行1.2 + 1.1,你会得到正确的答案。所以是否应该避免涉及小于1的值的任何数学运算?因为那似乎非常不切实际。在JS中进行数学运算是否还存在其他危险?

编辑:
我理解许多十进制分数无法存储为二进制,但我遇到的大多数其他语言处理误差的方式(如JS处理大于1的数字)似乎更直观,所以我不太习惯这种情况,这就是为什么我想看看其他程序员如何处理这些计算的原因。


7
在任何语言中,浮点数几乎永远无法与其他数字完全相等。 - deceze
四舍五入是你的好朋友。 - mellamokb
@deceze:PHP、MySQL、Perl、VB以及我过去使用过的大多数编程语言在这些类型的计算方面从未出现过任何问题。 - Lèse majesté
7
@Lèse $ php -r 'var_dump((0.1 + 0.2) * 10 == 3);'bool(false) :) 这段代码的含义是:将0.1与0.2相加,然后将结果乘以10,最后检查结果是否等于3。通过运行以上代码,我们可以看到输出为布尔值false,说明该表达式不成立。 - deceze
2
@mellamokb 嗯,我在四舍五入方面遇到了问题。0.5 应该被四舍五入为 1,但我得到的是 0.4999......8,它被四舍五入为 0。 - Sagar V
显示剩余2条评论
8个回答

54

1.2 + 1.1 可能没问题,但是 0.2 + 0.1 可能会有问题。

这是当今几乎所有编程语言都存在的问题。问题在于十分之一无法被准确地表示为二进制小数,就像三分之一无法表示为十进制小数一样。

解决方法包括仅保留所需小数位数进行四舍五入,并使用精确的字符串进行操作:

(0.2 + 0.1).toFixed(4) === 0.3.toFixed(4) // true

或者您可以在此之后将其转换为数字:

+(0.2 + 0.1).toFixed(4) === 0.3 // true

或者使用 Math.round:
Math.round(0.2 * X + 0.1 * X) / X === 0.3 // true

其中X是10的幂,例如100或10000——取决于所需的精度。

或者在计算货币时可以使用美分而不是美元:

cents = 1499; // $14.99

那样你只需使用整数,完全不需要担心小数和二进制分数的问题。
2017更新
在JavaScript中表示数字的情况可能比以前更加复杂。曾经我们只有一种数值类型:
64位浮点数(IEEE 754双精度浮点数 - 参见:ECMA-262第5.1版,第8.5节ECMA-262第6.0版,第6.1.6节
这已经不再是现实 - 当前JavaScript中有更多的数值类型,而且还会有更多的类型,包括向ECMAScript添加任意精度整数的建议,希望随之而来的是任意精度小数 - 有关详细信息,请参见此答案: JavaScript中浮点数和整数的区别? 另一个相关答案,其中包含一些处理计算的示例:

3
toFixed 方法返回一个数值的字符串表示,不确定这是否符合您的需求。 - Shane Stillwell

32
在这种情况下,您通常会更倾向于使用 epsilon 估计。
类似以下的东西(伪代码)
if (abs(((.2 + .1) * 10) - 3) > epsilon)

当epsilon为0.00000001或任何所需精度时使用。

请阅读比较浮点数


我认为你的 ) 放错了位置,除此之外完全同意。 - deceze
未来的访客请注意,相关文章现已移至https://randomascii.wordpress.com/category/floating-point。 - cautionbug
6
Chrome 和 Firefox 现在都有 Number.EPSILON - MDN 参考链接:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/EPSILON x = 0.2; y = 0.3; z = 0.1; equal = (Math.abs(x - y + z) < Number.EPSILON); - jeznag

9
(Math.floor(( 0.1+0.2 )*1000))/1000

这将降低浮点数的精度,但如果您不使用非常小的值,则可以解决问题。 例如:
.1+.2 =
0.30000000000000004

在进行建议的操作后,您将获得0.3。但是任何介于以下值之间的数值:

0.30000000000000000
0.30000000000000999

也将被视为0.3。


6

有一些库致力于解决这个问题,例如decimal.js,但如果您不想使用其中之一(或者由于某些原因无法使用,比如在GTM变量中工作),那么您可以使用我编写的此小函数:

用法:

var a = 194.1193;
var b = 159;
a - b; // returns 35.11930000000001
doDecimalSafeMath(a, '-', b); // returns 35.1193

这是函数:

function doDecimalSafeMath(a, operation, b, precision) {
    function decimalLength(numStr) {
        var pieces = numStr.toString().split(".");
        if(!pieces[1]) return 0;
        return pieces[1].length;
    }

    // Figure out what we need to multiply by to make everything a whole number
    precision = precision || Math.pow(10, Math.max(decimalLength(a), decimalLength(b)));

    a = a*precision;
    b = b*precision;

    // Figure out which operation to perform.
    var operator;
    switch(operation.toLowerCase()) {
        case '-':
            operator = function(a,b) { return a - b; }
        break;
        case '+':
            operator = function(a,b) { return a + b; }
        break;
        case '*':
        case 'x':
            precision = precision*precision;
            operator = function(a,b) { return a * b; }
        break;
        case '÷':
        case '/':
            precision = 1;
            operator = function(a,b) { return a / b; }
        break;

        // Let us pass in a function to perform other operations.
        default:
            operator = operation;
    }

    var result = operator(a,b);

    // Remove our multiplier to put the decimal back.
    return result/precision;
}

6

理解浮点算法中的舍入误差可不是件简单的事情!基本上,计算过程中会假设有无穷精度位数可用。然后根据相关IEEE规范制定的规则进行四舍五入。

这种四舍五入可能会产生一些奇怪的答案:

Math.floor(Math.log(1000000000) / Math.LN10) == 8 // true

这个误差达到了一个数量级。这是一些四舍五入误差!

对于任何浮点数架构,都存在一个表示可区分数字之间最小间隔的数字。它被称为EPSILON。

在不久的将来,它将成为EcmaScript标准的一部分。与此同时,您可以按以下方式计算它:

function epsilon() {
    if ("EPSILON" in Number) {
        return Number.EPSILON;
    }
    var eps = 1.0; 
    // Halve epsilon until we can no longer distinguish
    // 1 + (eps / 2) from 1
    do {
        eps /= 2.0;
    }
    while (1.0 + (eps / 2.0) != 1.0);
    return eps;
}

您可以像这样使用它:

然后您可以使用它,就像这样:

function numericallyEquivalent(n, m) {
    var delta = Math.abs(n - m);
    return (delta < epsilon());
}

或者,由于舍入误差可能会累积惊人的数量,您可能希望使用 delta / 2delta * delta 而不是 delta


感谢提供优秀的epsilon函数。我找到了一个有用的相等性测试并将其放在这里:https://dev59.com/xXrZa4cB1Zd3GeqP8_CH#20957752。 - Timo Kähkönen

5

您需要一些错误控制。

创建一个小的双重比较方法:

int CompareDouble(Double a,Double b) {
    Double eplsilon = 0.00000001; //maximum error allowed

    if ((a < b + epsilon) && (a > b - epsilon)) {
        return 0;
    }
    else if (a < b + epsilon)
        return -1;
    }
    else return 1;
}

0

当我在处理货币值时发现了这个问题,我只需将值转换为分,就找到了解决方案,因此我采取了以下措施:

result = ((value1*100) + (value2*100))/100;

在处理货币价值时,我们只有两个小数位,这就是为什么我要乘以100并除以100的原因。 如果您要使用更多的小数位,您将不得不将小数位数乘以该数字,例如:

  • .0 -> 10
  • .00 -> 100
  • .000 -> 1000
  • .0000 -> 10000 ...

通过这种方式,您将始终避免使用小数值。


0

通过乘法将小数转换为整数,最后再通过相同的数字除以结果进行转换。

以您的情况为例:

(0.2 * 100 + 0.1 * 100) / 100 * 10 === 3


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