Number handling regarding locales and validations

What is the suggested path for dealing with number formats in Rails?

Even if we set locale to pt-BR, for instance, if the user inputs a number like '1.000,00' or '1000,00' in a form input, numerical validation will yield an 'invalid_number' error, even though this is a valid format in Brazil.

I could monkey patch overriding parse_raw_value_as_a_number to replace '.' with '' and ',' with '.' in ActiveModel::Validations::NumericalityValidator but that wouldn't solve the problem because the application is internationalized. So, even if I monkey patch this method, I would have to take into consideration the current user locale before parsing the number...

Are there any plans for supporting this on Rails 3?

Rodrigo.

Hi Rodrigo,

I think the proper way to do this is to override those formats in your translation yaml files (config/locales). For reference of what the keys are take a look at http://github.com/rails/rails/blob/master/actionpack/lib/action_view/locale/en.yml#L7-9

You’ll notice that those I18n keys are used in the number helpers like in number_with_delimiter here: http://github.com/rails/rails/blob/master/actionpack/lib/action_view/helpers/number_helper.rb#L209

You’ll have to create a file like config/locales/pt-BR.yml and at the very least add those number I18n keys for separator and delimiter.

-Brian

We use the same formats in Argentina, but I luckily I haven't yet been bitten by this. By sheer circumstance, I can't recall any times where I had a field that could be either a float or an integer that wasn't for money. So in my experience it's been easy to determine how to interpret the commas and periods, and cast the param string to an appropriate numeric value before passing it into the model.

But if you have a field that can admit arbitrary numeric values in an app that needs to support multiple locales, then you could definitely get bitten because 100,001 and 100.001 could mean different things to different people. So perhaps it might make sense if you could add a local-sensitive `I18n.to_f` and `.to_i`, as well as a validator.

Regards,

Norman

Brian, unfortunately these settings have any effect on string to float conversion performed by the validators.

Take a look at these methods from Rails 3 beta 4:

      def parse_raw_value_as_a_number(raw_value)          case raw_value          when /\A0[xX]/            nil          else            begin              Kernel.Float(raw_value)            rescue ArgumentError, TypeError              nil            end          end        end

       def parse_raw_value_as_an_integer(raw_value)          raw_value.to_i if raw_value.to_s =~ /\A[+-]?\d+\Z/        end

The locale support you are talking about is only related to number helpers like those you have mentioned...

I'm talking about extending this notion of locale support for numbers.

Rodrigo.

Hi Norman,

This is not about easy of interpreting numbers, but about consistency and DRY.

In my case, this is also a currency situation and although it is clear how to parse the string, we shouldn't be concerned about this while defining our models.

This is something that a framework should do by its own since it is feasible, DRY and less error prone and I can't see any reasons for not supporting this on the Rails core side.

For instance, consider this pseudo-code snippet that could be put in the numericality validator code:

      def parse_raw_value_as_a_number(raw_value)          raw_value.gsub!(I18n.separator_char, '').gsub!(I18n.delimiter_char, '.')          ...        end

WDYT?

Best regards,

Rodrigo.

Yeah, I do think making it transparent for models is a good idea. Do I understand you correctly that Rails currently casts the numeric string to the wrong decimal value, even if you set the i18n delimiter and separator?

In any event I do still think a local-aware `to_f` and `to_i` could be useful as top-level I18n methods. You may need to do locale-aware casts outside validations, and you may need to do them for a specific locale even though your current locale is set to something else, e.g.:

I18n.to_f("100,001", :locale => :pt_br) #=> 100.001 I18n.to_f("100,001", :locale => :en_us) #=> 100001.00

This is not about easy of interpreting numbers, but about consistency and DRY.

In my case, this is also a currency situation and although it is clear how to parse the string, we shouldn't be concerned about this while defining our models.

This is something that a framework should do by its own since it is feasible, DRY and less error prone and I can't see any reasons for not supporting this on the Rails core side.

For instance, consider this pseudo-code snippet that could be put in the numericality validator code:

     def parse_raw_value_as_a_number(raw_value)         raw_value.gsub!(I18n.separator_char, '').gsub!(I18n.delimiter_char, '.')         ...       end

