【问题标题】:Why is BigDecimal returning a weird value?为什么 BigDecimal 返回一个奇怪的值?
【发布时间】:2009-04-23 18:32:14
【问题描述】:

我正在编写处理货币、费用等的代码。我打算使用 BigDecimal 类进行数学和存储,但我们遇到了一些奇怪的问题。

此声明:

1876.8 == BigDecimal('1876.8')

返回 false。

如果我通过格式化字符串 "%.13f" 运行这些值,我会得到:

"%.20f" % 1876.8 => 1876.8000000000000
"%.20f" % BigDecimal('1876.8') => 1876.8000000000002

请注意 BigDecimal 中最后一个小数位的额外 2

我认为 BigDecimal 应该能够解决将实数直接存储在计算机的本机浮点中的不准确性问题。这是哪里来的2

【问题讨论】:

    标签: ruby floating-point bigdecimal


    【解决方案1】:

    它不会让您尽可能多地控制小数位数,但 BigDecimal 的传统格式机制似乎是:

    a.to_s('F')
    

    如果您需要更多控制权,请考虑使用 Money gem,假设您的域问题主要与货币有关。

    gem install money
    

    【讨论】:

      【解决方案2】:

      你是对的,BigDecimal 应该正确存储它,我最好的猜测是:

      • BigDecimal 正确存储值
      • 当传递给字符串格式化函数时,BigDecimal 被转换为较低精度的浮点值,从而创建 ...02。
      • 当直接与浮点数比较时,浮点数的小数位数远远超过您看到的 20 位(经典的浮点数无法进行比较行为)。

      无论哪种方式,将浮点数与 BigDecimal 进行比较都不太可能获得准确的结果。

      【讨论】:

      • 大卫,你最后一句警告不要比较浮点数和 BigDecimals 是非常好的建议。这是正确的结论,但不是最好的解释。 Float 类型在小数点后第 17 位后没有任何值,问题不在于精度不足。问题在于表示。 Float 类型使用 52 位的双精度尾数,但如果它有 5,000 位,它仍然不能准确地表示该常数,因此与十进制字符串进行比较时会出现同样的问题仍然存在。我试图在下面留下一个完全准确的答案。
      【解决方案3】:

      不要比较 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/252 ... 和即使这样也需要四舍五入。

      如果生成数字的过程、输入转换代码或其他任何相关内容有任何不完善之处(呵呵,bit, 抱歉),他们不会看完全相等。

      甚至可以提出比较应该总是失败的论点,因为没有 IEEE 格式的 FPU 甚至可以准确地表示该数字。他们真的是不平等的,即使他们看起来很像。在左侧,您的十进制字符串已转换为二进制字符串,并且大多数数字并未完全转换。右边,还是十进制字符串。

      所以不要将浮点数与 BigDecimal 混合,只需将一个 BigDecimal 与另一个 BigDecimal 进行比较。 (即使两个操作数 都是 浮点数,测试是否相等也需要非常小心或模糊测试。此外,不要相信每个格式化的数字:输出格式会将余数带离分数的右侧,所以你通常不会开始看到零,你只会看到垃圾值。)


      *问题:机器数是x/2n,但是十进制常量是x/(2n * 5 m)。您作为符号、指数和尾数的值是无限重复的0 10000001001 1101010100110011001100110011001100110011001100110011... 具有讽刺意味的是,FP 算术非常精确,当值没有分数时,等式比较工作得很好。

      【讨论】:

        【解决方案4】:

        正如大卫所说,BigDecimal 正确存储它

         p (BigDecimal('1876.8') * 100000000000000).to_i
        

        返回 187680000000000000

        所以,是的,字符串格式正在破坏它

        【讨论】:

        • 这是一个很好的答案尝试,是的,BigDecimal is 正确存储它,但真正的解释与String 格式无关。根本问题是左侧操作数是 Ruby Float(机器 IEEE 754 double),并且大多数小数十进制字符串没有 any 的精确表示 该格式的精度。大多数十进制有理数根本无法表示为x/2**n IEEE 754 二进制分数。
        【解决方案5】:

        如果您不需要小数美分,请考虑将货币存储和操作为整数,然后在显示时除以 100。我发现这比处理浮点存储和操作不可避免的精度问题更容易。

        【讨论】:

        • 我已经考虑过了。但是,我们将对支持其他货币感兴趣。这意味着我们不能依赖美分作为正确的单位。
        • 是的,如果你想做多种货币,你需要存储一个转换单位 - 多少个 X 到 Y - 然后在显示时使用它而不是 100。
        【解决方案6】:

        在 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 也不是正确的方法。

        【讨论】:

        • 我正在运行 Windows XP,ruby 1.8.6 (2007-09-24 patchlevel 111) [i386-mswin32]。顺序不会影响比较,但 BigDecimal('1876.8') == 1876.8.to_d 是真的。 (to_d 转换为 BigDecimal)。格式化是我找到值之间差异的唯一方法。我同意它不应该用于其他偏好。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2016-04-05
        • 1970-01-01
        • 1970-01-01
        • 2014-12-06
        • 2014-04-13
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多