假设我在ieee兼容的语言(C, Java等)中使用默认的四舍五入到半偶数规则将数字1.20515
舍入到小数点后4位,结果将是"1.2051",这不是偶数。
我认为这是由于1.20515
在二进制存储时稍微偏向1.2051
,所以在二进制空间中甚至没有平局。
但是,如果输入的1.20515
小数是精确的,那么这种四舍五入实际上不是错误的吗?
我真正想知道的是,如果我不想使用精确的十进制算术(例如Java的BigDecimal
),这些二进制四舍五入规则会在工作流程中引入偏差:字符串中的精确十进制(6 d.p. max) ->解析到IEEE双->使用IEEE规则四舍五入到4 d.p.
"精确十进制"输入是由Java使用直接来自数据库的BigDecimal
或String
生成的。不幸的是,格式化必须在JavaScript中完成,而JavaScript缺乏对适当舍入的大量支持(我正在考虑实现一些)。
你是正确的:1.20515不能用IEEE754 binary64表示,所以十进制->二进制转换将四舍五入到最接近的值1.20514999999999435118525070720352232456207275390625。
IEEE754标准实际上没有任何关于将二进制值舍入为非整数小数的规定(舍入到最接近的整数不会受到此问题的影响),因此任何此类功能都取决于语言标准(如果它选择定义它)。JavaScript toFixed
清楚地将其定义为精确的数学值(即1.2051)。
(更新:实际上,IEEE754标准确实规定了FP ->字符串转换应该如何执行,参见下面Stephen Canon的评论)。
但是,如果你想在整个管道中正确舍入,你可以使用
function roundeven(x) {
return Math.sign(x)*((Math.abs(x) + 4.503599627370496e15) - 4.503599627370496e15);
}
roundeven(Math.round(parseFloat(s)*1e6)/1e2)/1e4;
只要s
小于16位(即绝对值小于109),
为什么会这样?
-
Math.round(parseFloat(s)*1e6)
是精确的:这是因为binary64可以正确地往返15个十进制数字,这基本上是通过缩放到整数值来做同样的事情。 - 除
1e2
将涉及一些四舍五入(因为不是所有的值都可以精确表示),但重要的是,它可以(i)精确地表示小数一半的值,并且(ii)不会将任何其他值四舍五入为小数一半(因为我们仍然有少于16个十进制数字)。 -
roundeven
实现四舍五入到最接近的整数。此实现对上述范围内的任何值都有效。 - 最后的除法将再次涉及一些四舍五入,但值将最接近正确的十进制值,因此转换回字符串(当需要时,通过
_.toFixed(2)
)将给出正确的结果。
(感谢bill.cn和Mark Dickinson的更正)