2009-06-23 18 views
28

¿Existe alguna manera de simplemente verificar si un valor de cadena es un valor flotante válido? Llamar a_f en una cadena la convertirá a 0.0 si no es un valor numérico. Y el uso de Float() genera una excepción cuando se pasa una cadena flotante no válida que está más cerca de lo que quiero, pero no quiero manejar excepciones de captura. Lo que realmente quiero es un método como Nan? que existe en la clase Float, pero eso no ayuda porque una cadena no numérica no se puede convertir en un flotante sin cambiarse a 0.0 (usando to_f).Determine si una cadena es un valor flotante válido

"a".to_f => 0.0 

"a".to_f.nan? => false 

Float("a") => ArgumentError: invalid value for Float(): "a" 

¿Hay una solución simple para esto o necesito para escribir código para comprobar si una cadena es un valor flotante válido?

Respuesta

26

Un hecho interesante sobre el mundo Ruby es la existencia del proyecto Rubinius, que implementa Ruby y su biblioteca estándar sobre todo en Ruby puro. Como resultado, tienen una implementación de Ruby puro del Kernel # flotador, que se parece:

def Float(obj) 
    raise TypeError, "can't convert nil into Float" if obj.nil? 

    if obj.is_a?(String) 
    if obj !~ /^\s*[+-]?((\d+_?)*\d+(\.(\d+_?)*\d+)?|\.(\d+_?)*\d+)(\s*|([eE][+-]?(\d+_?)*\d+)\s*)$/ 
     raise ArgumentError, "invalid value for Float(): #{obj.inspect}" 
    end 
    end 

    Type.coerce_to(obj, Float, :to_f) 
end 

Esto le proporciona una expresión regular que coincide con el trabajo interno de Ruby hace cuando se ejecuta Float(), pero sin la excepción. Por lo que ahora podría hacer:

class String 
    def nan? 
    self !~ /^\s*[+-]?((\d+_?)*\d+(\.(\d+_?)*\d+)?|\.(\d+_?)*\d+)(\s*|([eE][+-]?(\d+_?)*\d+)\s*)$/ 
    end 
end 

Lo bueno de esta solución es que desde Rubinius corre, y pasa RubySpec, sabes que esto expresiones regulares se encarga de las de borde casos que la propia Rubí maneja, y se le puede llamar to_f en el Cadena sin ningún miedo!

+0

¡Excelente respuesta! Nota: Esta expresión regular ha evolucionado un poco en la implementación de Rubinius, consulte las especificaciones en https://github.com/rubinius/rubinius/blob/master/spec/ruby/core/string/to_f_spec.rb para obtener más información. Tenga en cuenta también que si está utilizando esto para validar la entrada del usuario, es posible que desee omitir el soporte para guiones bajos y simplemente usar la expresión regular de Rubinius como inspiración :) – captainpete

+0

Corrección, Rubinius todavía usa la misma expresión regular para Float(). Encuentre el código en https://github.com/rubinius/rubinius/blob/master/kernel/common/kernel19.rb – captainpete

34

Aquí hay una manera:

class String 
    def valid_float? 
    # The double negation turns this into an actual boolean true - if you're 
    # okay with "truthy" values (like 0.0), you can remove it. 
    !!Float(self) rescue false 
    end 
end 

"a".valid_float? #false 
"2.4".valid_float? #true 

Si se quiere evitar el mono-trozo de cuerda, siempre se puede hacer de este un método de la clase de algún módulo a controlar, por supuesto:

module MyUtils 
    def self.valid_float?(str) 
    !!Float(str) rescue false 
    end 
end 
MyUtils.valid_float?("a") #false 
+0

No deberías evitar el uso de rescate en su forma modificador? – Benjamin

+1

