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

601

来自浮点指南:

我该怎么做才能避免这个问题?

这取决于你要进行的计算类型。

  • 如果您需要确切精度的结果,特别是在处理货币时:请使用特殊的十进制数据类型。
  • 如果您只是不想看到那些多余的小数位:在显示时将结果四舍五入为固定数量的小数位即可。
  • 如果没有十进制数据类型可用,则可以使用整数进行计算,例如完全以分为单位进行货币计算。但是这会更麻烦,并且有一些缺点。

请注意,第一点仅适用于您确实需要精确的十进制行为。大多数人并不需要它,他们只是因为程序无法正确处理像1/10这样的数字而感到烦恼,却未意识到如果发生1/3的相同错误,他们甚至不会产生同样的反应。

如果第一点确实适用于您,请使用JavaScript的BigDecimalDecimalJS,它们实际上解决了问题,而不是提供一个不完美的解决方案。


14
我注意到您的BigDecimal链接失效了,当我正在寻找一个替代品时,我发现了一个叫做BigNumber的选择:http://jsfromhell.com/classes/bignumber。 - Jacksonkr
5
是的,但浮点数可以精确地表示长达尾数位长度的整数,并根据ECMA标准是64位浮点数,因此它可以精确地表示高达2^52的整数。 - Michael Borgwardt
8
@Karl:十进制小数1/10在二进制基数下无法表示为有限的二进制小数,而这正是JavaScript数字所采用的。因此,实际上确实存在完全相同的问题。 - Michael Borgwardt
29
今天我了解到,即使是 JavaScript 中的偶数也存在精度问题。例如,执行 console.log(9332654729891549) 实际上会输出 9332654729891548(即偏移了一个数字!) - mlathe
24
2⁵²=4,503,599,627,370,4962⁵³=9,007,199,254,740,992之间,可表示的数值恰好是整数。在下一个范围内,从2⁵³2⁵⁴,所有数值都乘以2,因此可表示的数值为偶数,等等。相反,在前一个范围内,从2⁵¹2⁵²,间距为0.5,等等。这是由于在64位浮点值中简单地增加/减少基数2的指数(这反过来解释了在值为01之间的情况下toPrecision()的罕见文档化的“意外”行为)。 - GitaarLAB
显示剩余17条评论

192

我喜欢Pedro Ladaria的解决方案,我使用类似的东西。

function strip(number) {
    return (parseFloat(number).toPrecision(12));
}

与Pedros的解决方案不同,这个方案将四舍五入为0.999 ...重复,并在最低有效数字上精确到加/减一。
注意:在处理32位或64位浮点数时,应使用toPrecision(7)和toPrecision(15)以获得最佳结果。有关详细信息,请参见此问题

34
为什么你选择了12? - qwertymk
33
toPrecision返回一个字符串而不是一个数字。这可能并不总是理想的。 - SStanley
12
parseFloat(1.005).toPrecision(3)翻译为:保留三位有效数字后转换成字符串,结果为1.00。 - Peter
5
@user2428118,我知道,我的意思是要展示四舍五入误差,结果是1.00而不是1.01。 - Peter
20
@user2428118所说的可能不够明显: (9.99*5).toPrecision(2) = 50而不是49.95,因为toPrecision计算整个数字而不只是小数。你可以使用toPrecision(4),但如果结果>100,则再次运气不佳,因为它只允许前三个数字和一个小数,从而移动点,使其几乎无法使用。最终我使用了toFixed(2)代替。 - ᴍᴇʜᴏᴠ
显示剩余7条评论

95

对于数学倾向的读者:http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html

推荐的方法是使用修正因子(乘以适当的10的幂次方,使算术发生在整数之间)。例如,在0.1 * 0.2的情况下,修正因子是10,并且您正在执行以下计算:

> var x = 0.1
> var y = 0.2
> var cf = 10
> x * y
0.020000000000000004
> (x * cf) * (y * cf) / (cf * cf)
0.02

一个(非常快速的)解决方案看起来像:

var _cf = (function() {
  function _shift(x) {
    var parts = x.toString().split('.');
    return (parts.length < 2) ? 1 : Math.pow(10, parts[1].length);
  }
  return function() { 
    return Array.prototype.reduce.call(arguments, function (prev, next) { return prev === undefined || next === undefined ? undefined : Math.max(prev, _shift (next)); }, -Infinity);
  };
})();

Math.a = function () {
  var f = _cf.apply(null, arguments); if(f === undefined) return undefined;
  function cb(x, y, i, o) { return x + f * y; }
  return Array.prototype.reduce.call(arguments, cb, 0) / f;
};

Math.s = function (l,r) { var f = _cf(l,r); return (l * f - r * f) / f; };

Math.m = function () {
  var f = _cf.apply(null, arguments);
  function cb(x, y, i, o) { return (x*f) * (y*f) / (f * f); }
  return Array.prototype.reduce.call(arguments, cb, 1);
};

Math.d = function (l,r) { var f = _cf(l,r); return (l * f) / (r * f); };

在这种情况下:

> Math.m(0.1, 0.2)
0.02

