Ruby BigDecimal sanity check (floating point newb)

2019-01-11 04:13发布

问题:

Is my understanding correct that with Ruby BigDecimal types (even with varying precision and scale lengths) should calculate accurately or should I anticipate floating point shenanigans?

All my values within a Rails application are BigDecimal type and I'm seeing some errors (they do have different decimal lengths), hoping it's just my methods and not my object types.

回答1:

There are two common pitfalls when working with floating point arithmetic.

The first problem is that Ruby floating points have fixed precision. In practice this will either be 1) no problem for you or 2) disastrous, or 3) something in between. Consider the following:

# float
1.0e+25 - 9999999999999999900000000.0
#=> 0.0

# bigdecimal
BigDecimal("1.0e+25") - BigDecimal("9999999999999999900000000.0")
#=> 100000000

A precision difference of 100 million! Pretty serious, right?

Except the precision error is only about 0.000000000000001% of the original number. It really is up to you to decide if this is a problem or not. But the problem is removed by using BigDecimal because it has arbitrary precision. Your only limit is memory available to Ruby.

The second problem is that floating points cannot express all fractions accurately. In particular, they have problems with decimal fractions, because floats in Ruby (and most other languages) are binary floating points. For example, the decimal fraction 0.2 is an eternally-repeating binary fraction (0.001100110011...). This can never be stored accurately in a binary floating point, no matter what the precision is.

This can make a big difference when you're rounding numbers. Consider:

# float
(0.29 * 50).round
#=> 14  # not correct

# bigdecimal
(BigDecimal("0.29") * 50).round
#=> 15  # correct

A BigDecimal can describe decimal fractions precisely. However, there are fractions that cannot be described precisely with a decimal fraction either. For example 1/9 is an eternally-repeating decimal fraction (0.1111111111111...).

Again, this will bite you when you round a number. Consider:

# bigdecimal
(BigDecimal("1") / 9 * 9 / 2).round
#=> 0  # not correct

In this case, using decimal floating points will still give a rounding error.

Some conclusions:

  • Decimal floats are awesome if you do calculations with decimal fractions (money, for example).
  • Ruby's BigDecimal also works well if you need arbitrary precision floating points, and don't really care if they are decimal or binary floating points.
  • If you work with (scientific) data, you're typically dealing with fixed precision numbers; Ruby's built-in floats will probably suffice.
  • You can never expect arithmetic with any kind of floating point to be precise in all situations.