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:

http://www.ruby-forum.com/topic/112538

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...