18

What's the best Ruby/Rails way to allow users to use decimals or commas when entering a number into a form? In other words, I would like the user be able to enter 2,000.99 and not get 2.00 in my database.

Is there a best practice for this?

Does gsub work with floats or bigintegers? Or does rails automatically cut the number off at the , when entering floats or ints into a form? I tried using self.price.gsub(",", "") but get "undefined method `gsub' for 8:Fixnum" where 8 is whatever number I entered in the form.

VLAZ
  • 26,331
  • 9
  • 49
  • 67
James
  • 181
  • 1
  • 1
  • 3

8 Answers8

27

I had a similar problem trying to use localized content inside forms. Localizing output is relatively simple using ActionView::Helpers::NumberHelper built-in methods, but parsing localized input it is not supported by ActiveRecord.

This is my solution, please, tell me if I'm doing anything wrong. It seems to me too simple to be the right solution. Thanks! :)

First of all, let's add a method to String.

class String
  def to_delocalized_decimal
    delimiter = I18n::t('number.format.delimiter')
    separator = I18n::t('number.format.separator')
    self.gsub(/[#{delimiter}#{separator}]/, delimiter => '', separator => '.')
  end
end

Then let's add a class method to ActiveRecord::Base

class ActiveRecord::Base
  def self.attr_localized(*fields)
    fields.each do |field|
      define_method("#{field}=") do |value|
        self[field] = value.is_a?(String) ? value.to_delocalized_decimal : value
      end
    end
  end
end

Finally, let's declare what fields should have an input localized.

class Article < ActiveRecord::Base
  attr_localized :price
end

Now, in your form you can enter "1.936,27" and ActiveRecord will not raise errors on invalid number, because it becomes 1936.27.

panteo
  • 720
  • 8
  • 12
10

Here's some code I copied from Greg Brown (author of Ruby Best Practices) a few years back. In your model, you identify which items are "humanized".

class LineItem < ActiveRecord::Base
  humanized_integer_accessor :quantity
  humanized_money_accessor :price
end

In your view templates, you need to reference the humanized fields:

= form_for @line_item do |f|
  Price:
  = f.text_field :price_humanized

This is driven by the following:

class ActiveRecord::Base
  def self.humanized_integer_accessor(*fields)
    fields.each do |f|
      define_method("#{f}_humanized") do
        val = read_attribute(f)
        val ? val.to_i.with_commas : nil
      end
      define_method("#{f}_humanized=") do |e|
        write_attribute(f,e.to_s.delete(","))
      end
    end
  end

  def self.humanized_float_accessor(*fields)
    fields.each do |f|
      define_method("#{f}_humanized") do
        val = read_attribute(f)
        val ? val.to_f.with_commas : nil
      end
      define_method("#{f}_humanized=") do |e|
        write_attribute(f,e.to_s.delete(","))
      end
    end
  end

  def self.humanized_money_accessor(*fields)
    fields.each do |f|
      define_method("#{f}_humanized") do
        val = read_attribute(f)
        val ? ("$" + val.to_f.with_commas) : nil
      end
      define_method("#{f}_humanized=") do |e|
        write_attribute(f,e.to_s.delete(",$"))
      end
    end
  end
end
Jeremy Weathers
  • 2,556
  • 1
  • 16
  • 24
  • What's the `with_commas` method referenced here? – Tinynumbers Oct 15 '14 at 12:04
  • That was a custom extension in the app I extracted this code from - I forgot it wasn't included in Rails: `class String def with_commas self.reverse.gsub(/\d{3}/,"\\&,").reverse.sub(/^,/,"") end class Numeric def with_commas to_s.with_commas end end` – Jeremy Weathers Oct 15 '14 at 18:13
2

You can try stripping out the commas before_validation or before_save

Oops, you want to do that on the text field before it gets converted. You can use a virtual attribute:

def price=(price)
   price = price.gsub(",", "")
   self[:price] = price  # or perhaps price.to_f
end
Dex
  • 12,527
  • 15
  • 69
  • 90
  • This is an infinite loop. `self.price = price` will call `price=`. I would recommend doing `self[:price] = price` instead. – epochwolf May 07 '12 at 18:49
0

Take a look at the i18n_alchemy gem for date & number parsing and localization.

I18nAlchemy aims to handle date, time and number parsing, based on current I18n locale format. The main idea is to have ORMs, such as ActiveRecord for now, to automatically accept dates/numbers given in the current locale format, and return these values localized as well.

Thomas Klemm
  • 10,678
  • 1
  • 51
  • 54
0

I have written following code in my project. This solved all of my problems.

config/initializers/decimal_with_comma.rb

# frozen_string_literal: true

module ActiveRecord
  module Type
    class Decimal
      private

      alias_method :cast_value_without_comma_separator, :cast_value

      def cast_value(value)
        value = value.gsub(',', '') if value.is_a?(::String)
        cast_value_without_comma_separator(value)
      end
    end

    class Float
      private

      alias_method :cast_value_without_comma_separator, :cast_value

      def cast_value(value)
        value = value.gsub(',', '') if value.is_a?(::String)
        cast_value_without_comma_separator(value)
      end
    end

    class Integer
      private

      alias_method :cast_value_without_comma_separator, :cast_value

      def cast_value(value)
        value = value.gsub(',', '') if value.is_a?(::String)
        cast_value_without_comma_separator(value)
      end
    end
  end
end

module ActiveModel
  module Validations
    class NumericalityValidator
      protected

        def parse_raw_value_as_a_number(raw_value)
          raw_value = raw_value.gsub(',', '') if raw_value.is_a?(::String)
          Kernel.Float(raw_value) if raw_value !~ /\A0[xX]/
        end
    end
  end
end
-1

You can try this:

def price=(val)
  val = val.gsub(',', '')
  super
end
Dino
  • 7,779
  • 12
  • 46
  • 85
-1

I was unable to implement the earlier def price=(price) virtual attribute suggestion because the method seems to call itself recursively.

I ended up removing the comma from the attributes hash, since as you suspect ActiveRecord seems to truncate input with commas that gets slotted into DECIMAL fields.

In my model:

before_validation :remove_comma

def remove_comma
  @attributes["current_balance"].gsub!(',', '')  # current_balance here corresponds to the text field input in the form view

  logger.debug "WAS COMMA REMOVED? ==> #{self.current_balance}"
end
Bo Persson
  • 90,663
  • 31
  • 146
  • 203
-1

Here's something simple that makes sure that number input is read correctly. The output will still be with a point instead of a comma. That's not beautiful, but at least not critical in some cases.

It requires one method call in the controller where you want to enable the comma delimiter. Maybe not perfect in terms of MVC but pretty simple, e.g.:

class ProductsController < ApplicationController

  def create
    # correct the comma separation:
    allow_comma(params[:product][:gross_price])

    @product = Product.new(params[:product])

    if @product.save
      redirect_to @product, :notice => 'Product was successfully created.'
    else
      render :action => "new"
    end
  end

end

The idea is to modify the parameter string, e.g.:

class ApplicationController < ActionController::Base

  def allow_comma(number_string)
    number_string.sub!(".", "").sub!(",", ".")
  end

end
bass-t
  • 706
  • 7
  • 8