2

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

我发现有些计算在这里或那里偏离了一美分。除了总数与最终计算的服务器端总数不匹配(在 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 是否总是足够,而且永远不会太多,以强制获得所需的舍入结果?

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

编辑:例如,我想将 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 的想法只是一个肮脏的黑客。最好为工作使用正确的工具。

4

3 回答 3

2

我认为添加少量不会对您有利,我想在某些情况下添加太多了。它还需要正确记录,否则可能会被视为不正确。

以美分工作 [...] 需要我转换我的起始数字,将它们四舍五入,将它们相乘,将结果四舍五入,然后将其转换回:

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;
于 2013-02-18T15:33:55.300 回答
1

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

于 2013-02-18T15:07:52.253 回答
1

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

你的问题是这样的:

  • 您有一个浮点数 x,它已经包含舍入误差。例如,它旨在表示 149.95,但实际上包含 149.94999999999998863131622783839702606201171875。
  • 您想将此浮点数 x 乘以折扣值 d。
  • 您想知道乘法到最接近的一分钱的结果,就像使用理想数学一样,没有错误。

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

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

考虑价值x*d*10000。理想情况下,这将是一个整数,因为 x 和 d 都是 0.01 的理想倍数,因此将 x 和 d 的理想乘积乘以 10,000 会产生一个整数。由于 x 和 d 的误差很小,因此四舍五入x*d*10000将产生理想的整数。例如,我们有 x 和 d 加上小误差 x+e0 和 d+e1,而不是理想的 x 和 d,我们正在计算 (x+e0)•(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 小于 0.4。这个错误,加上 e1 的微小错误,不足以改变四舍五入x*d*10000从理想整数到其他整数。(这是因为误差必须至少为 0.5 才能更改应该为整数的结果的舍入方式。例如,3 加上 0.5 的误差将舍入为 4,但 3 加上 0.49999 不会。)

因此,Math.round(x*d*10000)产生所需的整数。然后Math.round(x*d*10000)/100是一个近似值x*d*100,精确到远小于一美分,因此将其四舍五入,Math.round(Math.round(x*d*10000)/100)精确地产生所需的美分数。最后,将其除以 100(以产生整数的百分比,而不是美分的整数)会产生新的舍入误差,但误差非常小,以至于当结果值正确转换为带两位小数的十进制,显示正确的值。(如果使用此值执行进一步的算术运算,则可能不正确。)

从上面我们可以看出,如果 x 的误差增长到 0.00005,这个计算就会失败。假设一个订单的价值可能增长到 100,000 美元。表示 100,000 左右的值的浮点误差最多为 100,000•2 -53。如果有人订购了 10 万件出现此错误的商品(他们不能,因为这些商品的单个价格会小于 100,000 美元,因此它们的错误会更小),然后将价格单独加起来,执行 10 万(减一)添加十万个新错误,那么我们有将近二十万个错误,最多 100,000•2 -53,所以总错误最多为 2•10 5 •10 5 •2 -53,大约是 0.00000222。因此,此解决方案应该适用于正常订单。

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

于 2013-02-18T17:50:01.780 回答