这是一种可靠的方法来处理Javascript中浮点数相乘时的舍入错误吗
Would this be a reliable way to deal with rounding errors when multiplying floating point numbers in Javascript?
我正在将客户端小计计算添加到我的订单页面中,以便在用户进行选择时显示批量折扣。
我发现有些计算在这里或那里偏离了一美分。这不是什么大不了的事,除非总数与服务器端(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)/100
是x*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
不应为整数。
- 使用javascript从字符串中提取浮点数
- 如何使用Javascript使浮点数的精度相同
- 浮点数的总和在 JavaScript 中没有给出小数
- 如何使用 javascript 将浮点数格式化为十进制数(7)
- javascript正则表达式浮点数,以字符串结尾
- 如何通过 javascript 在浮点数中保留小数点数后的 2 位数字
- 在Javascript中,大浮点数的小数位数有限制吗
- 如何在Javascript中将浮点数转换为字符串数组
- 如何在javascript中将分数转换为浮点数
- 用 JavaScript 加一堆浮点数,总和上的误差界限是多少
- 如何在 Javascript 中定义浮点数
- JavaScript正则表达式 - 用四舍五入的数字替换字符串中的所有浮点数
- 将浮点数向上舍入到 javascript 中的下一个整数
- 读取 JavaScript 中包含 32 位浮点数的二进制文件
- javascript:将浮点数四舍五入到最接近的 .25(或其他什么..)
- 比较 JavaScript 中的浮点数会产生意想不到的结果
- 在 javascript 中将浮点数转换为近似分数
- JavaScript 中的正则表达式用于存储在数组中的负浮点数结果
- 使用 Javascript / jQuery 解析 Google 地图的浮点数
- RegEx表示,对于浮点数(JavaScript),我的所有输入都为假