我强烈建议使用像SinfulJS这样经过测试的库。


2
我喜欢这个优雅的解决方案,但似乎并不完美:http://jsfiddle.net/Dm6F5/1/Math.a(76.65, 38.45) 返回115.10000000000002 - nicolallias
4
Math.m(10,2332226616) 给我的结果是“-19627406800”,这是一个负值...我希望它有一个上限 - 可能是导致这个问题的原因。请建议。 - Shiva Komuravelly
2
这看起来很不错,但似乎有一两个错误。 - MrYellow
13
他说这是一个非常快速的解决方案,修复破损但没有人提过。 - Cozzbie
4
请不要使用上述代码。如果它无法正常工作,那么它绝对不是一个“快速解决方案”。由于这是一个与数学相关的问题,因此需要准确性。 - Drenai
显示剩余3条评论

57

您只进行乘法计算吗? 如果是这样,那么您可以利用有关十进制算术的一个巧妙秘密。 也就是说,NumberOfDecimals(X)+ NumberOfDecimals(Y)= ExpectedNumberOfDecimals。 这意味着如果我们有0.123 * 0.12,则我们知道将会有5位小数,因为0.123有3位小数,而0.12有两位。 因此,如果JavaScript给我们一个像0.014760000002这样的数字,我们可以安全地四舍五入到第5位小数,而不用担心失去精度。


11
如何获取“精确”的小数位数。 - line-o
8
0.5乘以0.2等于0.10;你仍然可以截取小数点后两位(或更少)。但这个规律以外的任何数字都没有数学意义。 - Nate Zaugg
4
你有这个引用来源吗?同时请注意,对于除法来说并非如此。 - Griffin
2
Griffin:一份引用(更重要的是,易于理解的解释):https://www.mathsisfun.com/multiplying-decimals.html和http://www.math.com/school/subject1/lessons/S1U1L5DP.html。本质上,“因为当你(我添加:在纸上手动)没有小数点进行乘法运算时,你实际上是将小数点向右移以使其脱离干扰(我添加:对于*每个*数字)”,所以,x的#移位**加上**y的#移位。 - GitaarLAB
3
@NateZaugg 你不能截取溢出的小数位,你必须四舍五入到最接近的值,因为2090.5 * 8.61等于17999.205,但在浮点数中它是17999.204999999998。 - Lostfields

55

令人惊讶的是,尽管其他人有类似的变体,但此函数尚未被发布。它来自于 MDN Web Docs 的 Math.round()

function precisionRound(number, precision) {
    var factor = Math.pow(10, precision);
    return Math.round(number * factor) / factor;
}

console.log(precisionRound(1234.5678, 1));
// expected output: 1234.6

console.log(precisionRound(1234.5678, -1));
// expected output: 1230

var inp = document.querySelectorAll('input');
var btn = document.querySelector('button');

btn.onclick = function(){
  inp[2].value = precisionRound( parseFloat(inp[0].value) * parseFloat(inp[1].value) , 5 );
};

//MDN function
function precisionRound(number, precision) {
  var factor = Math.pow(10, precision);
  return Math.round(number * factor) / factor;
}
button{
display: block;
}
<input type='text' value='0.1'>
<input type='text' value='0.2'>
<button>Get Product</button>
<input type='text'>

更新:2019年8月20日

刚刚注意到这个错误。我相信这是由于使用Math.round()时的浮点数精度误差所致。

precisionRound(1.005, 2) // produces 1, incorrect, should be 1.01

这些条件是正确的:

precisionRound(0.005, 2) // produces 0.01
precisionRound(1.0005, 3) // produces 1.001
precisionRound(1234.5, 0) // produces 1235
precisionRound(1234.5, -1) // produces 1230

修复:

function precisionRoundMod(number, precision) {
  var factor = Math.pow(10, precision);
  var n = precision < 0 ? number : 0.01 / factor + number;
  return Math.round( n * factor) / factor;
}

当小数取整时,这只是在右侧添加一个数字。 MDN已更新Math.round()页面,所以可能有人能提供更好的解决方案。


错误的答案。10.2将始终返回10.19。https://jsbin.com/tozogiwide/edit?html,js,console,output - Žilvinas
@Žilvinas,您发布的JSBin链接未使用上述MDN函数。我认为您的评论是针对错误的人。 - HelloWorldPeace
Math.ceil不会考虑到那个0.01吗?(它将其转换为整数,然后再转换回浮点数,据我所知) - MrMesees
哇,谢谢,这对我所需的非常有效,使用约为12的精度和precisionRoundMod就可以解决我的用例问题! - mswieboda
你可以将Math.pow(10, precision)替换为10 ** precision - Arthur Ronconi
显示剩余3条评论

44

我发现BigNumber.js符合我的需求。

这是一个用于任意精度十进制和非十进制算术的JavaScript库。

它有良好的文档,作者也非常勤奋地回应反馈。

同一位作者还有另外两个类似的库:

Big.js

这是一个用于任意精度小数运算的小型、快速的JavaScript库。是bignumber.js的小妹妹。

