Swistak35's blog

Decimals, rails, and "," as a decimal separator

This will be completely technical post. I want just to leave somewhere a solution for a problem which I had to solve some time ago. I hope that there is some gem to do that. But the only one I found, which could fit my needs, is Delocalize, but it do some stuff on my models - I wanted to keep my models intact.

The problem is, that rails interpret numbers in format “1.23”, but in many countries we use “1,23”, and we have only ‘,’ on numpad, so if users have to type a lot of numbers in forms (usecase in my application), it’s obviously a pain.

params[:foo] = DecimalSupport.parse_model_decimals(params[:foo], Foo)
module DecimalSupport
  def self.parse_model_decimals(hash, model, options = {})
    defaults = I18n.translate(:'number.format', locale: options[:locale], default: {})
    precision_defaults = I18n.translate(:'number.precision.format', locale: options[:locale], default: {})
    defaults = defaults.merge(precision_defaults)
    options = options.reverse_merge(defaults)

    hash.each do |field, value|
      column = model.columns.find {|c| c.name == field.to_s}

      hash[field] = if column.type == :decimal
        self.parse_decimal(value, options)
      else
        value
      end
    end
    hash
  end

  def self.parse_decimal(value, options = {})
    if value.blank?
      "0.0"
    else
      separator_position = value.rindex(options[:separator])

      if separator_position.present?
        int_part = value[0...separator_position]
        frac_part = value[(separator_position+1)..-1]

        int_part.gsub!(options[:delimiter], '')
        [int_part, frac_part].join(".")
      else
        value
      end
    end
  end
end
ActionView::Helpers::FormBuilder.class_eval do
  include ActionView::Helpers::NumberHelper

  def decimal_number_field(field, options = {})
    value = object.send(field)
    column = object.class.columns.find {|c| c.name == field.to_s}

    new_options = { raise: true }
    new_options[:precision] = column.try(:scale)
    new_options[:separator] = options.delete(:separator)
    new_options[:delimiter] = options.delete(:delimiter)
    new_options[:significant] = options.delete(:significant)
    new_options[:strip_insignificant_zeros] = options.delete(:strip_insignificant_zeros)
    new_options[:raise] = true

    new_options.delete_if {|k,v| v.nil? }

    options[:value] = number_with_precision(value, new_options)
    options[:class] = "#{options[:class]} decimal_number_field"

    text_field(field, options)    
  end
end

In first gist, we have usage example in controller. In the second, an implementation of these method. It can probably be done in cleaner & easier way. Third file shows an implementation of decimal_number_field, which I use in my views.

The other thing I want to mention is that I have to always truncate my decimals when saving, instead of rounding them if they are longer, than defined “scale”. I found out that “The precise behavior is operating system-specific, but generally the effect is truncation to the permissible number of digits.” (from Stack Overflow), but unfortunately, on my system default behaviour was rounding a number, so I decided to create an observer.

def before_save(model)
  model.class.columns.each do |column|
    if column.type == :decimal
      int_part, frac_part = model.send(column.name).to_s.split(".")
      new_value = [int_part, frac_part[0...column.scale]].join(".")
      model.send("#{column.name}=", new_value)
    end
  end
end

And that’s all. It’s my first english post and I hope it isn’t as horrible as I think it is.