WDYT?      

Yeah, I do think making it transparent for models is a good idea. Do I understand you correctly that Rails currently casts the numeric string to the wrong decimal value, even if you set the i18n delimiter and separator?     Sure. This has been my main complaint about Rails since I started used it in 2007 (Rails 1). I found this message, that is probably my first contact to Rails developers:

I18n is the topic that most lacks love from Rails core team. Here are some more examples from patches I've submited for dealing better with internationalized applications:

http://www.mail-archive.com/rubyonrails-core@googlegroups.com/msg10234.html

https://rails.lighthouseapp.com/projects/8994/tickets/3768-patch-add-full_message-option-to-validations

There's a long time since I last got any feedback about this ticket, for instance (since February)...

I need to invest too many time for trying to convince the core team to give more love to i18n issues that I've just given up. It doesn't worth... :frowning:

And that is the only topic I'm sad about Rails. All the remaining parts are just perfect! In any event I do still think a local-aware `to_f` and `to_i` could be

useful as top-level I18n methods. You may need to do locale-aware casts outside validations, and you may need to do them for a specific locale even though your current locale is set to something else, e.g.:

I18n.to_f("100,001", :locale => :pt_br) #=> 100.001 I18n.to_f("100,001", :locale => :en_us) #=> 100001.00

Sure, this would be a great feature for I18n gem. But this would be probably better implemented like:

I18n.locale = :'pt-BR' I18n.l(13.5, 3) # => 13,500 (for locale=pt-BR)

Currently, this yields the following error (I18n.l(13.5)):

I18n::ArgumentError: Object must be a Date, DateTime or Time object. 13.5 given.          from /home/rodrigo/.rvm/gems/ruby-1.8.7-p174@rails3/gems/i18n-0.4.1/lib/i18n/backend/base.rb:57:in `localize'          from /home/rodrigo/.rvm/gems/ruby-1.8.7-p174@rails3/gems/i18n-0.4.1/lib/i18n.rb:231:in `l'

Best regards,

Rodrigo.

I need to invest too many time for trying to convince the core team to give more love to i18n issues that I've just given up. It doesn't worth... :frowning:

And that is the only topic I'm sad about Rails. All the remaining parts are just perfect!

Well, try not to get too frustrated. I think that's usually how it goes with any large, busy open source project. Looks like in that ticket's thread José took a pretty good amount of time to reply to you - and people don't usually waste time replying if they simply don't care.

Contributing to open source takes a lot of persistence and thick skin. I've had my fair share of patches rejected and ignored, too. Anyway, I think you've consistently raised important issues on this list, and given useful answers to other people's questions. I'm not a member of the Rails core team, but I would encourage you to keep doing what you are doing. :slight_smile:

In any event I do still think a local-aware `to_f` and `to_i` could be

useful as top-level I18n methods. You may need to do locale-aware casts outside validations, and you may need to do them for a specific locale even though your current locale is set to something else, e.g.:

I18n.to_f("100,001", :locale => :pt_br) #=> 100.001 I18n.to_f("100,001", :locale => :en_us) #=> 100001.00

Sure, this would be a great feature for I18n gem. But this would be probably better implemented like:

I18n.locale = :'pt-BR' I18n.l(13.5, 3) # => 13,500 (for locale=pt-BR)

That's actually the opposite of what I'm suggesting. That method formats an object so that it can be displayed according to the settings for the locale.

I'm taking about adding a method that accepts localized input like "10.000,001" and returns the correct float value.

But you're right: that method should be made to work correctly for the case you gave; and if not, then at least the documentation should be changed.

Do you have any real use case where it would make sense to use I18n.to_f(number, locale) instead of I18n.l(number, precision)?

Usually an application will be either a single-language application or an internationalized one. If it is a single-language application, I18n.default_locale would always format float numbers accordingly.

If it is a multi-language application, the locale wil probably be set by a before_filter on each request from user's preference or from a URL prefix, for instance. In this case, calling I18n.l(number) would use that locale for any conversion.

What would be a real use case for the other proposal (I18n.to_f(float, locale))?

