Rails, Money, Float, Decimal

November 17th, 2009

The problems of using floats to represent monetary values have been well discussed elsewhere. So it sufficeth me to say, “do not ever do it”. Now if you are confronted with this haunting bit of code (written by someone else, of course), and want to correct the elusive $35.000000000000000001 you are charging people then I have good news! It can be done, but the road is not paved. These are the four biggest bumps I’ve run into. They might apply to other styles of AR stores, but these tips are from my experiences with MySQL.

The problems of using floats to represent monetary values have been well discussed elsewhere. So it sufficeth me to say, “do not ever do it”. Now if you are confronted with this haunting bit of code (written by someone else, of course), and want to correct the elusive $35.000000000000000001 you are charging people then I have good news! It can be done, but the road is not paved. These are the four biggest bumps I’ve run into. They might apply to other styles of AR stores, but these tips are from my experiences with MySQL.

Problem #1.

MySQL: Decimal columns with default values are marshalled into schema.rb and fixtures as nasty Ruby object dumps, which start with the # character, which is a comment in ruby, which breaks the entire file, and thereby breaks any part of the app that reads those files.

Solution to Problem #1:

A. In config/environment.rb:
  config.active_record.schema_format = :sql

B. Delete db/schema.rb

C. In a terminal:
rake db:structure:dump
rake db:test:clone_structure

D. Now you should be able to run your tests again.

Problem #2.

MySQL: Writing the migration, I thought this would be fine to convert a column that had been previously defined with a comment and a default:
    change_column :auction_payments, :transaction_amount, :decimal, :precision => 9, :scale => 2
But when I ran it the :default setting was lost, and the :comment was lost. I imagine other settings I might have had would’ve been lost as well. The DATA in the column was fine, but the column itself had lost some settings (which caused specs to fail, because those defaults are there for a reason!)

Solution to Problem #2:

A. Make sure you replay all the current column settings that still apply to the change_column method. Yes, you do have to restate all those defaults, and comments, etc.

Problem #3:

Rails: When the value in a decimal column comes back to rails as a 0 instead of typecasting it as 0:BigDecimal it gets typecast as 0:fixnum. If your code involving money does division then you will end up with numbers that need to be rounded with .round(2). If that code meets one of these 0:Fixnums, there will be a nasty explosion, because FixNum does not have a round method.

Solution to Problem #3:

(Please let me know if you have a better solution):

A. Create a new initializer file in config/intializers/fixnum_rounding_fix.rb and put this in it:
# When Our DECIMAL database columns have a value of 0 rails is casting the value to Fixnum, instead of a BigDecimal.
# This causes the round() method we call on instances of BigDecimal to fail.
# Fixnum does not have a round method with an argument, so this should not pose a problem
# Essentially if we are calling round on a fixnum, it is because we expected a BigDecimal, and it needs to be a BigDecimal.
Fixnum.class_eval do
  def round(*args)
    BigDecimal("#{self}")
  end
end

Problem #4:

All the old code that used the old Float columns and had been hacked somehow to work almost reliably with forced rounding (faked precision) is now broken!

Solution to Problem #4:

A. Search the project for usages of the custom hackety-hack rounding method that was being used, in my case it was my_number.round_with_precision(2), and rethink the math. Usually I was able to just replace my_number.round(2).

B. Run the specs (you do have tests, right?) and make sure they all still pass. If they do not, then perhaps they’re not testing what they need to be testing?

Sorry, comments are closed for this article.