Handling international currency input in Ruby on Rails - ruby-on-rails

I have an application that handles currency inputs. However, if you're in the US, you might enter a number as 12,345.67; in France, it might be 12.345,67.
Is there an easy way, in Rails, to adapt the currency entry to a locale?
Note that I'm not looking for display of the currency (ala number_to_currency), I'm looking to deal with someone typing in a currency string, and converting it into a decimal.

You could give this a shot:
def string_to_float(string)
string.gsub!(/[^\d.,]/,'') # Replace all Currency Symbols, Letters and -- from the string
if string =~ /^.*[\.,]\d{1}$/ # If string ends in a single digit (e.g. ,2)
string = string + "0" # make it ,20 in order for the result to be in "cents"
end
unless string =~ /^.*[\.,]\d{2}$/ # If does not end in ,00 / .00 then
string = string + "00" # add trailing 00 to turn it into cents
end
string.gsub!(/[\.,]/,'') # Replace all (.) and (,) so the string result becomes in "cents"
string.to_f / 100 # Let to_float do the rest
end
And the test Cases:
describe Currency do
it "should mix and match" do
Currency.string_to_float("$ 1,000.50").should eql(1000.50)
Currency.string_to_float("€ 1.000,50").should eql(1000.50)
Currency.string_to_float("€ 1.000,--").should eql(1000.to_f)
Currency.string_to_float("$ 1,000.--").should eql(1000.to_f)
end
it "should strip the € sign" do
Currency.string_to_float("€1").should eql(1.to_f)
end
it "should strip the $ sign" do
Currency.string_to_float("$1").should eql(1.to_f)
end
it "should strip letter characters" do
Currency.string_to_float("a123bc2").should eql(1232.to_f)
end
it "should strip - and --" do
Currency.string_to_float("100,-").should eql(100.to_f)
Currency.string_to_float("100,--").should eql(100.to_f)
end
it "should convert the , as delimitor to a ." do
Currency.string_to_float("100,10").should eql(100.10)
end
it "should convert ignore , and . as separators" do
Currency.string_to_float("1.000,10").should eql(1000.10)
Currency.string_to_float("1,000.10").should eql(1000.10)
end
it "should be generous if you make a type in the last '0' digit" do
Currency.string_to_float("123,2").should eql(123.2)
end

We wrote this:
class String
def safe_parse
self.gsub(I18n.t("number.currency.format.unit"), '').gsub(I18n.t("number.currency.format.delimiter"), '').gsub(I18n.t("number.currency.format.separator"), '.').to_f
end
end
Of course, you will have to set the I18n.locale before using this. And it currently only converts the string to a float for the locale that was set. (In our case, if the user is on the french site, we expect the currency amount text to only have symbols and formatting pertaining to the french locale).

You need to clean the input so that users can type pretty much whatever they want to, and you'll get something consistent to store in your database. Assuming your model is called "DoughEntry" and your attribute is "amount," and it is stored as an integer.
Here's a method that converts a string input to cents (if the string ends in two digits following a delimeter, it's assumed to be cents). You may wish to make this smarter, but here's the concept:
def convert_to_cents(input)
if input =~ /^.*[\.,]\d{2}$/
input.gsub(/[^\d-]/,'').to_i
else
"#{input.gsub(/[^\d-]/,'')}00".to_i
end
end
>> convert_to_cents "12,345"
=> 1234500
>> convert_to_cents "12.345,67"
=> 1234567
>> convert_to_cents "$12.345,67"
=> 1234567
Then overwrite the default "amount" accessor, passing it through that method:
class DoughEntry << ActiveRecord::Base
def amount=(input)
write_attribute(:amount, convert_to_cents(input))
end
protected
def convert_to_cents(input)
if input =~ /^.*[\.,]\d{2}$/
input.gsub(/[^\d-]/,'').to_i
else
"#{input.gsub(/[^\d-]/,'')}00".to_i
end
end
end
Now you're storing cents in the database. Radar has the right idea for pulling it back out.

