Ruby Forum Rails-ES > Formatos numéricos

Posted by Alvaro Bautista (alvarobp)
on 09.08.2008 12:37
(Received via mailing list)
Hola,

Seguramente el siguiente problema se habrá discutido en la lista, pues
debe ser bastante 
común.
Mi problema es que en un modelo tengo varios campos de tipo decimal
(precision 12, escala 2, para ser más exactos) y querría permitir que
desde formularios para ese modelo se pudieran ingresar valores usando
tanto "." como "," ("1.2" seria lo mismo que "1,2"). Osea usar o bien
coma como separador decimal, o bien punto.

He estado googleando en busca de una solución y no he encontrado nada.
He estado probando a usar el callback before_validation para convertir
las comas en puntos y es cuando he caido en la cuenta de que cuando la
cadena que llega del formulario se convierte en BigDecimal los digitos
despues de la coma se descartan y se queda con el valor entero.

Entonces lo que se me ocurre es en el método update del controlador
correspondiente comprobar todos las cadenas para esos campos, pero son
unos 10, y aún así si luego los campos cambian (se quitan o se añaden)
debería modificar ese metódo para usar los nuevos campos.

En fin, alguien conoce alguna solución más o menos "elegante" ?

Saludos y gracias de antemano
Posted by Xavier Noria (fxn)
on 09.08.2008 13:18
(Received via mailing list)
2008/8/9 Alvaro Bautista <alvarobp@gmail.com>:

> Seguramente el siguiente problema se habrá discutido en la lista, pues
> debe ser bastante común.
>
> Mi problema es que en un modelo tengo varios campos de tipo decimal
> (precision 12, escala 2, para ser más exactos) y querría permitir que
> desde formularios para ese modelo se pudieran ingresar valores usando
> tanto "." como "," ("1.2" seria lo mismo que "1,2"). Osea usar o bien
> coma como separador decimal, o bien punto.

Para conseguir esto nosotros (ASPgems) hacemos lo siguiente. Definimos

    def l10n_decimal(*syms)
      syms.each do |s|
        class_eval <<-EOS
        before_save do |record|
          if record.#{s}_before_type_cast.is_a?(String)
            record.#{s} = 
MyAppUtils.parse_decimal(record.#{s}_before_type_cast)
          end
        end
        EOS
      end
    end

en un initializer, de manera que las clases declaran

   class Book < AR::Base
     l10n_decimal :price
   end

parse_decimal son unas heuristicas definidas por nosotros que de un
numero en una cadena te sacan algo como sea, no peta nunca, va debajo.

Hay dos gotchas de las heuristicas que aceptamos como trade-off:

* No se puede entrar "1.200" como mil doscientos, porque nuestra
heuristica asume que si hay un solo separador este es decimal.

* La aplicacion en particular no parsea "1.200" como mil doscientos a
pesar de que es como lo escribe en las vistas en castellano.

Como en general la gente no escribe separador de miles a la practica
va bien, tambien puedes asumir que solo van a haber dos decimales, y
en ese caso modificar la heuristica para que si hay 3 interpretes que
el separador es de miles (entonces el trade-off es que "1.20" es
decimal pero "1.200" no). En fin eso ya lo decide uno mismo.

-- fxn


  # Returns a BigDecimal out of the string n, 0.0.to_d on failure.
  def self.parse_decimal(n)
    return 0.0.to_d if n.blank?

    n = n.dup

    # remove everything that cannot be part of a number, as currency
symbols or garbage
    n.gsub!(/[^.,\d]+$/, '')

    ndots = n.count('.')
    ncommas = n.count(',')
    return n.to_d if ndots.zero? && ncommas.zero?

    # if it has a single separator and it is repeated assume it is a
thousands separator
    if (ndots.zero? && ncommas > 1) || (ndots > 1 && ncommas.zero?)
      n.tr!('.,', '')
      return n.to_d
    end

    # if n has no comma and at most one dot delegate and return
    return n.to_d if ncommas.zero?

    # if it has a comma, but no dot, assume it is a decimal separator
    return n.sub(',', '.').to_d if ndots.zero?

    # if we get here it has both a comma and a dot, strip whitespace
    n = n.strip

    # take sign and delete it, if any
    s = n.first == "-" ? -1 : 1
    n.sub!(/^[-+]/, '')

    # extract and remove the decimal part, which is assumed to be the 
one
    # after the rightmost separator, no matter whether it is a comma or 
a dot
    n.sub!(/[.,](\d*)$/, '')
    decimal_part = $1 # perhaps the empty string, no problem

    # in what remains, which is taken as the integer part, any non-digit 
is
    # simply ignored
    n.gsub!(/\D/, '')

    # done
    return s*("#{n}.#{decimal_part}".to_d)
  end
Posted by Alvaro Bautista (alvarobp)
on 09.08.2008 14:15
(Received via mailing list)
Muchas gracias Xavier!

He implementado tu solución con un par de cambios:

En lugar de usar el callback before_save en el metodo l10n_decimal
utilizo before_validation. Así cambiando esta línea:

n.gsub!(/[^.,\d]+$/, '')

por esta

return if n =~ /[^.,\d]/

del parse_decimal, puedo comprobar que es un número con
validates_numericality_of. Pero todo porque yo lo quiero 
así.
Tu solución me ha venido perfecta.

De nuevo, muchas gracias.
Posted by Xavier Noria (fxn)
on 09.08.2008 14:33
(Received via mailing list)
2008/8/9 Alvaro Bautista <alvarobp@gmail.com>:

> En lugar de usar el callback before_save en el metodo l10n_decimal
> utilizo before_validation. Así cambiando esta línea:
>
> n.gsub!(/[^.,\d]+$/, '')
>
> por esta
>
> return if n =~ /[^.,\d]/

Molt be! Ojo que ahi no aceptas negativos (igual ya es lo que quieres,
pero just in case).