为什么BigDecimal返回一个奇怪的值?
我正在编写处理货币,收费等的代码。我将使用BigDecimal类进行数学和存储,但我们遇到了一些奇怪的东西。
这个说法:
1876.8 == BigDecimal('1876.8')
返回false。
如果我通过格式化字符串"%.13f"
运行这些值,我得到:
"%.20f" % 1876.8 => 1876.8000000000000 "%.20f" % BigDecimal('1876.8') => 1876.8000000000002
请注意最后一个小数位的BigDecimal中的额外2
。
我认为BigDecimal应该能够抵消直接在计算机的本机浮点存储实数的不准确性。 这2
来自哪里?
你是对的,BigDecimal应该正确存储它,我最好的猜测是:
- BigDecimal正确存储值
- 传递给字符串格式化函数时,BigDecimal将被转换为较低精度的浮点值,从而创建… 02。
- 当直接与浮点数比较时,浮点数有一个额外的小数位,远远超过你看到的20(经典浮点数不能比较行为)。
无论哪种方式,您都不可能获得将浮点数与BigDecimal进行比较的准确结果。
它不会给你那么多的小数位数控制,但BigDecimal的传统格式机制似乎是:
a.to_s('F')
如果您需要更多控制,请考虑使用Money gem,假设您的域名问题主要与货币有关。
gem install money
不要将FPU十进制字符串分数与相等性进行比较
问题是浮点值或双精度值与包含分数的十进制常量的相等比较很少成功。
很少有十进制字符串分数在二进制FP表示中具有精确值,因此相等比较通常是注定要失败的。 *
为了回答您的确切问题, 2
来自于十进制字符串分数到Float
格式的稍有不同的转换。 由于无法准确表示分数,因此两次计算可能会在中间计算中考虑不同的精度,并最终将结果舍入为52位IEEE 754双精度尾数。 它几乎不重要,因为无论如何都没有确切的表示,但一个可能比另一个更错误。
特别是,您的1876.8
无法由FP对象精确表示,事实上,在0.01和0.99之间,只有0.25,0.50和0.75具有精确的二进制表示。 所有其他的,包括1876.8,永远重复,并四舍五入到52位。 这大约是BigDecimal存在的原因的一半。 (另一半原因是FP数据的固定精度:有时你需要更多。)
因此,在将实际机器值与十进制字符串常量进行比较时得到的结果取决于二进制分数中的每个位…低至1/2 52 …甚至需要舍入。
如果对于产生数字,输入转换代码或其他任何相关内容的过程有任何甚至一点点(hehe, bit, sorry)都不完美,那么它们看起来并不完全相同。
甚至可以说,比较应该始终失败,因为没有IEEE格式的FPU甚至可以准确地表示该数字。 它们真的不相等,即使它们看起来像它。 在左侧,您的十进制字符串已转换为二进制字符串,并且大多数数字不会完全转换。 在右边,它仍然是一个十进制字符串。
所以不要将浮点数与BigDecimal混合,只需将一个BigDecimal与另一个BigDecimal进行比较。 (即使两个操作数都是浮点数,对等式的测试也需要非常小心或模糊测试。另外,不要相信每个格式化的数字:输出格式化将在分数的右侧带有余数,因此通常不会启动看到零,你只会看到垃圾值。)
*问题:机器编号为x / 2 n ,但十进制常数为x /(2 n * 5 m )。 您作为符号,指数和尾数的值是无限重复的0 10000001001 1101010100110011001100110011001100110011001100110011...
具有讽刺意味的是,FP算术非常精确,当值没有分数时,相等比较可以很好地工作。
正如大卫所说,BigDecimal正确地存储它
p (BigDecimal('1876.8') * 100000000000000).to_i
返回187680000000000000
所以,是的,字符串格式化正在破坏它
如果您不需要小数美分,请考虑将货币存储和操作为整数,然后在显示时间除以100。 我发现这比处理浮点存储和操作的不可避免的精度问题更容易。
在Mac OS X上,我正在运行ruby 1.8.7 (2008-08-11 patchlevel 72) [i686-darwin9]
irb(main):004:0> 1876.8 == BigDecimal('1876.8') => true
但是,作为Ruby,我认为你应该考虑发送给对象的消息。 这回报给你的是什么:
BigDecimal('1876.8') == 1876.8
这两者不是等价的,如果你试图使用BigDecimal的能力来确定精确的十进制相等,它应该是消息的接收者询问相等性。
出于同样的原因,我不认为通过向格式字符串发送格式消息来格式化BigDecimal也是正确的方法。