开发者

Handling international currency input in Ruby on Rails

开发者 https://www.devze.com 2022-12-10 15:33 出处:网络
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 mi开发者_如何学JAVAght be 12.345,67.

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 mi开发者_如何学JAVAght 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.

0

精彩评论

暂无评论...
验证码 换一张
取 消