Problem with a setter that converts euros to cents


#1

Hi,

I have some problems with an application where I’m using custom
accessors to do currency conversions. In my model, I have a price
attribute in the database that stores the value in cents, to avoid
future problems with float arithmetic and round. But at the views, I
would like to show the price in euros, with decimal for the cents. So
I defined a new attribute called price_in_euros, and the corresponding
accesors:

class Report < ActiveRecord::Base
validates_numericality_of :price, :only_integer => true

attr_accessor :price_in_euros

def price_in_euros
	if self.price
		self.price / 100.0
	else
		# default price is 1 euro
		1
	end
end

def price_in_euros=(euros)
	self.price = (euros * 100).to_i
end

end

If I test this model with the rails console, all is working fine:

$ script/console
Loading development environment.

r = Report.new
=> #<Report:0xb74a53c0 @new_record=true, @attributes={“precio”=>nil}>

r.price_in_euros = 2.45
=> 2.45

r.save
=> true

r
=> #<Report:0xb74a53c0 @new_record_before_save=true,
@new_record=false, @errors=#<ActiveRecord::Errors:0xb7813264
@errors={}, @base=#<Report:0xb74a53c0 …>>, @attributes={“id”=>20,
“price”=>245}>

The attribute “price” has been set to 245 cents. Ok, let’s check the
database:

mysql> select id, price from reports;
±—±------+
| id | price |
±—±------+
| 20 | 245 |
±—±------+
1 row in set (0.00 sec)

It seems that all is ok. Well, let’s prepare a controller with a
couple of actions, insert and edit:

class ReportController < ApplicationController
def insert
@report = Report.new(params[:report])
if @request.post? and @report.save
redirect_to :action => ‘list’
end
end

def edit
	@report = Report.find(params[:id])
	@report.attributes = params[:report]
	if @request.post? and @report.save
		redirect_to :action => 'list'
	end
end

end

Here is part of the view for the insert action:

<% form_for :report, :url => { :action => ‘insert’ } do |form| %>
Price: <%= form.text_field :price_in_euros -%>
<%= submit_tag ‘Insert’ -%>

Here is part of the view for the edit action:

<% form_for :report, :url => { :action => ‘edit’ } do |form| %>
Price: <%= form.text_field :price_in_euros -%>
<%= submit_tag ‘Save’ -%>

Ok. Now I use that views to insert a couple of new reports, giving a
value of 2.45 for the first one and 1 for the second one. Here is what
it appears at the database:

mysql> select id,price from reports;
±—±-----------+
| id | price |
±—±-----------+
| 24 | 2 |
| 25 | 2147483647 |
±—±-----------+
2 rows in set (0.00 sec)

Ouch! That’s really strange.

And if I try to edit them, I can see a value of 0.02 on the price
field for the first one, and 21474836.47 for the second one (the price
at the database divided by 100), so the getter accesor seems to be
working well, but not the setter. If I click ‘Save’ on the edit form,
I then get:

mysql> select id,price from reports;
±—±---------+
| id | price |
±—±---------+
| 24 | 0 |
| 25 | 21474836 |
±—±---------+
2 rows in set (0.00 sec)

What in the hell I’m doing bad? Thanks in advance.

Carlos Alberto Paramio Danta .–.
http://www.sinfoniadebits.com/ |o_o |
email : carlosparamio @ gmail.com |:_/ |
jabber: parax @ jaim.at // \
----------------------------------------( | | )–
Fingerprint 41C6 D2BE 7DE7 AB61 C23F /’_ /`
F697 5A1D 1849 01B8 D318 _)=(
/


#2

I think you may find that the problem is that you are getting a string
from
the html form. Hence it works in irb when you pass a float, but coming
from
the web form it’s unreliable.

There are a couple of things that you could do. Probably one of the
more
robust is to convert the incoming parameter to a float inside the
method.
eg

def price_in_euros=(ammt)
self.price = (ammt.to_f * 100 ).to_i
end

There are issues with this tho… If the user enters some dodgy input
eg.
“abcd” the method will set your price to zero, or if they don’t enter
anything there is a problem as well.

One solution to this is to put in a callback hook to the before_save
callback that checks this value and sets it to a default if it’s zero
There
are shortcommings with this as well tho particularly with validations
They
will spit back errors if set for price > 0

def before_save
self.price = 100 if (self.price.nil? or self.price < 1)
end

This way you can use an int or a string for the operation. and you know
that
if the value is not specified then one euro (100 in the price field) is
set
in the database.

Also since you are specifying these getter and setter methods the call
to
attr_accessor :price_in_euros
is redundant since your over-writing the methods that this sets up.


#3

Hi Daniel,

I think you may find that the problem is that you are getting a string from
the html form. Hence it works in irb when you pass a float, but coming from
the web form it’s unreliable.

Yes! That was. Now works really well.

One solution to this is to put in a callback hook to the before_save
callback that checks this value and sets it to a default if it’s zero There
are shortcommings with this as well tho particularly with validations They
will spit back errors if set for price > 0

def before_save
self.price = 100 if (self.price.nil? or self.price < 1)
end

Great, this warranties a sensible value for the price attribute.

Also since you are specifying these getter and setter methods the call to

attr_accessor :price_in_euros
is redundant since your over-writing the methods that this sets up.

I supossed it, but I had some other error message in my view, and I
thinked that it was due to the attribute name used in the new way of
form definitions:

form.text_field :price_in_euros

I’ve removed the attr_accessor line and all works like a charm, so
finally it was some other thing that gets fixed.

Thanks!

Carlos A. Paramio
http://www.sinfoniadebits.com/