Dwayne Macgowan Instructor del Método DeRose
Para conocer el Método DeRose visitá:
Blog de DeRose |
|
Hola,
Tengo un formulario donde el usuario puede introducir un importe pero necesito que independientemente del formato de separador de decimales que se utilice ya sea , o . mi modelo lo acepte.
Ahora mismo cuando utilizo un . lo coge perfectamente pero si utilizo una coma me da una excepción
También quiero poder aceptar .29
La mejor forma es utilizar la internalización?
Mi modelo lo tengo definido asi:
field :amount, :type => Floatvalidates_presence_of :amountvalidates_numericality_of :amount, :greater_than => 0
Gracias--
Jose
twitter: @jomarmen
2012/3/9 Jose Vicente Martinez Mendoza <joma...@gmail.com>:
>
> Tengo un formulario donde el usuario puede introducir un importe pero
> necesito que independientemente del formato de separador de decimales que se
> utilice ya sea , o . mi modelo lo acepte.
>
> Ahora mismo cuando utilizo un . lo coge perfectamente pero si utilizo una
> coma me da una excepción
>
> También quiero poder aceptar .29
>
> La mejor forma es utilizar la internalización?
Lo mejor es que el modelo no tenga la responsabilidad de convertir
distintos valores a la representación que quiere guardar. En el lugar
en donde recibís los datos del formulario deberías tener una función
que convierta el string que envía el usuario en un valor que acepte tu
modelo. Yo usaría algo como Bureaucrat o Scrivener.
https://github.com/tizoc/bureaucrat
https://github.com/soveran/scrivener
> Mi modelo lo tengo definido asi:
>
> field :amount, :type => Float
> validates_presence_of :amount
> validates_numericality_of :amount, :greater_than => 0
Hay que tener mucho cuidado con los floats porque son muy imprecisos.
Si vas a operar con `amount`, entonces te recomiendo que o bien uses
enteros (la parte decimal pasa a ser una formalidad en la
presentación), o bien que uses BigDecimal de la standard library (esto
es lo que estoy haciendo en un proyecto).
Te paso un ejemplo: https://gist.github.com/25263a383fc0bf4b2d0e
Saludos.
Ese código tan simple como lo ves, tiene una falla grande, y es que
acepta valores que ni siquiera son válidos.
Lo correcto es usar Float(valor) y no valor.to_f
Dejando de lado eso, las conversiones implícitas, aunque parezcan
convenientes hacen todo más complicado, agregan más complejidad y más
casos borde que usar los tipos correctos donde van.
--
BD
Así lo hace Bureaucrat:
def to_object(value)
if Validators.empty_value?(value)
return nil
end
begin
Utils.make_float(value.to_s)
rescue ArgumentError
raise ValidationError.new(error_messages[:invalid])
end
end
y make_float
def make_float(value)
value += '0' if value.is_a?(String) && value != '.' && value[-1,1] == '.'
Float(value)
end
make_float es más complicado de lo que se necesita para Ruby 1.9, la
razón por la cual ese código es así es que cuando se hizo Bureaucrat
la versión de Ruby que se usaba en ese momento era 1.8.6 (por eso el
slice para mirar el ".", y por eso el tener que prefijar un "0" en
algunos casos para que se parsee bien).
En fin, no todo siempre es tan simple/fácil como parece, siempre hay
alguna molestia de costado que no estamos considerando hasta que es
demasiado tarde.
Igual estoy de acuerdo con que agregar una librería entera solo para
manejar un solo caso es overkill. La idea de Bureaucrat o Scrivener no
es resolver un caso puntual, si no cambiar la manera en que se
resuelve el proyecto en su totalidad al desacoplar las partes y
separar las responsabilidades.
--
BD
Al margen de lo que dijo Bruno, es raro que desde la consola quieras
hacer foo.amount = "2,442".
Además, la capa en donde se realiza la transformación también la podés
usar desde la consola o desde un script.
> Son 4 líneas de código. De la otra manera terminás requiriendo una gema,
> escribiendo nuevas clases y demás.
La cantidad de líneas de código no tiene nada que ver con determinar
de quién es la responsabilidad de hacer esa transformación. Con tu
argumento, se podría decir que es mejor hacer una aplicación
monolítica sin reutilizar otras herramientas.
>
> KISS :-)
Con esto estás diciendo que tu solución es más simple, pero en
realidad yo veo todo lo contrario. Un modelo con un atributo ahora se
tiene que ocupar de validar lo que manda un usuario desde un
formulario. Ahora ese modelo es "complected". Te recomiendo que veas
todo lo que fuimos linkeando sobre simplicidad, sobre todo Simple vs
Easy: http://www.infoq.com/presentations/Simple-Made-Easy
amount = 2.40 # => 2.4
amount = "2.40" # => 2.4
amount = "2,40") # => 2.4
amount = "2000,40") # => 2000.4
amount = "2,000.40") # => 2.0
amount = "$2000.40" # => 0.0
Excepto por la "pequeña diferencia" de:
irb(main):001:0> nil.to_f
=> 0.0
irb(main):002:0> Float(nil)
TypeError: can't convert nil into Float
from (irb):2:in `Float'
from (irb):2
from /Users/foca/.rbenv/versions/1.9.3-p0-perf/bin/irb:12:in `<main>'
o
irb(main):003:0> "pepe".to_f
=> 0.0
irb(main):004:0> Float("pepe")
ArgumentError: invalid value for Float(): "pepe"
from (irb):4:in `Float'
from (irb):4
from /Users/foca/.rbenv/versions/1.9.3-p0-perf/bin/irb:12:in `<main>'
Es una diferencia importante. Float() es más seguro que #to_f.
-foca
Estás mezclando conceptos. Por un lado, está el tema de no delegar en
el modelo la responsabilidad de convertir el valor que manda el
usuario desde un formulario. Por otro lado, está la sugerencia usar
otra herramienta para abstraer ese problema definitivamente. Nadie te
está obligando a instalar una gema, aprender a usarla, etc. Eso es una
sugerencia que podés ignorar tranquilamente, como bien quedó
demostrado.
> Qué valores inválidos acepta lo que definí? Es cierto igual que tendría que
> haber usado Float(), aunque la documentación de ese método es:
>
> Returns arg converted to a float. Numeric types are converted directly, the
> rest are converted using arg.to_f. As of Ruby 1.8, converting nil generates
> a TypeError.
>
> Así que básicamente se llama a to_f.
>
> Resulta mucho más fácil de testear también:
>
> it "assigns float with comma" do
> model.amount = "2,5"
> model.amount.should eq(2.5)
> end
Como dice Mencken: "For every problem there is a solution which is
simple, clean and wrong."
Fijate los casos que plantearon Matías y Foca.
Como ya explicó foca, no son iguales. to_f es mucho más permisivo, no
valida el input mientras que Float si.
> Resulta mucho más fácil de testear también:
>
> it "assigns float with comma" do
> model.amount = "2,5"
> model.amount.should eq(2.5)
> end
>
Y apesar de que hiciste ese test, no fue suficiente como ya mostró
Matías. El problema, aunque básico, es mucho más complicado de lo que
parece a primera vista.
Ahora, viendo lo complejo que se vuelve algo tan básico como querer
aceptar números con comas, yo quiero que pienses y te imagines la
complejidad que le agrega esto tanto al ORM como tus modelos por
querer hacer todo en el mismo lugar en ves de separar las
responsabilidades en abstracciones separadas según corresponda.
--
BD
La respuesta fue: hacerlo fuera del modelo, para evitar hacer
validaciones y conversiones de input de usuario a nivel de modelo, ya
que no es bueno hacelro ahí. Se nombraron 2 librerías que ya hacen
esto. El consejo no se trataba de las librerías en si, se trataba de
no meter esta lógica en el modelo, nada más ni nada menos.
A esto se le sumó el consejo de usar BigDecimal por los problemas de
presición con los cuales nos podemos encontrar al usar floats.
Ahora, respecto a usar Bureaucrat o Scrivener, vos pensá que no solo
le van a resolver este caso puntual que tiene, si no que van a hacer
algo bueno por la estructura de su programa en general. Así como en
este caso puntual lo que estaba haciendo era esto de los números con
coma, yo me imagino que su programa no es solo esto (un form con un
solo input donde hay que aceptar un número), si no que hay más cosas.
Si ese es el caso, que es lo más probable, de seguro le van a servir
las librerías (sobretodo Scrivener, porque Bureaucrat probablemente
hace bastante más de lo que espera)
> Yo digo: definé el método "amount=" y gasto 1 segundo de mi tiempo en
> proveer un ejemplo de implementación. Jamás fue mi intención que ese ejemplo
> funcione correctamente. Está claro que sólo requiriendo bureaucrat no se van
> a empezar a aceptar mágicamente los strings en esos formatos.
>
La crítica a tu ejemplo, más allá de subestimar el problema y por ende
cometer errores, está en que decidiste acoplar eso al modelo, en ves
de definir un componente externo (un módulo con helpers, una clase
para validar datos, lo que sea) para que se encargue de esto.
Dados los últimos eventos donde se mostró que desde el sitio más choto
hasta el sitio más grande de Rails tiene una falla de seguridad muy
boluda por estos acoples que introduce Rails dándole demasiada
responsabilidad al modelo, creo que queda claro que no es bueno, y no
tenés que estar tomando mi palabra, solo observar.
> (espero que nadie esté enojado con esta discución, en los emails se suelen
> perder las caras y las emociones... paz :-P)
>
> Creo que mientras menos código haya en un programa, más fácil es entenderlo
> (mientras no esté todo ofuscado). Sin requerir ninguna gema y agregando el
> método "amount=", les puedo asegurar que la cantidad del código final es
> mucho menor. Por ende, el proyecto es más entendible.
>
Si. Igual, cuando yo era chico era de creer que menos código equivalía
a más simple, con el tiempo me di cuenta de que esto no es cierto. Un
diseño simple va en tener partes que son fáciles de entender, porque
hacen poco. Estas partes se tienen que combinar bien entre si,
tratando siempre de mantener su simpleza, que aunque puede llegar a
estar relacionada con la cantidad de código, no es necesariamente así.
Es mucho más fácil de entender un componente con unas pocas
responsabilidades que uno con muchas. Entender algo con 4
responsabilidades no es el doble de dificil que entender algo con 2
responsabilidades, es como 5 veces más dificil.
Volviendo a los modelos de ActiveRecord, qué tan bien me decís que los
entendés? A esto sumale los "plugins" que se meten por dentro para
agregar más cosas y hasta cambian las reglas.
> De todas maneras es una creencia mía. No nos olvidemos del mail de Emmanuel
> (defocus), no hice ninguna estadística para demostrarlo. :-)
>
> En general, me gusta que los métodos acepten "lo que venga mientras tenga
> sentido". Pueden ver que eso es cierto en muchas clases core de ruby, y
> hasta en active record (Model.find(1) == Model.find("1")). Por eso me
> pareció sensato que amount=(value) acepte los valores en el formato que
> venga, mientras tenga sentido. Así no hay que repetir la lógica de
> conversión por todas partes (acordarse de ponerla en tal formulario, en tal
> otro).
>
Es conveniente para casos chicos, a medida que tu programa va
creciendo se vuelve un problema. Algo que me molesta de Javascript y
Perl son las coerciones automáticas entre tipos (sumar un número con
una cadena es el caso más común). A primera vista parece simpático y
conveniente, y para cosas chicas la comodidad gana. Ahora, cuando el
programa crece esto se vuelve problemático, agrega montones de casos
borde que antes no tenías, cosas que fallan silenciosamente y por ende
son muy difíciles de depurar.
Si tomás en cuenta que la mayoría del tiempo la pasamos depurando (lo
que incliuye probar que las cosas funcionan, no solo arreglar bugs) y
no escribiendo el código, lo mejor que podemos hacer es no agregar
"magia" que nos dificulte esto.
Ruby (por suerte) no hace coerción automática entre tipos (bueno,
mayormente), no la introduzcamos nosotros.
--
BD
No está mal que el modelo valide, es útil para mantener la
consistencia de los datos. El problema es cuando ese modelo implementa
validaciones que son particulares a una interacción particular (los
"workflows" supongo, como decía foca en otro thread).
Mi ORM preferido es SQLAlchemy, y no implementa validaciones como lo
hacen los ORMs que nombraste. Te permite usar código arbitrario para
fijarse que el valor de un field sea válido, pero la mayoría de las
validaciones se hacen a nivel de la base de datos usando constraints
(que podés declararlos desde SQLAlchemy)
http://docs.sqlalchemy.org/en/latest/orm/mapper_config.html#simple-validators
Tampoco implementa coerción automática de tipos, pero si querés
hacerlo, va por tu cuenta. Lo podés hacés creando readers/writers
custom:
http://docs.sqlalchemy.org/en/latest/orm/mapper_config.html#using-descriptors-and-hybrids
O especificando tus propias reglas en base a TypeDecorator. No hace
nada de eso por default porque generalmente no es una buena idea. Si
lo llegás a necesitar probablemente sea porque tenés algún tipo custom
a nivel del lenguaje y querés mapearlo a algo en la base de datos, lo
cual es muy distinto a andar haciendo mapeos automáticos entre cadenas
y números.
--
BD
O sea, si
yo genero un modelo y alguien mas
tiene que reutilizarlo debo estar seguro que no olvide pasar por la
clase "intermedia" que se encarga de
las validaciones.
Lo habíamos hablado en otro thread:
https://groups.google.com/d/msg/rubysur/bDYw3Isi9QU/FqSA_oQLoZ8J
En resumen, se hablaba de tres responsabilidades: integridad de datos,
transformaciones e interacciones.
Creo que esto es cultural, pero es un hábito fácil de cambiar.
> Ahora, porque si es una mala práctica delegar la responsabilidad de la
> validación a los modelos casi todos los
> ORM implementan validación? (model de sequel, datamapper,
> ActiveRecord)
Creo que es por un error que venimos arrastrando desde hace años en el
mundo Ruby.
En DBI sólo era posible declarar tipos, pero nada de validaciones.
Según recuerdo, el primer ORM que hubo en Ruby fue Lafcadio
(http://lafcadio.rubyforge.org/manual.html#id801069), y copiaba de DBI
el tema de los tipos, pero tampoco tenía validaciones. Después salió
Og (está abandonado, pueden leer algo acá:
http://en.wikipedia.org/wiki/Nitro_%28web_framework%29), que es como
el ancestro conceptual de DataMapper. Las primeras versiones
(pre-ActiveRecord) no tenían validaciones, pero apenas salió
ActiveRecord copiaron el formato (class level validations). A partir
de ahí, todos los ORM repitieron ese estilo, hasta que Sequel
implementó las validaciones a nivel de instancia. Ohm hizo lo mismo un
tiempo después.
Ohm incluía el módulo Ohm::Validations y así lo usamos durante años.
Bruno lanzó Bureaucrat a fines del 2009 y se usó en muchos proyectos.
En otros, insistíamos con especificar las interacciones o bien en la
validación de cada modelo (con if/else), o con whitelists al recibir
los parámetros (por ejemplo, hace tres años un colega de Citrusbyte
publicó esta gema: https://github.com/citrusbyte/whitelist).
A mediados del año pasado por fin me convencí de que usar Bureaucrat
era lo correcto, y armé Scrivener para mover ahí todas las
validaciones que estaban en Ohm. La nueva versión de Ohm, que va a
salir dentro de poco tiempo, ya no define validaciones sino que
incluye las de Scrivener. Desde la documentación vamos a sugerir usar
Scrivener directamente.
En Django este problema se solucionó de otra forma desde el principio.
Me parece que las validaciones a nivel de clase que propuso
ActiveRecord hace casi ocho años resultaron mucho más interesantes que
las otras propuestas, y el problema fue que los puntos flojos no eran
fáciles de detectar. Aún hoy a muchos les puede parecer la mejor
opción, con un costo bajo que es lidiar con los casos en donde la
aplicación directa no es posible (mass assignments, roles, etc.).
En fin, esto es más o menos lo que recuerdo.