When (ruby) floating just isn't good enough
Nov 28, 2008
Hopefully we all know that when talking about numbers "float" means "floating point" and as such the number isn't entirely precise, it's just an approximation. Most of the time it's a good enough approximation to serve our needs, but when it comes to dealing with money it's best to play it safe and have the absolute precision we want. Otherwise all those .000001 of a cents can add up to a large sum.
Working with fixed point arithmetic
To get around the short-comings of using floating points, a fairly common and trivial task is simply multiply it to the number of significant digits we care about and then treat it as an integer. So for dealing with money, we would multiply the value $120.19 by 100 to end up with 12019 cents. Any arithmetic we do, we do on the expanded cent value and we store that in the database. If we want to display a nicely format dollar value to the end user, we do that after we pull it back out from the database.
Losing money with Ruby
As I said, all this is rather trivial and many of you have probably had to do it in the past. So here is an excercise for the reader to follow along with at home. What do you think the following would output?
If you answered 0.29 pat yourself on the back. Let's take it a step further:
"0.29".to_f * 100
Who answered 29.0? Congratulations. Okay, and now to take home the full showcase and all the cash:
("0.29".to_f * 100).to_i
Do we hear a 29 out there? Hooray! You're not alone, but you're wrong. Fire up your nearest ruby console and give it a shot, you'll actually get _28_ as the output. If you've made the same assumption yourself in the past, it'd be worth going back and checking your code to make sure it works. Likewise if you're using a 3rd party library it's worth double-checking they've done the correct implementation.
Doing ruby floating points and money properly
So how do you get around this problem? Well from reading the ruby docs on float I'd still expect .toi_ to work, but clearly it's not actually truncating the value as documented. Instead, you have to call round():
("0.29".to_f * 100).round
Alternatively, you can use the money gem as it does it properly.