Buena respuesta. Como una pequeña sugerencia, yo evitaría el uso de la doble negación como se sugiere [aquí] (http://www.rubydoc.info/github/bbatsov/rubocop/Rubocop/Cop/Style/DoubleNegation) –

1

Intenté agregar esto como un comentario, pero aparentemente no hay formateo en los comentarios:

por otro lado, ¿por qué no usar eso como su función de conversión, como

class String 
    def to_float 
    Float self rescue (0.0/0.0) 
    end 
end 
"a".to_float.nan? => true 

que por supuesto es lo que no quería hacer en primer lugar. Supongo que la respuesta es: "tienes que escribir tu propia función si realmente no quieres usar el manejo de excepciones, pero, ¿por qué harías eso?"

+1

Sólo quería ser claro que el uso de 0.0/0.0 es un truco sucio, pero si quieres obtener NaN, actualmente es la única forma (que yo sepa). Si fuera mi programa, consideraría usar nil en su lugar. – Sam

3

Umm, si usted no quiere excepciones entonces tal vez:

 
def is_float?(fl) 
    fl =~ /(^(\d+)(\.)?(\d+)?)|(^(\d+)?(\.)(\d+))/ 
end 

Desde OP preguntó específicamente para una solución sin excepciones. solución basada en la expresión regular es ligeramente lento:

 
require "benchmark" 
n = 500000 

def is_float?(fl) 
    !!Float(fl) rescue false 
end 

def is_float_reg(fl) 
    fl =~ /(^(\d+)(\.)?(\d+)?)|(^(\d+)?(\.)(\d+))/ 
end 

Benchmark.bm(7) do |x| 
    x.report("Using cast") { 
    n.times do |i| 
     temp_fl = "#{i + 0.5}" 
     is_float?(temp_fl) 
    end 
    } 
    x.report("using regexp") { 
    n.times do |i| 
     temp_fl = "#{i + 0.5}" 
     is_float_reg(temp_fl) 
    end 
    } 
end 

Resultados:

 
5286 snippets:master!? % 
      user  system  total  real 
Using cast 3.000000 0.000000 3.000000 ( 3.010926) 
using regexp 5.020000 0.000000 5.020000 ( 5.021762) 
+0

¿El Float no es una rutina nativa mientras que la expresión regular es mucho más lenta? –

+0

"La solución basada en Regexp es marginalmente lenta": verifique nuevamente los números 3/5 equivale al 60%. No llamaría perder un 40% como una caída marginal. –

+0

Además, tenga en cuenta que si su conversión generará excepciones con mayor frecuencia que la anterior, será mucho más lenta que la expresión regular. Esto se debe a que rescatar de una excepción es muy lento, como se muestra aquí: http://www.simonecarletti.com/blog/2010/01/how-slow-are-ruby-exceptions/ –

9
# Edge Cases: 
# numeric?"Infinity" => true is_numeric?"Infinity" => false 


def numeric?(object) 
true if Float(object) rescue false 
end 

#Possibly faster alternative 
def is_numeric?(i) 
i.to_i.to_s == i || i.to_f.to_s == i 
end 
+1

NB la 'alternativa más rápida' devolverá falso para '5.00' – Lambart

3

vi el debate no resuelto sobre el modelo + excepciones vs expresiones regulares y pensé que iba a tratar de referencia todo y producir una respuesta objetiva:

Aquí es la fuente para el mejor de los casos y lo peor de cada método intentado aquí:

require "benchmark" 
n = 500000 

def is_float?(fl) 
    !!Float(fl) rescue false 
end 

def is_float_reg(fl) 
    fl =~ /(^(\d+)(\.)?(\d+)?)|(^(\d+)?(\.)(\d+))/ 
end 

class String 
    def to_float 
    Float self rescue (0.0/0.0) 
    end 
end 


Benchmark.bm(7) do |x| 
    x.report("Using cast best case") { 
    n.times do |i| 
     temp_fl = "#{i + 0.5}" 
     is_float?(temp_fl) 
    end 
    } 
    x.report("Using cast worst case") { 
    n.times do |i| 
     temp_fl = "asdf#{i + 0.5}" 
     is_float?(temp_fl) 
    end 
    } 
    x.report("Using cast2 best case") { 
    n.times do |i| 
     "#{i + 0.5}".to_float 
    end 
    } 
    x.report("Using cast2 worst case") { 
    n.times do |i| 
     "asdf#{i + 0.5}".to_float 
    end 
    } 
    x.report("Using regexp short") { 
    n.times do |i| 
     temp_fl = "#{i + 0.5}" 
     is_float_reg(temp_fl) 
    end 
    } 
    x.report("Using regexp long") { 
    n.times do |i| 
     temp_fl = "12340918234981234#{i + 0.5}" 
     is_float_reg(temp_fl) 
    end 
    } 
    x.report("Using regexp short fail") { 
    n.times do |i| 
     temp_fl = "asdf#{i + 0.5}" 
     is_float_reg(temp_fl) 
    end 
    } 
    x.report("Using regexp long fail") { 
    n.times do |i| 
     temp_fl = "12340918234981234#{i + 0.5}asdf" 
     is_float_reg(temp_fl) 
    end 
    } 

end 

con los siguientes resultados con mri193:

   user  system  total  real 
Using cast best case 0.608000 0.000000 0.608000 ( 0.615000) 
Using cast worst case 5.647000 0.094000 5.741000 ( 5.745000) 
Using cast2 best case 0.593000 0.000000 0.593000 ( 0.586000) 
Using cast2 worst case 5.788000 0.047000 5.835000 ( 5.839000) 
Using regexp short 0.951000 0.000000 0.951000 ( 0.952000) 
Using regexp long 1.217000 0.000000 1.217000 ( 1.214000) 
Using regexp short fail 1.201000 0.000000 1.201000 ( 1.202000) 
Using regexp long fail 1.295000 0.000000 1.295000 ( 1.284000) 

Dado que se trata de sólo algoritmos de tiempo lineal creo que utilizamos mediciones empíricas para hacer generalizaciones. Es fácil ver que la expresión regular es más consistente y solo fluctuará un poco en función de la longitud de la secuencia que se transmita. El lanzamiento es claramente más rápido cuando no hay falla, y mucho más lento cuando hay fallas.

Si comparamos los tiempos de éxito, podemos ver que el mejor de los casos fundido es de aproximadamente 0,3 segundos más rápido que el mejor de los casos de expresiones regulares. Si dividimos esto por la cantidad de tiempo en el caso peor de los casos se puede estimar el número de carreras que se necesitaría para romper incluso con excepciones ralentizar el arrojado a igualar las velocidades de expresiones regulares. Alrededor de 6 segundos buceados por .3 nos da alrededor de 20. Por lo tanto, si el rendimiento es importante y espera que falle menos de 1 de cada 20 de su prueba, entonces se utilizarán cast + excepciones.

JRuby 1.7.4 tiene resultados completamente diferentes:

   user  system  total  real 
Using cast best case 2.575000 0.000000 2.575000 ( 2.575000) 
Using cast worst case 53.260000 0.000000 53.260000 (53.260000) 
Using cast2 best case 2.375000 0.000000 2.375000 ( 2.375000) 
Using cast2 worst case 53.822000 0.000000 53.822000 (53.822000) 
Using regexp short 2.637000 0.000000 2.637000 ( 2.637000) 
Using regexp long 3.395000 0.000000 3.395000 ( 3.396000) 
Using regexp short fail 3.072000 0.000000 3.072000 ( 3.073000) 
Using regexp long fail 3.375000 0.000000 3.375000 ( 3.374000) 

Cast es sólo marginalmente más rápido en el mejor de los casos (el 10%). Suponiendo que esta diferencia es apropiada para hacer generalizaciones (no creo que sea así), entonces el punto de equilibrio está en algún lugar entre 200 y 250 carreras y solo 1 causa una excepción.

excepciones por lo que sólo deben usarse cuando ocurre cosas realmente excepcionales, esta es una decisión para usted y su código base. Cuando no se usan el código en el que están, puede ser más simple y más rápido.

Si el rendimiento no importa, probablemente debería simplemente siguiendo lo que las convenciones del equipo o de la base de código que ya tiene y pasar por alto todo esto esta respuesta.

2

probar este

def is_float(val) 
    fval = !!Float(val) rescue false 
    # if val is "1.50" for instance 
    # we need to lop off the trailing 0(s) with gsub else no match 
    return fval && Float(val).to_s == val.to_s.gsub(/0+$/,'') ? true:false 
end 

s = "1000" 
is_float s 
=> false 

s = "1.5" 
is_float s 
=> true 

s = "Bob" 
is_float s 
=> false 

n = 1000 
is_float n 
=> false 

n = 1.5 
is_float n 
=> true 
1
def float?(string) 
    true if Float(string) rescue false 
end 

Esto apoya 1.5, 5, 123.456, 1_000 pero no 1 000, 1,000, etc. (por ejemplo, mismo que String#to_f).

>> float?("1.2") 
=> true 
>> float?("1") 
=> true 
>> float?("1 000") 
=> false 
>> float?("abc") 
=> false 
>> float?("1_000") 
=> true 

Fuente: https://github.com/ruby/ruby/blob/trunk/object.c#L2934-L2959

Cuestiones relacionadas