Best Regards,

Rodrigo.

p.s: thank you for trying to reduce my frustrations, but I really don't like to waste my time. That doesn't mean I stopped contributing at all. I'm just not contributing to Rails any time soon... I'll use my free time for contributing to other projects with lower barriers, such as Redmine, some Rails plugins and others. When I have some free time again, I'll make a second attempt of contributing to Gitorious. In the first one, I've submitted a simple merge request for correcting the Gitorious name in two places (it was Gitorius in 2 places). I've got no feedback for a long time. Some day they have corrected the name but didn't update my merge request to cancel it so that I had to close it myself.

Some months ago I've modified my local installation of Gitorious for allowing tree view of the repository source-code and it is working great at my company. But I didn't submit yet because I need to organize it a bit more before requesting a merge from Gitorious commiters. But I'm postponing this because of my previous experience with Gitorious feedback. I feel that my effort will be wasted, but I'll try it anyway when I get some free time. If I don't get any feedback I'll give up on contributing to Gitorious too.

I had a much better experience contributing to Redmine and EzGraphix plugin for Rails... So, I'll probably concentrate my efforts on these projects instead, for instance...

I appreciate your words anyway, thank you.

Do you have any real use case where it would make sense to use I18n.to_f(number, locale) instead of I18n.l(number, precision)?

Ok, either I don't understand something, am not communicating clearly, or both. I think they are opposites. This is what I think they could do:

# Given an instance of Float, format it to a string to display to # Brazilian users. I18n.localize(100000.01, :locale => :'pt-BR') #=> "100.000,01"

# Given input from a Brazilian user, cast to a Float. I18n.to_f("100.001", :locale => :'pt-BR') #=> 100001.00

# Given an instance of Float, format it to a string to display to # USA users. I18n.localize(100000.01, :locale => :'en-US') #=> "100,000.01"

# Given input from a USA user, cast to a Float. Notice the value # is different from what a Brazilian user would mean. I18n.to_f("100.001", :locale => :'en-US') #=> 100.001

Ok, you're right, sorry, I didn't get it at first. Than, I would say that to_f should be implemented as:

def I18n.to_f(value, locale=nil)    locale ||= I18n.locale # current locale if none was given end

This would allow I18n.to_f('19.250,30') == 19250.3 == I18n.to_f('19,250.30', :en)

Why passing an option as the second parameter. What other options can you think of?

Best regards,

Rodrigo.

No other options occur to me; I would implement it similarly to the other top-level methods in i18n. Maybe something like:

def to_f(object, options = {})   return object.to_f unless locale.separator && locale.delimiter   locale = options.delete(:locale) || config.locale   object.to_s.gsub(locale.separator_char, '').gsub(locale.delimiter_char, '.').to_f end

Thinking about it some more though, the problem is, "delimiter" and "separator" are not defined by default, I'm assuming because the i18n library doesn't want to hard-code any assumptions about what goes into the localization files. So I'm not 100% sure this should actually go into i18n, it may be best added somewhere in Rails. I definitely think this is a problem that should be solved, but giving it some more thought, I'm not sure this is the right way to do it.

   

Do you have any real use case where it would make sense to use I18n.to_f(number, locale) instead of I18n.l(number, precision)?

Ok, either I don't understand something, am not communicating clearly, or both. I think they are opposites. This is what I think they could do:

# Given an instance of Float, format it to a string to display to # Brazilian users. I18n.localize(100000.01, :locale => :'pt-BR') #=> "100.000,01"

# Given input from a Brazilian user, cast to a Float. I18n.to_f("100.001", :locale => :'pt-BR') #=> 100001.00

# Given an instance of Float, format it to a string to display to # USA users. I18n.localize(100000.01, :locale => :'en-US') #=> "100,000.01"

# Given input from a USA user, cast to a Float. Notice the value # is different from what a Brazilian user would mean. I18n.to_f("100.001", :locale => :'en-US') #=> 100.001

Ok, you're right, sorry, I didn't get it at first. Than, I would say that to_f should be implemented as:

def I18n.to_f(value, locale=nil)   locale ||= I18n.locale # current locale if none was given end

