Prueba si la cadena es un número en Ruby on Rails

103

Tengo lo siguiente en mi controlador de aplicación:

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

y la siguiente condición en mi controlador:

if mystring.is_number?

end

La condición arroja un undefined methoderror. ¿Supongo que lo he definido is_numberen el lugar equivocado ...?

Jamie Buchanan
fuente
4
Sé que hay mucha gente aquí debido a la clase Rails for Zombies Testing de codeschool. Solo espera a que siga explicando. Se supone que las pruebas no deben pasar --- está bien que la prueba falle por error, siempre puede parchear rieles para inventar métodos como self.is_number?
boulder_ruby
La respuesta aceptada falla en casos como "1,000" y es 39 veces más lenta que usar un enfoque de expresiones regulares. Vea mi respuesta a continuación.
pthamm

Respuestas:

186

Crear is_number?método.

Crea un método auxiliar:

def is_number? string
  true if Float(string) rescue false
end

Y luego llámalo así:

my_string = '12.34'

is_number?( my_string )
# => true

Ampliar Stringclase.

Si desea poder llamar is_number?directamente a la cadena en lugar de pasarla como un parámetro a su función auxiliar, debe definir is_number?como una extensión de la Stringclase, así:

class String
  def is_number?
    true if Float(self) rescue false
  end
end

Y luego puedes llamarlo con:

my_string.is_number?
# => true
Jakob S
fuente
2
Esta es una mala idea. "330.346.11" .to_f # => 330.346
epochwolf
11
No hay to_fen lo anterior, y Float () no exhibe ese comportamiento: Float("330.346.11")aumentaArgumentError: invalid value for Float(): "330.346.11"
Jakob S
7
Si usa ese parche, lo cambiaría a numérico ?, para mantenerme en línea con las convenciones de nomenclatura de ruby ​​(las clases numéricas heredan de Numeric, los prefijos is_ son javaish).
Konrad Reiche
10
No es realmente relevante para la pregunta original, pero probablemente pondría el código lib/core_ext/string.rb.
Jakob S
1
No creo que el is_number?(string)bit funcione Ruby 1.9. ¿Quizás eso es parte de Rails o 1.8? String.is_a?(Numeric)trabajos. Consulte también stackoverflow.com/questions/2095493/… .
Ross Attrill
30

Aquí hay un punto de referencia para las formas comunes de abordar este problema. Tenga en cuenta cuál debe usar probablemente depende de la proporción de casos falsos esperados.

  1. Si son relativamente poco comunes, el casting es definitivamente más rápido.
  2. Si los casos falsos son comunes y solo está buscando entradas, la comparación frente a un estado transformado es una buena opción.
  3. Si los casos falsos son comunes y está verificando flotantes, regexp probablemente sea el camino a seguir

Si el rendimiento no importa, usa lo que te gusta. :-)

Detalles de verificación de enteros:

# 1.9.3-p448
#
# Calculating -------------------------------------
#                 cast     57485 i/100ms
#            cast fail      5549 i/100ms
#                 to_s     47509 i/100ms
#            to_s fail     50573 i/100ms
#               regexp     45187 i/100ms
#          regexp fail     42566 i/100ms
# -------------------------------------------------
#                 cast  2353703.4 (±4.9%) i/s -   11726940 in   4.998270s
#            cast fail    65590.2 (±4.6%) i/s -     327391 in   5.003511s
#                 to_s  1420892.0 (±6.8%) i/s -    7078841 in   5.011462s
#            to_s fail  1717948.8 (±6.0%) i/s -    8546837 in   4.998672s
#               regexp  1525729.9 (±7.0%) i/s -    7591416 in   5.007105s
#          regexp fail  1154461.1 (±5.5%) i/s -    5788976 in   5.035311s

require 'benchmark/ips'

int = '220000'
bad_int = '22.to.2'

Benchmark.ips do |x|
  x.report('cast') do
    Integer(int) rescue false
  end

  x.report('cast fail') do
    Integer(bad_int) rescue false
  end

  x.report('to_s') do
    int.to_i.to_s == int
  end

  x.report('to_s fail') do
    bad_int.to_i.to_s == bad_int
  end

  x.report('regexp') do
    int =~ /^\d+$/
  end

  x.report('regexp fail') do
    bad_int =~ /^\d+$/
  end
end

Detalles de verificación de flotación:

# 1.9.3-p448
#
# Calculating -------------------------------------
#                 cast     47430 i/100ms
#            cast fail      5023 i/100ms
#                 to_s     27435 i/100ms
#            to_s fail     29609 i/100ms
#               regexp     37620 i/100ms
#          regexp fail     32557 i/100ms
# -------------------------------------------------
#                 cast  2283762.5 (±6.8%) i/s -   11383200 in   5.012934s
#            cast fail    63108.8 (±6.7%) i/s -     316449 in   5.038518s
#                 to_s   593069.3 (±8.8%) i/s -    2962980 in   5.042459s
#            to_s fail   857217.1 (±10.0%) i/s -    4263696 in   5.033024s
#               regexp  1383194.8 (±6.7%) i/s -    6884460 in   5.008275s
#          regexp fail   723390.2 (±5.8%) i/s -    3613827 in   5.016494s

