这是一种可靠的方法来处理Javascript中浮点数相乘时的舍入错误吗

Would this be a reliable way to deal with rounding errors when multiplying floating point numbers in Javascript?

本文关键字:浮点数 Javascript 错误 舍入 处理 方法 一种      更新时间:2023-09-26

我正在将客户端小计计算添加到我的订单页面中,以便在用户进行选择时显示批量折扣。

我发现有些计算在这里或那里偏离了一美分。这不是什么大不了的事,除非总数与服务器端(PHP)计算的最终总数不匹配。

我知道在处理浮点数时,舍入误差是意料之中的结果。例如,149.95*0.15=22.492499999999996和149.95*0.30=44.98499999999999。前者随心所欲,后者则不然。

我搜索过这个话题,发现了各种各样的讨论,但都没有令人满意地解决这个问题。

我目前的计算如下:

discount = Math.round(price * factor * 100) / 100;

一个常见的建议是用美分工作,而不是用美元的零头。然而,这将需要我转换我的起始数字,四舍五入,相乘,四舍五入结果,然后将其转换回来。

本质上:

discount = Math.round(Math.round(price * 100) * Math.round(factor * 100) / 100) / 100;

我想在四舍五入之前把数字加0.0001。例如:

discount = Math.round(price * factor * 100 + 0.0001) / 100;

这适用于我尝试过的场景,但我想知道我的逻辑。加0.0001是否总是足够,而且永远不会太多,以强制获得所需的舍入结果?

注意:就我在这里的目的而言,我只关心每个价格的一次计算(因此不会复合错误),并且永远不会显示超过两位小数。

EDIT:例如,我想将149.95*0.30的结果四舍五入到小数点后两位,得到44.99。然而,我得到了44.98,因为实际结果是44.98499999999999,而不是44.985。/ 100没有引入错误。在那之前就已经发生了。

测试:

alert(149.95 * 0.30); // yields 44.98499999999999

因此:

alert(Math.round(149.95 * 0.30 * 100) / 100); // yields 44.98

44.98是考虑到乘法的实际结果而期望的,但不是期望的,因为它不是用户所期望的(并且与PHP结果不同)。

解决方案:我要将所有内容转换为整数来进行计算。正如公认的答案所指出的,我可以在一定程度上简化我最初的换算计算。我添加0.0001的想法只是一个肮脏的黑客。最好为工作使用合适的工具。

我不认为添加少量会对您有利,我想有些情况下会太多。此外,它还需要适当地记录,否则人们可能会认为它是不正确的。

以美分[…]为单位工作需要我转换起始数字,四舍五入,相乘,四舍五入结果,然后将其转换回:

discount = Math.round(Math.round(price * 100) * Math.round(factor * 100) / 100) / 100;

我认为只在事后取整也是可行的。然而,您应该首先将结果相乘,使有效数字是之前两个sig数字的总和,即在您的示例中2+2=4个小数位:

discount = Math.round(Math.round( (price * factor) * 10000) / 100) / 100;

在数字中添加少量数字不会非常准确。您可以尝试使用库来获得更好的结果:https://github.com/jtobey/javascript-bignum.

Bergi的回答显示了一个解决方案。这个答案从数学上证明了它是正确的。在这个过程中,它还建立了输入中可容忍的误差的一些界限。

你的问题是:

  • 您有一个浮点数x,它已经包含舍入错误。例如,它打算表示149.95,但实际上包含149.94999999999998863131622783839702606201171875
  • 您要将这个浮点数x乘以一个折扣值d
  • 你想知道乘法运算的结果,精确到一分钱,就好像使用了理想的数学一样,没有错误

假设我们再添加两个假设:

  • x总是代表一些精确的美分数。也就是说,它表示一个精确为百分之一的数字,例如149.95
  • x的误差很小,比如说小于0.00004
  • 折扣值d表示一个整数百分比(也就是说,也是一个精确的百分位数,例如.25表示25%),并且在区间[0%,100%]内
  • 误差是d很小,总是将小数点后两位的十进制数字正确转换为双精度(64位)二进制浮点的结果

考虑值x*d*10000。理想情况下,这将是一个整数,因为x和d在理想情况下都是.01的倍数,所以将x和d的理想乘积乘以10000会得到一个整数。由于x和d中的误差很小,因此将x*d*10000四舍五入为整数将产生理想整数。例如,不是理想的x和d,而是x和d加上小误差x+e0和d+e1,我们正在计算(x+eO)•(d+e1)•10000=(x•d+x•e1+d•e0+e0•e1)•10000。我们假设e1很小,因此主要误差为d•e0•10000。我们假设x中的误差e0小于0.00004,d最多为1(100%),因此d•e0•10000小于.4。这个误差,加上e1的微小误差,不足以将x*d*10000的四舍五入从理想整数更改为其他整数。(这是因为误差必须至少为.5才能改变应该是整数的结果的舍入方式。例如,3加上误差.5将舍入为4,但3加.44999则不会。)

因此,Math.round(x*d*10000)产生所需的整数。那么Math.round(x*d*10000)/100x*d*100的近似值,精确到远小于1美分,因此用Math.round(Math.round(x*d*10000)/100)对其进行四舍五入可以产生所需的美分数。最后,将其除以100(产生一个以百分之一表示的美元数,而不是以美分表示的整数)会产生一个新的舍入误差,但误差非常小,因此,当所得值正确地转换为以两位小数表示的小数时,会显示正确的值。(如果用这个值进行进一步的运算,可能不会保持正确。)

从上面可以看出,如果x中的误差增长到0.00005,则此计算可能失败。假设订单的价值可能会增长到100000美元。表示100000左右的值时的浮点错误最多为100000•2-53。如果有人订购了10万件有这个错误的商品(他们不能,因为这些商品的单个价格会小于10万美元,所以他们的错误会更小),然后将价格单独相加,进行10万(减去1)的添加,添加10万个新错误,那么我们有将近20万个误差,最多100000•2-53,所以总误差最多为2•105•105•2-53,大约为00000222。因此,此解决方案应该适用于正常订单。

请注意,如果折扣不是整数百分比,则需要重新考虑解决方案。例如,如果折扣表示为"三分之一"而不是33%,则x*d*10000不应为整数。