This would allow I18n.to_f('19.250,30') == 19250.3 == I18n.to_f('19,250.30', :en)

Why passing an option as the second parameter. What other options can you think of?      

No other options occur to me; I would implement it similarly to the other top-level methods in i18n. Maybe something like:

def to_f(object, options = {})    return object.to_f unless locale.separator&& locale.delimiter    locale = options.delete(:locale) || config.locale    object.to_s.gsub(locale.separator_char, '').gsub(locale.delimiter_char, '.').to_f end    

Actually, the order does not seem to be right. Maybe you mean:

def to_f(object, options = {})    locale = options.delete(:locale) || config.locale    return object.to_f unless locale.separator&& locale.delimiter    ... end

Thinking about it some more though, the problem is, "delimiter" and "separator" are not defined by default, I'm assuming because the i18n library doesn't want to hard-code any assumptions about what goes into the localization files. So I'm not 100% sure this should actually go into i18n, it may be best added somewhere in Rails. I definitely think this is a problem that should be solved, but giving it some more thought, I'm not sure this is the right way to do it.     I warned you it was a pseudo-code. delimiter_char and separator_char are not defined in I18n but it made my example clearer. Actual code would be something like:

def I18n.delimiter_char      t('number.format.delimiter', :default => nil) end

No other options occur to me; I would implement it similarly to the other top-level methods in i18n. Maybe something like:

def to_f(object, options = {}) return object.to_f unless locale.separator&& locale.delimiter locale = options.delete(:locale) || config.locale object.to_s.gsub(locale.separator_char, '').gsub(locale.delimiter_char, '.').to_f end

Actually, the order does not seem to be right. Maybe you mean:

def to_f(object, options = {}) locale = options.delete(:locale) || config.locale return object.to_f unless locale.separator&& locale.delimiter ... end

Right... so much for email driven development. :slight_smile:

Thinking about it some more though, the problem is, "delimiter" and "separator" are not defined by default, I'm assuming because the i18n library doesn't want to hard-code any assumptions about what goes into the localization files. So I'm not 100% sure this should actually go into i18n, it may be best added somewhere in Rails. I definitely think this is a problem that should be solved, but giving it some more thought, I'm not sure this is the right way to do it.

I warned you it was a pseudo-code. delimiter_char and separator_char are not defined in I18n but it made my example clearer. Actual code would be something like:

def I18n.delimiter_char t('number.format.delimiter', :default => nil) end

Sure... the thing is, we're still depending on the locale implementation putting the delimiter in a certain place.... which might be fine but would need to be chosen carefully and documented. Perhaps it would be best to take the discussion to the i18n list.

No other options occur to me; I would implement it similarly to the other top-level methods in i18n. Maybe something like:

def to_f(object, options = {})    return object.to_f unless locale.separator&& locale.delimiter    locale = options.delete(:locale) || config.locale    object.to_s.gsub(locale.separator_char, '').gsub(locale.delimiter_char, '.').to_f end

Actually, the order does not seem to be right. Maybe you mean:

def to_f(object, options = {})   locale = options.delete(:locale) || config.locale   return object.to_f unless locale.separator&& locale.delimiter   ... end

Right... so much for email driven development. :slight_smile:     :slight_smile:

Thinking about it some more though, the problem is, "delimiter" and "separator" are not defined by default, I'm assuming because the i18n library doesn't want to hard-code any assumptions about what goes into the localization files. So I'm not 100% sure this should actually go into i18n, it may be best added somewhere in Rails. I definitely think this is a problem that should be solved, but giving it some more thought, I'm not sure this is the right way to do it.

I warned you it was a pseudo-code. delimiter_char and separator_char are not defined in I18n but it made my example clearer. Actual code would be something like:

def I18n.delimiter_char     t('number.format.delimiter', :default => nil) end      

Sure... the thing is, we're still depending on the locale implementation putting the delimiter in a certain place.... which might be fine but would need to be chosen carefully and documented. Perhaps it would be best to take the discussion to the i18n list.     I believe this is where I18n already looks for delimiter and separator in number helpers unless I'm missing something...