I'm working on a very basic shopping cart system.
I have a table items that has a column price of type integer.
I'm having trouble displaying the price value in my views for prices that include both Euros and cents. Am I missing something obvious as far as handling currency in the Rails framework is concerned?
You'll probably want to use a DECIMAL type in your database. In your migration, do something like this:
# precision is the total number of digits
# scale is the number of digits to the right of the decimal point
add_column :items, :price, :decimal, :precision => 8, :scale => 2
In Rails, the :decimal type is returned as BigDecimal, which is great for price calculation.
If you insist on using integers, you will have to manually convert to and from BigDecimals everywhere, which will probably just become a pain.
As pointed out by mcl, to print the price, use:
number_to_currency(price, :unit => "€")
#=> €1,234.01
Here's a fine, simple approach that leverages composed_of (part of ActiveRecord, using the ValueObject pattern) and the Money gem
You'll need
The Money gem (version 4.1.0)
A model, for example Product
An integer column in your model (and database), for example :price
Write this in your product.rb file:
class Product > ActiveRecord::Base
composed_of :price,
:class_name => 'Money',
:mapping => %w(price cents),
:converter => Proc.new { |value| Money.new(value) }
# ...
What you'll get:
Without any extra changes, all of your forms will show dollars and cents, but the internal representation is still just cents. The forms will accept values like "$12,034.95" and convert it for you. There's no need to add extra handlers or attributes to your model, or helpers in your view.
product.price = "$12.00" automatically converts to the Money class
product.price.to_s displays a decimal formatted number ("1234.00")
product.price.format displays a properly formatted string for the currency
If you need to send cents (to a payment gateway that wants pennies), product.price.cents.to_s
Currency conversion for free
Common practice for handling currency is to use decimal type.
Here is a simple example from "Agile Web Development with Rails"
add_column :products, :price, :decimal, :precision => 8, :scale => 2
This will allow you to handle prices from -999,999.99 to 999,999.99
You may also want to include a validation in your items like
def validate
errors.add(:price, "should be at least 0.01") if price.nil? || price < 0.01
end
to sanity-check your values.
Just a little update and a cohesion of all the answers for some aspiring juniors/beginners in RoR development that will surely come here for some explanations.
Working with money
Use :decimal to store money in the DB, as #molf suggested (and what my company uses as a golden standard when working with money).
# precision is the total number of digits
# scale is the number of digits to the right of the decimal point
add_column :items, :price, :decimal, precision: 8, scale: 2
Few points:
:decimal is going to be used as BigDecimal which solves a lot of issues.
precision and scale should be adjusted, depending on what you are representing
If you work with receiving and sending payments, precision: 8 and scale: 2 gives you 999,999.99 as the highest amount, which is fine in 90% of cases.
If you need to represent the value of a property or a rare car, you should use a higher precision.
If you work with coordinates (longitude and latitude), you will surely need a higher scale.
How to generate a migration
To generate the migration with the above content, run in terminal:
bin/rails g migration AddPriceToItems price:decimal{8-2}
or
bin/rails g migration AddPriceToItems 'price:decimal{5,2}'
as explained in this blog post.
Currency formatting
KISS the extra libraries goodbye and use built-in helpers. Use number_to_currency as #molf and #facundofarias suggested.
To play with number_to_currency helper in Rails console, send a call to the ActiveSupport's NumberHelper class in order to access the helper.
For example:
ActiveSupport::NumberHelper.number_to_currency(2_500_000.61, unit: '€', precision: 2, separator: ',', delimiter: '', format: "%n%u")
gives the following output
2500000,61€
Check the other options of number_to_currency helper.
Where to put it
You can put it in an application helper and use it inside views for any amount.
module ApplicationHelper
def format_currency(amount)
number_to_currency(amount, unit: '€', precision: 2, separator: ',', delimiter: '', format: "%n%u")
end
end
Or you can put it in the Item model as an instance method, and call it where you need to format the price (in views or helpers).
class Item < ActiveRecord::Base
def format_price
number_to_currency(price, unit: '€', precision: 2, separator: ',', delimiter: '', format: "%n%u")
end
end
And, an example how I use the number_to_currency inside a contrroler (notice the negative_format option, used to represent refunds)
def refund_information
amount_formatted =
ActionController::Base.helpers.number_to_currency(#refund.amount, negative_format: '(%u%n)')
{
# ...
amount_formatted: amount_formatted,
# ...
}
end
If you are using Postgres (and since we're in 2017 now) you might want to give their :money column type a try.
add_column :products, :price, :money, default: 0
Use money-rails gem. It nicely handles money and currencies in your model and also has a bunch of helpers to format your prices.
Using Virtual Attributes (Link to revised(paid) Railscast) you can store your price_in_cents in an integer column and add a virtual attribute price_in_dollars in your product model as a getter and setter.
# Add a price_in_cents integer column
$ rails g migration add_price_in_cents_to_products price_in_cents:integer
# Use virtual attributes in your Product model
# app/models/product.rb
def price_in_dollars
price_in_cents.to_d/100 if price_in_cents
end
def price_in_dollars=(dollars)
self.price_in_cents = dollars.to_d*100 if dollars.present?
end
Source: RailsCasts #016: Virtual Attributes: Virtual attributes are a clean way to add form fields that do not map directly to the database. Here I show how to handle validations, associations, and more.
Definitely integers.
And even though BigDecimal technically exists 1.5 will still give you a pure Float in Ruby.
If someone is using Sequel the migration would look something like:
add_column :products, :price, "decimal(8,2)"
somehow Sequel ignores :precision and :scale
(Sequel Version: sequel (3.39.0, 3.38.0))
My underlying APIs were all using cents to represent money, and I didn't want to change that. Nor was I working with large amounts of money. So I just put this in a helper method:
sprintf("%03d", amount).insert(-3, ".")
That converts the integer to a string with at least three digits (adding leading zeroes if necessary), then inserts a decimal point before the last two digits, never using a Float. From there you can add whatever currency symbols are appropriate for your use case.
It's definitely quick and dirty, but sometimes that's just fine!
I am using it on this way:
number_to_currency(amount, unit: '€', precision: 2, format: "%u %n")
Of course that the currency symbol, precision, format and so on depends on each currency.
You can pass some options to number_to_currency (a standard Rails 4 view helper):
number_to_currency(12.0, :precision => 2)
# => "$12.00"
As posted by Dylan Markow
Simple code for Ruby & Rails
<%= number_to_currency(1234567890.50) %>
OUT PUT => $1,234,567,890.50
Related
I have an ActiveRecord model like this:
class Person < ActiveRecord::Base
attr_accessible :name
end
and need to get a hash mapping Person's ids to their names:
{1 => "Paul", 2 => "Aliyah", 3 => ... }
Now, the obvious way would be
Person.all.collect { |p| [p.id, p.name] }.to_h
However, I don't need to instantiate every Person, I just need the hash. In Rails 4, I can .pluck(:id, :name) instead of collect, however in 3.x, pluck takes only one argument. However I found this workaround to get what I want without loading the models:
Person.all.group(:id).minimum(:name)
Question: will I burn in hell? Also, is there a more elegant way to do this, and are there any drawbacks of this hacky approach that I may not be aware of? Thanks!
Here's a pretty good write up of this situation and various tactics for handling it: Plucking Multiple Columns in Rails 3
My preference of suggested solutions there is to make and include a module:
# multi_pluck.rb
require 'active_support/concern'
module MultiPluck
extend ActiveSupport::Concern
included do
def self.pluck_all(relation, *args)
connection.select_all(relation.select(args))
end
end
end
class Person < ActiveRecord::Base
attr_accessible :name
def self.pluck_id_and_name
result = connection.select_all(select(:id, :name))
if result.any?
# if you are using Ruby 2.1+
result.to_h
# Works in 1.9.3+
Hash[result]
end
end
end
Since the result should be an array of arrays we can use nifty trick to get a hash with the first element as keys and the second as values:
Hash[ [ [1, "Joe"], [2, "Jill"] ] ]
# => { 1 => "Joe", 2 => "Jill"}
See:
Convert array of 2-element arrays into a hash, where duplicate keys append additional values
To avoid loading all of the objects you could do this:
hash = Hash.new
ActiveRecord::Base.connection.execute("SELECT id, name FROM persons").each {|person| hash[person['id'].to_s] = person['name'].to_s}
My company used one of the tactics from Plucking Multiple Columns in Rails 3 before.
But we had trouble upgrading from Rails 3 to Rails 4 because it didn't work in Rails 4.
I suggest using pluck_all gem which has high test coverage in Rails 3, 4, 5, so you will not worry about future upgrades.
I'm fairly new to rails and having a problem that I'm sure is not unique, but can't seem to find an elegant solution to on my own:
I have a model with a :tax_rate decimal attribute that is used to create a total later on using some multiplication in the model.
This works just fine if the user enters the tax rate as '0.09' in the tax rate field, but obviously creates an erroniously large amount when the user enters '9'.
I'm looking to create some code that will handle a whole number being entered into my form.
My form:
<td><%= f.input :tax_rate, label: false, input_html: { class: 'taxtarget', precision: 2 }%></td>
The calcualtions in the model:
before_save :persist_calculations
validates_presence_of :price, :units
def calculated_total
price * units + calculated_tax
end
def calculated_tax
if self.tax_rate.present?
self.price * self.tax_rate
else
self.price
end
end
def persist_calculations
self.total = calculated_total
self.tax_amount = calculated_tax
end
I would like to either handle the whole number by preventing a whole number (requiring a decimal point) or by converting a whole number into a decimal by adding two decimal places to it.
Anyone have any good resources or ideas for how to accomplish this?
Either add a validation to your model for tax_rate so that it cannot be a larger than 1.0:
validates :tax_rate, less_than: 1.0
and/or add a before_validation to your model that converts large numbers to small ones:
before_validation ->(obj) {
obj.tax_rate = obj.tax_rate / 100 if obj.tax_rate > 1
}
and/or handle it your controller, which is generally preferable to using callbacks.
I am aware of number_to_currency, but wanted to use number_with_precision to output a price for some products in a db
I have a prices stored in a db, which has column type decimal precision: 10, scale: 2. The prices are stored as 3.00 and 2.99
I have a problem outputting the prices - it outputs correctly for the 2.99 price, but only outputs 3.0 for the other
Do you have any information on how to get all numbers to show two decimal places regardless of their value?
Thanks!
Here is my code:
#app/models/feature.rb
def price=(price)
number_with_precision price
end
There's really not a need for a more complicated method to do this in your case. Just use Ruby:
'%.2f' % price
# e.g:
'%.2f' % 3 #=> "3.00"
You specify the number of digits after decimal in the precision option:
<%= number_with_precision(price, precision: 2) %>
number_with_precision is from ActionView::Helpers::NumberHelper module. To use it where you don't have this helper available, you need to include this module:
class MyClass
include ActionView::Helpers::NumberHelper
def price_to_number(price)
number_with_precision(price, precision: 2)
end
end
The answer was thus:
#app/models/feature.rb
class Feature < ActiveRecord::Base
include ActionView::Helpers::NumberHelper
def price
number_with_precision read_attribute(:price), precision: 2
end
end
Using Rails 3.1.3 and Ruby 1.9.3.
I want to give the user a list of possible date/time formats. The user's selection will be stored in the Users table. Date/time values are then formatted using the I18n.localize function. I actually have 10 formats; here by way of example are the first two:
config/locales/datetime.en.yml
en:
time:
format_labels:
mdyslash12: mm/dd/yyyy - hh:mm am (12-hour)
mdyslash24: mm/dd/yyyy - hh:mm (24-hour)
formats:
mdyslash12: ! '%m/%d/%Y %I:%M%p'
mdyslash24: ! '%m/%d/%Y %H:%M'
My question is where to store the list of possible date/time formats. I've identified three possibilities.
1. List options as a CONSTANT in model:
app/models/user.rb
DATETIME_FORMATS = %w[mdyslash12 mdyslash24]
validates :datetime_format, :presence => true,
:inclusion => { :in => DATETIME_FORMATS }
2. Create an application constant and validate against that:
config/initializers/constants.rb
Rails.configuration.datetime_formats = "mdyslash12 mdyslash24"
app/models/user.rb
validates :datetime_format, :presence => true,
:inclusion => { :in => Rails.application.config.datetime_formats.split(" ") }
3. Validate directly against the locale file:
app/models/user.rb
validates :datetime_format, :presence => true,
:inclusion => { :in => (I18n.t 'time.format_labels').stringify_keys.keys }
This option uses a feature that is new to me: I18n.t 'time.format_labels' returns a hash of ALL keys and values from that branch of the locale file. The hash keys are symbols, so to get a string array, I call stringify_keys to convert the symbols to strings, then keys to give me only the keys (no values).
Option #3 is the DRYest in that I don't have to list the possible values in two places. But it doesn't feel quite right to depend on the locale file for the discreet list of possible date/time formats.
What would you recommend? One of these options? Something else?
I'd go with option 1 to start with, since it's simple, clear, and fairly DRY. I might refactor to option 2 if I ended up needing that constant in another model.
Option 3 has the potential to behave differently based on the locale, so I don't like that. If you end up forgetting to specify your format labels in a new locale, your selection list might end up being empty (or if there's a typo in one locale, it might take longer to notice, since the typo would be treated as valid for that locale). Regardless it's probably a good idea to unittest this in all your supported locales.
I have a model like this.
class Money
include Mongoid::Document
#interval is how often the compensation is paid
field :salary, :type => Integer # must be saved in cents
field :commission, :type => Integer # must be saved in cents
field :total, :type => Integer # must be saved in cents
end
total is sum of salary and commission. salary and commission both are saved in cents.
But my problem is that when it is edited i need to show it in dollar figure.
For example, if salary in cent is 5000000 then when i press edit i need to see 50000 in the salary textbox.
Some other solutions are also welcomed
If you want to enforce this pattern at the model level then you could override the setters and getters:
class Money
#...
def salary
self.salary / 100
end
def salary=(value)
self.salary * 100
end
end
In this case you'll have the editing/displaying for free, without writing any helpers.
Although, I think the proper way for doing it is at the view level through a helper definition. The model should not be concerned with this.
Look at ActionView::Helpers::NumberHelper. In your case you could write your own helper like this:
def money_to_textbox (money)
money / 100
end
This helper method should be placed in app\helpers and then in a view you can use like this:
<%= money_to_textbox #money %>