, como todo rubyfan decidi hacerlo en ruby, pero ruby solo tiene un tipo de dato de punto flotante, llamado Float, que wrappea el double nativo de C
El truco, como en otros lenguajes con solo una precision de punto flotante, es emularlo; es decir, truncarlo a la precision deseada despues de cada operacion. Ruby tiene soluciones interesantes para hacer este tipo de cosas.
El decorator
En un principio, usamos un patron llamado decorator, con la idea de modificar el comportamiento del float:
Nuestro FloatDecorator repite cada operacion en el objeto "decorado", pero al retornar el resultado lo trunca y lo decora tambien asi mantiene el comportamiento
Asi, la raiz cuadrada de dos 2**0.5 da 1.41 y no 1.4142135623730951
La manera mas rubyway de implementar un decorator es usando method_missing En este caso se implementaria asi:
class FloatPrecisionDecorator def initialize(inner, factor) @factor = factor @inner = inner.to_f end def method_missing(m,*x) # todas las operaciones sobre el numero se ejecutan sobre el float verdadero # y se obtiene el verdadero resultado con precision completa del Float original verdadero_resultado = @inner.send(m,*x) # se reduce la precision, multiplicando por el factor, redondeando y volviendo a dividir reduced = (verdadero_resultado.to_f * @factor).round.to_f / @factor FloatPrecisionDecorator.new(reduced, @factor) end end # esto da un FloatPrecisionDecorator con un 1.41, adentro, no un 1.4142135623730951 p FloatPrecisionDecorator.new(2,100)**0.5
Inspect
Pero cuando se hace el print por salida estandar, aparece algo asi:
#..floatprecisiondecorator:0x8cd63f8 factor="10," inner="1.4"...
¿ No seria mejor que simplemente mostrara el 1.41 ? Para eso hay que sobrecargar el metodo inspect
class FloatPrecisionDecorator def inspect # para que llame al inspect del float decorado @inner.inspect end end # ahora si va a mostra simplemente 1.41 p FloatPrecisionDecorator.new(2,100)**0.5
Numeric
Es mejor si se puede hacer asi:
4.0.to_reduced_precision(:decimals => 2)
4.0.to_rp(:decimals => 2)
Para eso lo mejor es agregarle el metodo a la clase numeric:
class Numeric def self.reduce_precision(number, factor) (number.to_f * factor).round.to_f / factor end def to_single_precision(options) unless options[:factor] options[:factor] = (options[:base]||10) ** (options[:decimals]||10) end factor = options[:factor] FloatPrecisionDecorator.new(Numeric.reduce_precision(to_f, factor), factor) end end
Coerce
Puede surgir que se tenga que sumar un numero de precision reducida a un float, pero el metodo + de la clase Float de ruby no tiene forma de saber como sumar el numero que implementamos nosotros. Esos casos ruby lo contempla con el metodo coerce, que habilita conmutar los numeros en una operacion matematica, asi:
class FloatPrecisionDecorator def coerce(other) return self, other end end
inifinite?, nan? y demas
Si se hace a nuestro numero
p 2.to_sp(:decimals => 10).infinite?
Va a devolver 0.0, cuando se supone que el metodo inifnite debe devolver true o false, esto ocurre porque inifnite? tambien llama a method_missing y este trata de truncar y wrappear lo que sea, lo mejor en estos casos es evitar modificar objetos que no sean numeros. Habra que modificar method_missing:
class FloatPrecisionDecorator def method_missing(m,*x) # todas las operaciones sobre el numero se ejecutan sobre el float verdadero # y se obtiene el verdadero resultado con precision completa del Float original verdadero_resultado = @inner.send(m,*x) # si es numeric, truncar y wrappear if Numeric === verdadero_resultado # se reduce la precision, multiplicando por el factor, redondeando y volviendo a dividir reduced = (verdadero_resultado.to_f * @factor).round.to_f / @factor FloatPrecisionDecorator.new(reduced, @factor) else # si no, devolve el resultado como es verdadero_resultado end end end
Codigo completo
class Numeric def self.reduce_precision(number, factor) (number.to_f * factor).round.to_f / factor end def to_single_precision(options) unless options[:factor] options[:factor] = (options[:base]||10) ** (options[:decimals]||10) end factor = options[:factor] FloatPrecisionDecorator.new(Numeric.reduce_precision(to_f, factor), factor) end end class FloatPrecisionDecorator def initialize(inner, factor) @factor = factor @inner = inner.to_f end def method_missing(m,*x) # todas las operaciones sobre el numero se ejecutan sobre el float verdadero # y se obtiene el verdadero resultado con precision completa del Float original verdadero_resultado = @inner.send(m,*x) # si es numeric, truncar y wrappear if Numeric === verdadero_resultado # se reduce la precision, multiplicando por el factor, redondeando y volviendo a dividir reduced = (verdadero_resultado.to_f * @factor).round.to_f / @factor FloatPrecisionDecorator.new(reduced, @factor) else # si no, devolve el resultado como es verdadero_resultado end end def coerce(other) return self, other end def inspect # para que llame al inspect del float decorado @inner.inspect end end p FloatPrecisionDecorator.new(2,10).infinite?