Dealing with Money in Rails
In process of determining how to handle fields such as price, in Rails, I’ve conducted some research. There seems to be a debate on whether to use decimal or integer as the price field. In case of where integer data types are used, the integer actually will store the price in cents.
One argument for using an integer data type instead of decimal, for the price field, is the idea that decimal can cause precision errors during calculations. However, I’ve heard that decimal data types are used successfully at Banks so I don’t believe this argument (yet).
The reason I’m leaning towards using decimal as the price is that probably 99% of the time, you’ll need to display the price in decimal format, not integer. You just have to remember to only do the math with BigDecimal ruby type. This translates to decimal data type in the database. BigDecimal stores the number and fractional part of the decimal as integers anyway, is what I hear. Can someone comment on this?
If you’d like to go with storing the price as a integer, then I recommend using the Money gem. If you’d like to go with storing the price as a decimal field, then I recommend using the money_column gem found at Money Column Gem.
My colleague and I discovered that it’s difficult to validate to ensure that only a numeric value can be entered. The reason: the Money gem converts any non-numeric value to 0 cents. I’ve been trying to validate the price as it’s entered by the user, before it’s converted to cents. I believe a virtual attributes (getter and setter) must be created for the price field and converted to cents, so that money :price declaration cannot be used. However, I haven’t yet been able to get this to work, mainly because I switched over to thinking that maybe I should be storing the price in decimal type in the database anyway. This is when I switched over to using the money_column gem instead.
With the money_column gem, I realized that non-numeric price values are converted to 0.0, instead of like nil and I still cannot get my rspec test to pass because yes 0.0 is a 0 or greater and also yes, it exists! I emailed the author of the money_column gem and he suggested I modify the code to be able to allow it to return nil for non-numeric values, as long as it doesn’t break existing code. I don’t want to invest too much time in doing this yet. If anyone has a suggestion, please feel free to comment. Apparently, the money_column gem has been extracted from Shopify’s code and has been used successfully for a while, so I believe both solutions are doable. However, I’d like to hear from developers who have tried both routes and hear pros and cons to both.
So I did a bunch of research on this and it sort of boils down to this.
BigDecimal in ruby maps to decimal datatype in the database as I mentioned above.
It can be used succesfully for price.
I think the main argument in using integer for price is to avoid potential bugs in the way the decimal price field is used. For example, someone can forget and try to convert the decimal price field to a float using the to_f method.
For example, try this in script/console: BigDecimal(“10.03”).to_f
It spits out 10.3 which is a rounding error.
So some solutions are to write tests! and to redefine or remove the to_f method in BigDecimal. Most of the bugs on the internet around this have to do with forgetting and trying to convert to a float.
class BigDecimal; undef :to_f; end;
OR
class BigDecimal; def to_f; self.to_s.to_f; end; end
OR
redefine the to_f method to have it throw an Exception whenever it is used in order to lessen chances of running into this issue.
2 Comments to “Dealing with Money in Rails”