如何处理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个回答

0

如果你不想每次都考虑调用函数,你可以创建一个处理转换的类。

class Decimal {
  constructor(value = 0, scale = 4) {
    this.intervalValue = value;
    this.scale = scale;
  }

  get value() {
    return this.intervalValue;
  }

  set value(value) {
    this.intervalValue = Decimal.toDecimal(value, this.scale);
  }

  static toDecimal(val, scale) {
    const factor = 10 ** scale;
    return Math.round(val * factor) / factor;
  }
}

使用方法:

const d = new Decimal(0, 4);
d.value = 0.1 + 0.2;              // 0.3
d.value = 0.3 - 0.2;              // 0.1
d.value = 0.1 + 0.2 - 0.3;        // 0
d.value = 5.551115123125783e-17;  // 0
d.value = 1 / 9;                  // 0.1111

当处理十进制数时,有一些需要注意的地方:

d.value = 1/3 + 1/3 + 1/3;   // 1
d.value -= 1/3;              // 0.6667
d.value -= 1/3;              // 0.3334
d.value -= 1/3;              // 0.0001

理想情况下,您应该使用高比例(如12),然后在需要呈现或存储它时将其转换下来。就我个人而言,我尝试创建一个UInt8Array并尝试创建精度值(类似于SQL Decimal类型),但由于Javascript不允许您重载运算符,因此不能使用基本数学运算符(+-/*)而是使用函数,例如add()substract()mult()。对于我的需求来说,这不值得。

但是,如果您确实需要那种精度水平并愿意忍受使用数学函数,则建议使用decimal.js库。


0

我喜欢使用修正因子的方法,这是我针对ES6和ES5标准做出的简化决策。与toFixed方法相比,它的优势在于,如果我们想要将数值四舍五入到百位,但结果数字是小数点后某个十分位数,则不会在数字末尾保留不必要的零。

ES6变种:

// .1 + .2
((a,b,crr) => (a*crr + b*crr)/crr)(.1,.2,100/*correction factor*/);//0.3
// .1 * .2
((a,b,crr) => a*crr*b/crr)(.1,.2,100);//0.02

ES5 变体:

// .1 + .2
(function(a,b,crr){ return (a*crr + b*crr)/crr; })(.1,.2,100/*correction factor*/);//0.3
// .1 * .2
(function(a,b,crr){ return a*crr*b/crr; })(.1,.2,100);//0.02

0

我通常会使用类似这样的方法。

function pf(n) {
    return Math.round(n * 1e15) / 1e15;
}

我不保证这是最优的方法,但我喜欢它的简单性。它将数字四舍五入到15位小数左右。虽然我没有见过它返回不准确的浮点数,但奇怪的是,当我在末尾使用* 1e-15时,它会这样做,但使用这种方法则不会。

这个解决方案可能更适合非精确数学用途,而不是精确数学用途,因为精度误差会破坏您的代码。


-1

我在这里有一个解决方法。例如,只用10E^x相乘不适用于1.1。

function sum(a,b){
    var tabA = (a + "").split(".");
    var tabB = (b + "").split(".");
    decA = tabA.length>1?tabA[1].length:0;
    decB = tabB.length>1?tabB[1].length:0;
    a = (tabA[0]+tabA[1])*1.0;
    b = (tabB[0]+tabB[1])*1.0;
    var diff = decA-decB;
    if(diff >0){
        //a has more decimals than b
        b=b*Math.pow(10,diff);
        return (a+b)/Math.pow(10,decA);
    }else if (diff<0){
        //a has more decimals than b
        a=a*Math.pow(10,-diff);
                return (a+b)/Math.pow(10,decB);
    }else{
        return (a+b)/Math.pow(10,decA);
    }       
}

有点吓人,但是能用 :)


-1

根据@SheetJs的答案,我整理了一下这个:

  getCorrectionFactor(numberToCheck: number): number {
    var correctionFactor: number = 1;
    
    if (!Number.isInteger(numberToCheck)) {
      while (!Number.isInteger(numberToCheck)) {
        correctionFactor *= 10;
        numberToCheck *= correctionFactor;
      }
    }

    return correctionFactor;
  }


-4

处理任意浮点数:

function shorten(num) {
    num += 0.000000001;// to deal with "12.03999999997" form
    num += '';
    return num.replace(/(\.\d*?)0{5,}\d+$/, '$1') * 1;
}

console.log(1.2+1.9===1.3+1.8);// false
console.log(shorten(1.2+1.9)===shorten(1.3+1.8));// true

-9
你可以使用正则表达式来检查数字是否以一长串0结尾,后面跟着一个小余数:
// using max number of 0s = 8, maximum remainder = 4 digits
x = 0.1048000000000051
parseFloat(x.toString().replace(/(\.[\d]+[1-9])0{8,}[1-9]{0,4}/, '$1'), 10)
// = 0.1048

6
哦天啊,请不要这样。与某个阈值足够接近的数字十进制表示集合不是一个正则语言。 - benesch
3
抱歉,但那真的很荒谬。至少试着理解为什么会出现错误,并正确地解决它。这是我第一次被踩票...已经4年了,我猜迟早会发生。 - jmc
16
你遇到了一个问题,决定使用正则表达式来解决它。现在你又多了一个问题。 - Qqwy
哈哈哈哈哈哈哈哈哈 - Andrew

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