以及Decimal.js

这是JavaScript的一个任意精度Decimal类型。

以下是使用BigNumber的一些代码示例:

$(function(){      
  var product = BigNumber(.1).times(.2);  
  $('#product').text(product);

  var sum = BigNumber(.1).plus(.2);  
  $('#sum').text(sum);
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>

<!-- 1.4.1 is not the current version, but works for this example. -->
<script src="http://cdn.bootcss.com/bignumber.js/1.4.1/bignumber.min.js"></script>

.1 &times; .2 = <span id="product"></span><br>
.1 &plus; .2 = <span id="sum"></span><br>


4
在我看来,使用图书馆绝对是最佳选择。 - Anthony
1
从这个链接 https://github.com/MikeMcl/big.js/issues/45 bignumber.js -> 金融 decimal.js -> 科学 big.js -> ??? - vee

31

您正在寻找JavaScript中的sprintf实现,以便以您所期望的格式写出带有小误差的浮点数(因为它们以二进制格式存储)。

尝试使用javascript-sprintf,您可以这样调用:

var yourString = sprintf("%.2f", yourNumber);

要将您的数字以两位小数形式打印出来,请使用 Number.toFixed()。如果您不想仅为浮点数四舍五入到给定精度而包含更多文件,则也可以使用它来进行显示。


4
我认为这是最干净的解决方案。除非你非常非常需要结果为0.02,否则这个小误差可以忽略不计。听起来重要的是你的数字被漂亮地显示,而不是具有任意精度。 - Long Ouyang
2
对于显示来说,这确实是最好的选择;对于复杂的计算,请查看Borgwardt的答案。 - Not Available
4
但是再说一遍,这将返回与yourNumber.toFixed(2)完全相同的字符串。 - Robert

25
var times = function (a, b) {
    return Math.round((a * b) * 100)/100;
};

---or---

var fpFix = function (n) {
    return Math.round(n * 100)/100;
};

fpFix(0.1*0.2); // -> 0.02

---也---

var fpArithmetic = function (op, x, y) {
    var n = {
            '*': x * y,
            '-': x - y,
            '+': x + y,
            '/': x / y
        }[op];        

    return Math.round(n * 100)/100;
};

--- as in ---

fpArithmetic('*', 0.1, 0.2);
// 0.02

fpArithmetic('+', 0.1, 0.2);
// 0.3

fpArithmetic('-', 0.1, 0.2);
// -0.1

fpArithmetic('/', 0.2, 0.1);
// 2

6
我认为这会导致相同的问题。你返回一个浮点数,因此返回值也很可能是“不正确的”。 - Gertjan
3
非常聪明且有用,点赞。 - Jonatas Walker

22
你可以使用parseFloat()toFixed()来解决小操作的问题:
a = 0.1;
b = 0.2;

a + b = 0.30000000000000004;

c = parseFloat((a+b).toFixed(2));

c = 0.3;

a = 0.3;
b = 0.2;

a - b = 0.09999999999999998;

c = parseFloat((a-b).toFixed(2));

c = 0.1;

也许这段代码需要一些注释,因为按照现在的写法有点难以理解。另外,.toFixed()只有在你在特定时间知道需要多少位小数时才能使用。有时候你可能想要保留一定数量的有效数字,而不知道小数位的确切数量。在这种情况下,应该使用.toPrecision()而不是.toFixed() - undefined

15

你需要确定你需要保留多少位小数 - 不能两全其美 :-)

每进行一次操作,数值误差都会累积,并且如果您不及时截断它,它将不断增长。数值库呈现看起来很干净的结果仅仅是在每一步截去最后两位,数值协处理器也有同样原因的“normal”和“full”长度。对于处理器来说,截断是很便宜的,但对于脚本中的您来说代价非常昂贵(使用乘法、除法和pov(...))。一个优秀的数学库应该提供floor(x,n)函数以帮助您进行截断。

所以,至少您应该使用pov(10,n)创建一个全局变量/常量 - 这意味着您已经决定了需要的精度 :-) 然后进行以下操作:

Math.floor(x*PREC_LIM)/PREC_LIM  // floor - you are cutting off, not rounding

如果您只是展示数据而不进行if-s比较,您也可以继续进行数学计算,并在最后截断-这样做可能更有效,即使使用.toFixed(...)。

如果您要进行if-s/comparisons比较且不想截断,则还需要一个小常量,通常称为eps,其小数位数比预期的最大误差高一位。假设您的截断是最后两个小数位-那么您的eps在倒数第3位上有1(第3位最不重要),您可以使用它来比较结果是否在预期值的eps范围内(0.02 -eps < 0.1*0.2 < 0.02 +eps)。


你也可以加上0.5来进行粗略的四舍五入:Math.floor(x*PREC_LIM + 0.5)/PREC_LIM - cmroanirgo
请注意,例如 Math.floor(-2.1) 的结果是 -3。因此,建议使用 Math[x<0?'ceil':'floor'](x*PREC_LIM)/PREC_LIM - MikeM
为什么使用 floor 而不是 round - Quinn Comendant

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