Tim,
You can try to use the 'aggregation' feature, combined with a delegation class. I would do something like:
class Product
composed_of :balance,
:class_name => "Money",
:mapping => %w(amount)
end
class Money < SimpleDelegator.new
include Comparable
attr_reader :amount
def initialize(amount)
#amount = Money.special_transform(amount)
super(#amount)
end
def self.special_transform(amount)
# your special convesion function here
end
def to_s
nummber_to_currency #amount
end
end
In this way, you will be able to directly assign:
Product.update_attributes(:price => '12.244,6')
or
Product.update_attributes(:price => '12,244.6')
The advantage is that you do not have to modify anything on controllers/views.

Using the translations for numbers in the built-in I18n should allow you to enter your prices in one format (1234.56) and then using I18n bringing them back out with number_to_currency to have them automatically printed out in the correct locale.
Of course you'll have to set I18n.locale using a before_filter, check out the I18n guide, section 2.3.

Related

How to format a price correctly regardless of decimal format

I want it to be able to read in JSON and save it correctly regardless whether the value is 44.5, 44 or 44.99. The price attributes are a decimal format.
The error is happening in the convert_price method. The price in the JSON response can be 44, 44.50 or 44.99. However, I noticed that sometimes the last decimal is cut off, like in the error 44.5.
I'm receiving this error:
undefined method 'match' for float 74.5:Float
My code is:
# read in JSON and create books
def create_item
job_items_url = "https://foobar.com&format=json"
response = open(job_items_url).read.to_s
books = JSON.parse(response)
Book.create(reg_price: convert_price(item['reg_price']),
sale_price: convert_price(item['sale_price']))
end
# format the price
def convert_price(price)
return nil if price.blank? || price.to_f.zero?
price = "#{price}.00" unless price.match(/[,.]\d{2}\z/)
price.delete(',.').to_f / 100
end
You can use number_to_currency without a unit:
>> number_to_currency(45,unit:"")
=> "45.00"
>> number_to_currency(45.5,unit:"")
=> "45.50"
>> number_to_currency(45.55,unit:"")
=> "45.55"
>>
See number_to_currency for more information.
It looks like price is already a Numeric object. Check out sprintf for simple Type-aware padding, for example:
sprintf('%.2f', 44.5) # => "44.50"
# so you should do something like this:
sprintf('%.2f', price.to_f)
A suggestion:
def try_format_currency(price)
sprintf('%.2f', Float(price))
rescue => ex
# log error if you want
nil
end
Use Float() conversion, which can raise, which will clearly express that price is "untrustworthy" input, and might not be a proper number
Express the same thing in the naming of the method

Rails - Usernames that cannot start or end with characters

Originally asked this question about Regex for Usernames: Usernames that cannot start or end with characters
How can I achieve this with correct syntax for Ruby on Rails?
Here's my current User.rb validation:
validates_format_of :username, with: /\A[\w\._]{3,28}\z/i
This validation allows underscores and periods, but the goal is to not allow them at the start or end of a username.
I'm trying to achieve these rules with Rails regex:
Can contain lowercase and uppercase letters and numbers
Can contain underscores and periods
Cannot contain 2 underscores in a row
Cannot contain 2 periods in a row
Cannot begin or end with an underscore or period
Cannot contain letters with accents
Must be between 3 and 28 letters in length
Valid:
Spicy_Pizza
97Indigos
Infinity.Beyond
Invalid:
_yahoo
powerup.
un__real
no..way
You may use
/\A(?=.{3,28}\z)[a-zA-Z0-9]+(?:[._][a-zA-Z0-9]+)*\z/
See the Rubular demo.
Details
\A - start of string
(?=.{3,28}\z) - 3 to 28 chars other than line break chars up to the end of the string are allowed/required
[a-zA-Z0-9]+ - one or more ASCII letters / digits
(?:[._][a-zA-Z0-9]+)* - 0+ sequences of:
[._] - a . or _
[a-zA-Z0-9]+ - one or more ASCII letters / digits
\z - end of string.
Although totally possible, as Wiktor's answer shows, my recommendation would be to not define this in a single regular expression, since:
The solution is quite confusing to understand, unless you know regular expressions quite well.
Similarly, the solution is quite difficult to update with new requirements, unless you understand regular expressions quite well.
By performing this entire check in one go, if a validation fails then you'll inevitably end up with one generic error message, e.g. "Invalid Format", which does not explain why it's invalid. The exercise is then left to the user to re-read the nontrivial format rules and understand why.
Instead, I would recommend defining a custom validation class, which can perform each of these checks separately (via easy to understand methods), and add a different error message upon each check failing.
Something along the lines of:
# app/models/user.rb
class User < ApplicationRecord
validates :username, presence: true, username: true
end
# app/validators/username_validator.rb
class UsernameValidator < ActiveModel::EachValidator
def validate(record, attribute, value)
validate_length(record, attribute, value)
validate_allowed_chars(record, attribute, value)
validate_sequential_chars(record, attribute, value)
validate_first_and_last_chars(record, attribute, value)
end
private
def validate_length(record, attribute, value)
unless value.length >= 3 && value.length <= 28
record.errors[attribute] << "must be between 3 and 28 characters long"
end
end
def validate_allowed_chars(record, attribute, value)
unless value =~ /\A[._a-zA-Z0-9]*\z/
record.errors[attribute] << "must only contain periods, underscores, a-z, A-Z or 0-9"
end
end
def validate_sequential_chars(record, attribute, value)
if value =~ /[._]{2}/
record.errors[attribute] << "cannot contain two consecutive periods or underscores"
end
end
def validate_first_and_last_chars(record, attribute, value)
if value =~ /\A[._]/ || value =~ /[._]\z/
record.errors[attribute] << "cannot start/end with a period or underscore"
end
end
end
So for instance, you asked above: "What if I needed to extend this to allow lowercase letters only?" I think it's now quite obvious how the code could be updated to accommodate such behaviour, but to be clear - all you'd need to do is:
def validate_allowed_chars(record, attribute, value)
unless value =~ /\A[._a-z0-9]*\z/
record.errors[attribute] << "must only contain periods, underscores, a-z or 0-9"
end
end
You could also now, quite easily, write tests for these validation checks, and assert that the correct validation is being performed by verifying against the contents of the error message; something that is not possible when all validation failures result in the same error,
Another benefit to this approach is that the code can easily be shared (perhaps with some slight behavioural differences). You could perform the same validation on multiple attributes, or multiple models, perhaps with different allowed lengths or formats.

Strange things happening when I override the getters and setters of my ActiveRecord model

I have a User model where I include a mobile phone number. The mobile phone number will be saved in the database in international format. Because users are often not aware of this and do not like international formats, I changed the getter and setters so that the user is able to enter numbers in local format and they get the local format displayed, if there is no international prefix.
class User < ActiveRecord::Base
def mobile=(number)
# Strip whitespace, dashes and slashes first
[" ", "-", "/", "\\"].each do |particle|
number.gsub!(particle, "")
end
# Check if there is leading 00, this indicates a
# country code.
number.gsub!(/^00/,"+")
# Check if there is only one leading zero. If there is,
# treat as German number
number.gsub!(/^0/,"+49")
# Now write to attribute. Validate later.
puts "Writing: #{number}"
write_attribute(:mobile, number)
end
def mobile
number = read_attribute(:mobile)
puts "Reading: #{number}"
# If this is a German number, display as local number.
number.gsub!(/\+49/,"0")
number
end
end
Now, it seems that this does not work quite as expected. This is my rails console session:
> u = User.new(:mobile => "0163 12345")
Writing: +4916312345
=> #<User id: nil, mobile: "+4916312345", ...>
This worked as expected. So, lets check the getter:
> u.mobile
Reading: +4916312345
=> "016312345"
Looks good. But better check it again:
> u.mobile
Reading: 016312345
=> "016312345"
WTF? My attribute changed. Is this limited to the getter function?
> u
=> #<User id: nil, mobile: "016312345", ...>
No. It sets the attribute even in the database model.
If I access the attribute twice, the attribute changes. I did not access write_attribute. Why does my attribute change?
Consider this simplified example:
class User
def initialize
#attributes = { :mobile => "+4916312345" }
end
def read_attribute name
#attributes[name]
end
end
Notice that read_attribute returns the value of the attribute, not a copy of the value.
Now:
user = User.new
mobile = user.read_attribute :mobile
=> "+4916312345"
mobile.gsub!(/\+49/,"0")
=> "016312345"
mobile = user.read_attribute :mobile
=> "016312345" # because we modified it in place with gsub!
All you need to do is use gsub instead of gsub! in your getter, and since you'll never replace +49 more than once in the same string, you might as well just use sub.
def mobile
number = read_attribute(:mobile)
puts "Reading: #{number}"
# If this is a German number, display as local number.
number.sub(/\+49/,"0")
end

Localizing Ruby alphabet

I'm working on I18N for a web application (Rails), and part of the app needs to display a select containing the alphabet for a selected locale. My question is, is there a way to get Ruby to handle this or do I need to go thru the Rails-provided I18N API?
This is the array I'm using for generating the select options:
'A'.upto('Z').to_a.concat(0.upto(9).to_a)
I need to translate that to Russian, Chinese & Arabic.
You need to create an HTML select, with all the letters of a particular alphabet?
That would theoretically work for Russian and Arabic, but Chinese doesn't have an 'alphabet'.
The writing system contains thousands of characters.
I think you need to implement this yourself. Afaik Rails i18n plugins don't provide this information.
A nice solution would be to creating you own Range.
Example from the docs:
class Xs # represent a string of 'x's
include Comparable
attr :length
def initialize(n)
#length = n
end
def succ
Xs.new(#length + 1)
end
def <=>(other)
#length <=> other.length
end
def to_s
sprintf "%2d #{inspect}", #length
end
def inspect
'x' * #length
end
end
r = Xs.new(3)..Xs.new(6) #=> xxx..xxxxxx
r.to_a #=> [xxx, xxxx, xxxxx, xxxxxx]
r.member?(Xs.new(5)) #=> true

Rails / ActiveRecord: field normalization

I'm trying to remove the commas from a field in a model. I want the user to type a number, i.e. 10,000 and that number should be stored in the database as 10000. I was hoping that I could do some model-side normalization to remove the comma. I don't want to depend on the view or controller to properly format my data.
I tried:
before_validation :normalize
def normalize
self['thenumber'] = self['thenumber'].to_s.gsub(',','')
end
no worky.
http://github.com/mdeering/attribute_normalizer looks like a promising solution to this common problem. Here are a few examples from the home page:
# By default it will strip leading and trailing whitespace
# and set to nil if blank.
normalize_attributes :author, :publisher
# Using one of our predefined normalizers.
normalize_attribute :price, :with => :currency
# You can also define your normalization block inline.
normalize_attribute :title do |value|
value.is_a?(String) ? value.titleize.strip : value
end
So in your case you might do something like this:
normalize_attribute :title do |value|
value.to_s.gsub(',', '')
end
I think you're doing it right. This test passes:
test "should remove commas from thenumber" do
f = Foo.new(:thenumber => "10,000")
f.save
f = Foo.find(f.id)
assert f.thenumber == "10000"
end
And I used your code.
class Foo < ActiveRecord::Base
before_validation :normalize
def normalize
self['thenumber'] = self['thenumber'].to_s.gsub(',','')
end
end
Now, my schema is set up for thenumber to be a string though, not an integer.
Started
.
Finished in 0.049666 seconds.
1 tests, 1 assertions, 0 failures, 0 errors
If you wanted to store this in the db as an integer, then you definitely need to override the setter:
def thenumber=(value)
self['thenumber'] = value.to_s.gsub(',','').to_i
end
If you do it your way, with an integer column, it gets truncated by AR....
>> f.thenumber = "10,000"
=> "10,000"
>> f.thenumber
=> 10
That's a little-known thing with Ruby and integers... it auto-casts by truncating anything that's no longer an integer.
irb(main):004:0> i = "155-brian-hogan".to_i
=> 155
Can be cool for things like
/users/155-brian-hogan
#user = User.find_by_id(params[:id])
But not so cool for what you're doing.
So either change the col to a string and use the filter, or change the setter :)
Good luck!
The problem with doing it that way is that for a while, the non-normalized stuff will exist in the object; if you have code that works on the attributes before stuff gets normalised, then that will be a problem.
You could define a setter:
def thenumber=(value)
# normalise stuff here, call write_attribute
end
Unfortunately I think a lot of the Rails form stuff writes the attributes directly, which is one of the reasons I don't tend to use it.
Or you could normalise the params in the controller before you pass them through.
Does ruby let you interchange between a . and [''] ?
I don't know, I'll try later, but I think you are supposed to use .
self.thenumber = self.thenumber.to_s.gsub(',','')
You should return true from your before_validation method, otherwise if the expression being assigned to self['thenumber'] ends up being nil or false, the data will not be saved, per the Rails documention:
If a before_* callback returns false,
all the later callbacks and the
associated action are cancelled.
Ostensibly, you are trying to normalize here then check the result of the normalization with your Rails validations, which will decide if nil/false/blank are okay or not.
before_validation :normalize
def normalize
self['thenumber'] = self['thenumber'].to_s.gsub(',','')
return true
end

Resources