require 'benchmark/ips'

float = '12.2312'
bad_float = '22.to.2'

Benchmark.ips do |x|
  x.report('cast') do
    Float(float) rescue false
  end

  x.report('cast fail') do
    Float(bad_float) rescue false
  end

  x.report('to_s') do
    float.to_f.to_s == float
  end

  x.report('to_s fail') do
    bad_float.to_f.to_s == bad_float
  end

  x.report('regexp') do
    float =~ /^[-+]?[0-9]*\.?[0-9]+$/
  end

  x.report('regexp fail') do
    bad_float =~ /^[-+]?[0-9]*\.?[0-9]+$/
  end
end
Matt Sanders
fuente
29
class String
  def numeric?
    return true if self =~ /\A\d+\Z/
    true if Float(self) rescue false
  end
end  

p "1".numeric?  # => true
p "1.2".numeric? # => true
p "5.4e-29".numeric? # => true
p "12e20".numeric? # true
p "1a".numeric? # => false
p "1.2.3.4".numeric? # => false
hipertracker
fuente
12
/^\d+$/no es una expresión regular segura en Ruby, sí /\A\d+\Z/. (por ejemplo, "42 \ nalgo de texto" volvería true)
Timothee A
Para aclarar el comentario de @ TimotheeA, es seguro usarlo /^\d+$/si se trata de líneas, pero en este caso se trata del comienzo y el final de una cadena, por lo tanto /\A\d+\Z/.
Julio
1
¿No deberían editarse las respuestas para cambiar la respuesta real POR parte del respondedor? cambiar la respuesta en una edición si no eres el respondedor parece ... posiblemente deshonesto y debería estar fuera de los límites.
Jaydel
2
\ Z permite tener \ n al final de la cadena, por lo que "123 \ n" pasará la validación, independientemente de que no sea completamente numérico. Pero si usa \ z, entonces será más correcta la
expresión regular
15

Confiar en la excepción planteada no es la solución más rápida, legible ni confiable.
Haría lo siguiente:

my_string.should =~ /^[0-9]+$/
Damien MATHIEU
fuente
1
Sin embargo, esto solo funciona para números enteros positivos. Los valores como '-1', '0.0' o '1_000' devuelven falso aunque sean valores numéricos válidos. Estás mirando algo como / ^ [- .0-9] + $ /, pero eso acepta erróneamente '- -'.
Jakob S
13
De Rails 'validates_numericality_of': raw_value.to_s = ~ / \ A [+ -]? \ D + \ Z /
Morten
NoMethodError: método indefinido `should 'para" asd ": String
sergserg
En la última rspec, esto se convierte enexpect(my_string).to match(/^[0-9]+$/)
Damien MATHIEU
Me gusta: my_string =~ /\A-?(\d+)?\.?\d+\Z/te permite hacer '.1', '-0.1' o '12' pero no '' o '-' o '.'
Josh
8

A partir de Ruby 2.6.0, los métodos de exceptionconversión numéricos tienen un argumento opcional [1] . Esto nos permite usar los métodos integrados sin usar excepciones como flujo de control:

Float('x') # => ArgumentError (invalid value for Float(): "x")
Float('x', exception: false) # => nil

Por lo tanto, no tiene que definir su propio método, pero puede verificar directamente variables como, por ejemplo,

if Float(my_var, exception: false)
  # do something if my_var is a float
end
Timitry
fuente
7

así es como lo hago, pero creo que también debe haber una mejor manera

object.to_i.to_s == object || object.to_f.to_s == object
hormiga
fuente
5
No reconoce la notación flotante, por ejemplo, 1.2e + 35.
hipertracker
1
En Ruby 2.4.0 corrí object = "1.2e+35"; object.to_f.to_s == objecty funcionó
Giovanni Benussi
6

no, solo lo estás usando mal. tu es_number? tiene un argumento. lo llamaste sin el argumento

deberías estar haciendo is_number? (mystring)

corroído
fuente
¿Basado en is_number? método en la pregunta, usando is_a? no está dando la respuesta correcta. Si mystringes un String, mystring.is_a?(Integer)siempre será falso. Parece que quiere un resultado comois_number?("12.4") #=> true
Jakob S
Jakob S tiene razón. mystring es siempre una cadena, pero puede estar formada solo por números. quizás mi pregunta debería haber sido is_numeric? para no confundir el tipo de datos
Jamie Buchanan
6

Tl; dr: use un enfoque de expresiones regulares. Es 39 veces más rápido que el enfoque de rescate en la respuesta aceptada y también maneja casos como "1,000"

def regex_is_number? string
  no_commas =  string.gsub(',', '')
  matches = no_commas.match(/-?\d+(?:\.\d+)?/)
  if !matches.nil? && matches.size == 1 && matches[0] == no_commas
    true
  else
    false
  end
end

-

La respuesta aceptada por @Jakob S funciona en su mayor parte, pero detectar excepciones puede ser muy lento. Además, el enfoque de rescate falla en una cadena como "1,000".

Definamos los métodos:

def rescue_is_number? string
  true if Float(string) rescue false
end

def regex_is_number? string
  no_commas =  string.gsub(',', '')
  matches = no_commas.match(/-?\d+(?:\.\d+)?/)
  if !matches.nil? && matches.size == 1 && matches[0] == no_commas
    true
  else
    false
  end
end

Y ahora algunos casos de prueba:

test_cases = {
  true => ["5.5", "23", "-123", "1,234,123"],
  false => ["hello", "99designs", "(123)456-7890"]
}

Y un pequeño código para ejecutar los casos de prueba:

test_cases.each do |expected_answer, cases|
  cases.each do |test_case|
    if rescue_is_number?(test_case) != expected_answer
      puts "**rescue_is_number? got #{test_case} wrong**"
    else
      puts "rescue_is_number? got #{test_case} right"
    end

    if regex_is_number?(test_case) != expected_answer
      puts "**regex_is_number? got #{test_case} wrong**"
    else
      puts "regex_is_number? got #{test_case} right"
    end  
  end
end

Aquí está el resultado de los casos de prueba:

rescue_is_number? got 5.5 right
regex_is_number? got 5.5 right
rescue_is_number? got 23 right
regex_is_number? got 23 right
rescue_is_number? got -123 right
regex_is_number? got -123 right
**rescue_is_number? got 1,234,123 wrong**
regex_is_number? got 1,234,123 right
rescue_is_number? got hello right
regex_is_number? got hello right
rescue_is_number? got 99designs right
regex_is_number? got 99designs right
rescue_is_number? got (123)456-7890 right
regex_is_number? got (123)456-7890 right

Es hora de hacer algunas evaluaciones comparativas de rendimiento:

Benchmark.ips do |x|

  x.report("rescue") { test_cases.values.flatten.each { |c| rescue_is_number? c } }
  x.report("regex") { test_cases.values.flatten.each { |c| regex_is_number? c } }

  x.compare!
end

Y los resultados:

Calculating -------------------------------------
              rescue   128.000  i/100ms
               regex     4.649k i/100ms
-------------------------------------------------
              rescue      1.348k 16.8%) i/s -      6.656k
               regex     52.113k  7.8%) i/s -    260.344k

Comparison:
               regex:    52113.3 i/s
              rescue:     1347.5 i/s - 38.67x slower
pthamm
fuente
Gracias por el benchmark. La respuesta aceptada tiene la ventaja de aceptar entradas como 5.4e-29. Supongo que su expresión regular podría modificarse para aceptarlos también.
Jodi
3
Manejar casos como 1,000 es realmente difícil, ya que depende de la intención del usuario. Hay muchas, muchas formas en que los humanos pueden formatear números. ¿1000 es aproximadamente igual a 1000 o aproximadamente igual a 1? La mayor parte del mundo dice que se trata de 1, no una forma de mostrar el entero 1000.
James Moore
4

En rails 4, debe require File.expand_path('../../lib', __FILE__) + '/ext/string' ingresar su config / application.rb

jcye
fuente
1
en realidad no necesita hacer esto, simplemente puede poner string.rb en "inicializadores" y ¡funciona!
mahatmanich
3

Si prefiere no usar excepciones como parte de la lógica, puede intentar esto:

class String
   def numeric?
    !!(self =~ /^-?\d+(\.\d*)?$/)
  end
end

O, si desea que funcione en todas las clases de objetos, reemplácelo class Stringcon class Objectun convert self en una cadena: !!(self.to_s =~ /^-?\d+(\.\d*)?$/)

Mark Schneider
fuente
¿Cuál es el propósito de negar y hacer nil?cero es trurthy en ruby, para que pueda hacerlo solo?!!(self =~ /^-?\d+(\.\d*)?$/)
Arnold Roa
El uso !!ciertamente funciona. Al menos una guía de estilo de Ruby ( github.com/bbatsov/ruby-style-guide ) sugirió evitar !!a favor de la .nil?legibilidad, pero he visto que se !!usa en repositorios populares, y creo que es una buena manera de convertir a booleano. Edité la respuesta.
Mark Schneider
-3

usa la siguiente función:

def is_numeric? val
    return val.try(:to_f).try(:to_s) == val
end

entonces,

is_numeric? "1.2f" = falso

is_numeric? "1.2" = verdadero

is_numeric? "12f" = falso

is_numeric? "12" = verdadero

Rajesh Paul
fuente
Esto fallará si val es "0". También tenga en cuenta que el método .tryno forma parte de la biblioteca principal de Ruby y solo está disponible si incluye ActiveSupport.
GMA
De hecho, también falla "12", por lo que su cuarto ejemplo en esta pregunta es incorrecto. "12.10"y "12.00"fallar también.
GMA
-5

¿Qué tan tonta es esta solución?

def is_number?(i)
  begin
    i+0 == i
  rescue TypeError
    false
  end
end
Donvnielsen
fuente
1
Esto es subóptimo porque usar '.respond_to? (: +)' Siempre es mejor que fallar y detectar una excepción en una llamada a un método específico (: +). Esto también puede fallar por una variedad de razones en las que los métodos de conversión y Regex no lo hacen.
Sqeaky