Search code examples
rubyfloating-pointprecisionbigdecimal

Why does ruby BigDecimal show representation inaccuracy similar to float?


Certain floating point numbers have inherent inaccuracy from binary floating point representation:

> puts "%.50f" % (0.5)  # cleanly representable
0.50000000000000000000000000000000000000000000000000

> puts "%.50f" % (0.1)  # not cleanly representable
0.10000000000000000555111512312578270211815834045410

This is nothing new. But why does ruby's BigDecimal also show this behaviour?

> puts "%.50f" % ("0.1".to_d)
0.10000000000000000555111512312578270211815834045410

(I'm using the rails shorthand .to_d instead of BigDecimal.new for brevity only, this is not a rails specific question.)

Question: Why is "0.1".to_d still showing errors on the order of 10-17? I thought the purpose of BigDecimal was expressly to avoid inaccuracies like that?

At first I thought this was because I was converting an already inaccurate floating point 0.1 to BigDecimal, and BigDecimal was just losslessly representing the inaccuraccy. But I made sure I was using the string constructor (as in the snippet above), which should avoid the problem.


EDIT:

A bit more investigation shows that BigDecimal does still internally represent things cleanly. (Obvious, because otherwise this would be a huge bug in a very widely used system.) Here's an example with an operation that would still show error:

> puts "%.50f" % ("0.1".to_d * "10".to_d)
1.00000000000000000000000000000000000000000000000000

If the representation were lossy, that would show the same error as above, just shifted by an order of magnitude. What is going on here?


Solution

  • The %.50f specifier takes a floating point value, so that decimal value needs to be converted to floating point before it's rendered for display, and as such is subjected to the same floating point noise you get in ordinary floating point values.

    sprintf and friends, like the String#% method, do conversions automatically depending on the type specified in the placeholder.

    To suppress that you'd have to use the .to_s method on the BigDecimal number directly. It can take an optional format specifier if you want a certain number of places, and this can be chained in to a %s placeholder in your other string.