dollars_and_cents: a Rails plugin
Update: Use of this plugin is deprecated. If you’re using Rails 1.2 or higher, use the :decimal field type for your monetary values. This will provide you with fixed-point math on both the database and Rails sides of your application.
(Yes, another plugin. I’m full of them.)
One of the big problems with a database-agnostic framework like ActiveRecord is that it doesn’t have a decent data type for money. Yes, you can use a FLOAT, but then you end up charging someone $12.3000000000000001, which is just awkward. You can hard-code a database-specific type, but then your code takes a dive when you hit the bigtime and get bounced up to a DB2 backend or some nonsense. Or you could store prices as an integer value of the smallest unit possible–the cent–and do the math in the model, but then you have to write an attribute wrapper, which is boring and not very DRY. Or…
Aw snap, Coda’s hauled off and done it for you! Oh happy people! Oh frabjous day!
Installamacation
Love SVN?
./script/plugin install -x http://svn.codahale.com/dollars_and_cents/
Ambivalent about it?
./script/plugin install http://svn.codahale.com/dollars_and_cents/
Usamization
dollars_and_cents is an easy plugin to use.
Name your fields the right thing
All prices should end with _in_cents. That’s the convention I made up, and you all get to deal with it. I think it’s rational, but I think many things are rational with which other people violently disagree (e.g., my dad and I on the direction in which twisty ties should be fastened–he says clockwise, I say whatever).
In a migration or somesuch:
create_table :buyable_things do |t|
t.column :name, :string
t.column :price_in_cents, :integer
end
Profit
Once installed, dollars_and_cents adds a Float attribute price to the model BuyableThing. Use as you would any other attribute:
@buyable_thing = BuyableThing.find(params[:id])
old_price = @buyable_thing.price # => 19.99
@buyable_thing.price = 23.99
new_price = @buyable_thing.price # => 23.99
And now 2399 is stored in the database, in a nice, cross-platform format: the humble int.
(If you had a database field called tulpa_death_chant_cost_in_cents, it would add an attribute called tulpa_death_chant_cost. It’s not limited to the phrase “price”. FYI.)
All yours, under the MIT license © 2006. Woo hah.
June 1st, 2006 at 12:55pm
Excellent man! I love this thing.
June 6th, 2006 at 4:30pm
Hmm, plugin doesn’t format prices as I’d expected. For a value in cent’s of 5300 I’d expect the output to be “53.00″ not “53.0″.
However as I type this I realize that the plugin makes no pretense about formating the output. So take this comment as just an observation rather than a request to modify the plugin.
June 6th, 2006 at 5:07pm
Sanjeev–the reason it does so is because that\’s the way Ruby prints out Numeric objects.
If you\’d like to print out numbers as currency, try this:
June 12th, 2006 at 8:00am
[...] dollars_and_cents erleichtert das Speichern von Währungs-Werten in einer Rails-Anwendung, in dem es für Attribute mit Namen wie foo_as_cents, die die Cent-Werte (als Rundungs-sichere Integers) speichern, automatisch Float-Attribute namens foo bereit stellen. Nett. [...]
June 13th, 2006 at 2:31am
Perfect timing, I was pretty much about to implement something like this myself. Thanks Coda.
June 15th, 2006 at 9:11am
xal has also written the ‘Money’ gem, which takes care of currency as well as the float-to-integer problem:
http://dist.leetsoft.com/api/money/
June 15th, 2006 at 11:10am
Awesome! I prefer this to the Money gem, especially appreaciate the svn integration
July 5th, 2006 at 1:07am
I’m getting the following error:
NoMethodError in Admin/activitiesController#update
undefined method `price_before_type_cast’ for #
any idea about what could be the problem? I just followed the easy instructions and my models automatically displayed the prices as floats, but when I try to save I get that error.
Thanks in advance
Maykel
July 6th, 2006 at 10:18pm
Maykel, you are probably getting that error from a validation. I was getting the same error. I changed it to validates_presence_of :price_in_cents
July 6th, 2006 at 10:26pm
Thanks, Scott!
I’ve been Very Busy recently, Maykel, but I’ll see if I can make this act a bit more transparently, especially as Scott’s solution will display stuff like “Price in cents cannot be blank” for error messages, which may confuse the hell out of your users. It should just be a matter of also responding to
price_before_type_cast.July 11th, 2006 at 6:30pm
I am getting rounding errors.
If you input the following numbers:
18.01 you get into the database field 18.02
18.10 you get into the database field 18.11
18.17 you get into the database field 18.18
18.19 you get into the database field 18.2
19.94 you get into the database field 19.95
18.92 you get into the database field 18.93
my database is called products and the field is called price_in_cents
i am accessing via a _form:
(RHTML eaten by WordPress)
Why?
July 11th, 2006 at 6:34pm
Victor–I’m not sure. What version of Ruby are you running, and what database platform?
July 11th, 2006 at 7:02pm
Ruby version 1.8.4 (powerpc-darwin8.6.0)
RubyGems version 0.8.11
Rails version 1.1.4
Active Record version 1.14.3
Action Pack version 1.12.3
Action Web Service version 1.1.4
Action Mailer version 1.2.3
Active Support version 1.3.1
Environment development
Database adapter mysql version: 4.1.12-standard
July 11th, 2006 at 7:10pm
Sorry, Victor, that was a stupid bug of mine. Fixed now.
July 11th, 2006 at 7:23pm
Coda;
Thanks a lot for the fix !!!!
After installing Revision 10 of the plugin
AND restarting the server in this case lighttpd
everything works fine.
July 28th, 2006 at 10:32am
guess I’m mising something. tested out the plugin in console and it keeps returning 0.0 for my max_bid price:
>> k = KeywordLink.find(2)
#>, @attributes={ “max_bid_in_cents”=>”2323″, “id”=>”2″}, @new_record_before_save=nil>
>> k.max_bid
=> 0.0
>> k.max_bid_in_cents
=> 2323
>> k.max_bid = 9.99
=> 9.99
>> k.save
=> true
>> k
=> #>, @attributes={”max_bid_in_cents”=>999,”id”=>”2″}, @new_record_before_save=nil>
>> k.max_bid
=> 0.0
ideas? my db column was created by a migration. it’s a nullable INT(11) named max_bid_in_cents (as you can see by the dump above). running linux, ruby 1.8.4, rails 1.1.4 and the non-svn version of dollars_and_cents
July 29th, 2006 at 7:25pm
Ed, I have no idea. I can’t duplicate that problem in any of my applications, and it’s not something I’ve heard about before. I can’t think of any reason why it would do that–maybe a conflict with another plugin?
Let me know if you find anything else.
July 31st, 2006 at 8:20am
you’re right about the conflicting plugin. i had given up on your plugin and was setting up to use the Money gem when, in my model, i noticed i had a directive for another plugin (default_values) set for the max_bid field. it was setting the default to 0.0. apparently it did that after dollars_and_cents so all my values were 0.0. i removed that plugin and your’s now works.
thanks,
ed
August 4th, 2006 at 8:55pm
Hi from New Zealand!
I’m getting a “undefined method `flat_rate’ for #” error when I try to call it in a view.
I’ve got a model “AssetRate” with a flat_rate_in_cents column in the database.
When I play with the object in consoler, everything works fine (I can call/set asset_rate.flat_rate no probs), but no within a rhtml view.
Any ideas?
Thanks
Peter
August 4th, 2006 at 9:01pm
The code snippet from my RHTML that should have been with my previous post/question (i’ve taken out the RHTML tags so it doesn’t get swallowed by WordPress) :
for asset_rate in @asset_rates
#this works
=asset_rate.flat_rate_in_cents
#this fails
=asset_rate.flat_rate
end
August 4th, 2006 at 9:01pm
Peter–Uh, none of the code actually made it into your comment–comments are filtered for HTML.
Honestly, I don’t have a clue as to why your records wouldn’t work with your views. If it works anywhere, it should work everywhere.
August 4th, 2006 at 9:10pm
August 4th, 2006 at 9:25pm
Got it sorted… I needed to restart my webserver (mongrel) for it to work in the views… since consoler is direct.
Doh!
Cheers
Peter
August 4th, 2006 at 10:05pm
Hi Again Coda.
In terms of usage, to avoid rounding errors, should I perform all calculations (eg x 27.3) on the price_in_cents and then divide by 100 before putting it back inot the price field?
And if I just want to display (but not persist) a temporary value (price * 1.3314) should I do price_in_cents * 1.3314 / 100 or can I safely do price * 1.3314?
Thanks.
August 4th, 2006 at 10:10pm
Honestly, you’re probably better off running Edge Rails and using the new
:decimaltype withBigDecimals.But yeah, do all your math in cents and set
property_in_centsto the results. If you have a float value from a user, you can convert it to cents using the following:That shouldn’t cause too much ruckus.
For anything display-based, feel free to use the float methods.
August 13th, 2006 at 7:38pm
This is a time saver! Thanks!
I found the “_in_cents” suffix a bit limiting though and decided to rename it to “_in_hundredths” by changing the CENTS_SUFFIX constant in my application.
August 18th, 2006 at 10:57pm
Hi Coda;
How should I define a tax and how should I use it?
I would like to define it in my model as a constant and then use it
with the plugin.
Say I need to define a sales tax that is 15%
I was thinking on something like this on the model:
TAX = [ [:iva => '15'] ].freeze # freeze to make this array constant
and if i have in the DB something like this:
create_table "prices", :force => true do |t|
t.column "price_in_cents", :integer
t.column "iva_in_cents", :integer
t.column "total_in_cents", :integer
end
then if i had this values:
price_in_cents = 10000 # $100.00
TAX = 15 #15%
how would i get the total price?
I don't know how to go about so as to not mix a floating point number
August 28th, 2006 at 7:04am
After I installed this plugin I started getting problems in my testing code. I think my fixtures aren’t setup properly in the database and an exception is being thrown. Notice how the teardown method is blowing up. I think the setup method is not getting to start a transaction because of an exception, and the teardown is just blindly calling it. But, I can’t find any evidence of the initial exception. My log files don’t have it. Any ideas?
Here is my stack trace:
Exception: You have a nil object when you didn’t expect it!
You might have expected an instance of Array.
The error occured while evaluating nil.-
D:/dev/ruby/lib/ruby/gems/1.8/gems/activerecord-1.14.4/lib/active_record/transactions.rb:112:in `unlock_mutex’
D:/dev/ruby/lib/ruby/gems/1.8/gems/activerecord-1.14.4/lib/active_record/fixtures.rb:534:in `teardown’
D:/dev/ruby/lib/ruby/1.8/test/unit/testcase.rb:77:in `run’
D:/dev/ruby/lib/ruby/1.8/test/unit/testsuite.rb:32:in `run’
D:/dev/ruby/lib/ruby/1.8/test/unit/testsuite.rb:31:in `run’
D:/dev/ruby/lib/ruby/1.8/test/unit/testsuite.rb:32:in `run’
D:/dev/ruby/lib/ruby/1.8/test/unit/testsuite.rb:31:in `run’
D:/dev/ruby/lib/ruby/1.8/test/unit/ui/testrunnermediator.rb:44:in `run_suite’
D:/dev/RadRails/plugins/org.rubypeople.rdt.testunit_0.8.0.604272100PRD/ruby/RemoteTestRunner.rb:107:in `start_mediator’
D:/dev/RadRails/plugins/org.rubypeople.rdt.testunit_0.8.0.604272100PRD/ruby/RemoteTestRunner.rb:52:in `start’
D:/dev/RadRails/plugins/org.rubypeople.rdt.testunit_0.8.0.604272100PRD/ruby/RemoteTestRunner.rb:272
August 28th, 2006 at 7:46am
Nevermind I figured it out. RadRails was not showing the exception when setup failed.
August 29th, 2006 at 5:51pm
I spent much of today looking for a solution to the “rounding errors with price-as-float” issue, and so far I’m very happy I settled on dollars_and_cents. I wrote about the decision process here, if it’d interest anyone.
Thanks Coda!
September 16th, 2006 at 7:36pm
Coda,
Any progress on making the “…in_cents” more transparent on validation? (Your comment #10, above.)
September 21st, 2006 at 4:10am
[...] dollars_and_cents: a Rails plugin [...]
October 18th, 2006 at 9:01am
[...] dollars_and_cents: a Rails plugin | Archives | codablog | Coda Hale [...]
October 24th, 2006 at 8:23am
Hi,
I finally got this plugin to work in the console, I used
amount_in_cents and i could call the record and update it by
@amt = Amount.find(3)
@amt.amount = 23.90
@amt.save
and i took the value, but i don’t how to tie to the scaffold i have created. The scaffolding seems to have created a default integer entry field and is not taking decimal values. basically how do i update my record from the view with using this plugin. Appreciate all your help
November 4th, 2006 at 5:00pm
[...] So, instead of keeping people waiting, I decided to make this release, which isn’t quite as big as I was hoping it would be. I did toss in some fun stuff, though, like how to use the dollars_and_cents plugin for storing money amounts in the database and the acts_as_sluggable plugin to help with SEO for your store. You can check out the full list of changes, and of course, you can also buy it now! [...]
November 5th, 2006 at 7:41pm
i’m loving your plugin, but the problem i’m running into is that if a input is blank, dollar_and_cents seems to default to 0.0 . I would like to make sure a user inputs a value as opposed to using a default value. is there a way to work around this?
November 7th, 2006 at 11:54am
I’m loving the plugin as well, but I’m running into an issue I believe you’ve already solved. I just can’t find the solution.
I’m working on an application where the users want to see money fields formatted as money, with dollar signs and comma separators. I have written the javascript to make that happen. The issue I’m having is that the dollars_and_cents plugin doesn’t like those changes. It changes the amount to 0.0 before the data is saved to the database.
I tried to strip out the ‘$’ and commas using before_save and before_validation on the model, but it always finds 0 in the amount field.
Have you answered this before? Where might I find a solution?
Chris
November 25th, 2006 at 4:37pm
hi, i’m working through agile webdev with rails and i’m using your plugin to handle the money side of things. i’m using postgres and it won’t allow me to follow along the mysql tutorial.
however, i’m not sure how to get the Price formatted correctly - it just displays the number of cents.
how can I get it to display correctly? i’m working around page 80 of the 2nd edition.
thanks.
December 9th, 2006 at 3:30pm
Michael Campbell, you can just do this in your model until Coda gets around to fixing validations:
def validate
errors.add(:price, "can't be blank") if price_in_cents.blank?
end
December 9th, 2006 at 5:43pm
Just to let everyone know, Rails 1.2 will have native support for the DECIMAL fixed-point type, using the BigDecimal class. This means that once 1.2 comes out, this plugin becomes useless. I don’t plan on doing any further development on it, so if you have a pressing need for functionality, please don’t wait for me.
December 12th, 2006 at 1:33pm
What I want to do on my blog, is every few hours take the oldest post and move it to the
front of the queue, all automatically. Anyone know if there is a plugin that can do this or
a simple way to set up another plugin to do this (use my own feed perhaps)?
Thanks.
January 13th, 2007 at 10:37am
I don’t know anything about that data type, but I’m assuming it means the we can force the DB to cut off floats at two decimal places?
January 13th, 2007 at 12:42pm
Jed: Effectively, yes. Fixed-point math is what we do with money, and the BigDecimal class and DECIMAL SQL type do that.
January 30th, 2007 at 3:01pm
Here are a couple of migration helper methods you can use. I created these so that I could write a migration to update my database to use decimal columns instead of the *_in_cents columns now that Rails 1.2 has been released.
Example usage:
require "migration_helpers"
class ChangePriceColumnsToDecimal
The helper method code should be placed in lib/migration_helpers.rb:
module MigrationHelpers
def decimalize_column(model_name, old_column, new_column)
rename_column model_name.table_name, old_column, new_column
change_column model_name.table_name, new_column, :decimal, :precision => 8, :scale => 2
model_name.reset_column_information
model_name.find(:all).each do |row|
unless row[new_column] == nil
row.update_attribute new_column, row[new_column] / 100
end
end
end
def undecimalize_column(model_name, old_column, new_column)
model_name.find(:all).each do |row|
unless row[new_column] == nil
row.update_attribute new_column, row[new_column] * 100
end
end
change_column model_name.table_name, new_column, :integer
rename_column model_name.table_name, new_column, old_column
end
end
It works for me, but I haven’t tested in real extensively. Hopefully this can save someone some time.
January 30th, 2007 at 3:04pm
Ack, well that got pretty badly eaten.
I’ll post it up on my blog here in a minute. :)
(Feel free to delete that abomination of a comment Coda…)
January 30th, 2007 at 5:05pm
Here’s the permalink to my decimal column migration helper methods.
March 1st, 2007 at 10:59pm
[...] This is a bad idea in the long run since floats might store $2.50 as 2.50000001. See Coda Hale’s dollars_and_cents plugin. Ian adds: “Database tables for currency should be of type decimal. (e.g. amount DECIMAL(10,2)) [...]
March 27th, 2007 at 9:23pm
[...] this meant rounding errors. And no one wants to see rounding errors with their money. There were workarounds but it’s all been [...]
January 21st, 2008 at 5:24pm
I found quite a bad bug.
amount = “100″ #=> amount_in_cents = 10000 as it should be, but:
amount = “-100″ #=> amount_in_cents = -100, now this is wrong!
April 28th, 2008 at 6:43pm
[...] dollars_and_cents: a Rails plugin | Archives | codablog | Coda Hale [...]