Teste se string é um número em Ruby on Rails

103

Tenho o seguinte em meu controlador de aplicativo:

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

e a seguinte condição no meu controlador:

if mystring.is_number?

end

A condição está gerando um undefined methoderro. Estou supondo que defini is_numberno lugar errado ...?

Jamie Buchanan
fonte
4
Eu sei que muitas pessoas estão aqui por causa da aula de Teste Rails for Zombies do codeschool. Apenas espere ele continuar explicando. Os testes não devem passar --- tudo bem se você falhar no teste, você pode sempre corrigir os trilhos para inventar métodos como self.is_number?
boulder_ruby
A resposta aceita falha em casos como "1.000" e é 39 vezes mais lenta do que usando uma abordagem regex. Veja minha resposta abaixo.
pthamm

Respostas:

186

is_number?Método de criação .

Crie um método auxiliar:

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

E então chame assim:

my_string = '12.34'

is_number?( my_string )
# => true

Estender Stringaula.

Se você deseja chamar is_number?diretamente a string em vez de passá-la como um parâmetro para sua função auxiliar, você precisa definir is_number?como uma extensão da Stringclasse, assim:

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

E então você pode chamá-lo com:

my_string.is_number?
# => true
Jakob S
fonte
2
Esta é uma má ideia. "330.346.11" .to_f # => 330.346
epochwolf
11
Não há nada to_facima, e Float () não exibe esse comportamento: Float("330.346.11")aumentosArgumentError: invalid value for Float(): "330.346.11"
Jakob S
7
Se você usar esse patch, eu o renomeia para numeric ?, para ficar em linha com as convenções de nomenclatura ruby ​​(classes numéricas herdam de Numeric, os prefixos is_ são javaish).
Konrad Reiche
10
Não é realmente relevante para a pergunta original, mas provavelmente colocaria o código lib/core_ext/string.rb.
Jakob S
1
Eu não acho que isso is_number?(string)funcione no Ruby 1.9. Talvez seja parte do Rails ou 1.8? String.is_a?(Numeric)trabalho. Consulte também stackoverflow.com/questions/2095493/… .
Ross Attrill
30

Aqui está uma referência para maneiras comuns de resolver esse problema. Observe qual você deve usar provavelmente depende da proporção de casos falsos esperada.

  1. Se forem relativamente incomuns, o casting é definitivamente mais rápido.
  2. Se casos falsos forem comuns e você estiver apenas verificando ints, a comparação com um estado transformado é uma boa opção.
  3. Se casos falsos são comuns e você está verificando floats, regexp é provavelmente o caminho a percorrer

Se o desempenho não importa, use o que quiser. :-)

Detalhes de verificação de inteiros:

# 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

Detalhes de verificação do flutuador:

# 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
fonte
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
fonte
12
/^\d+$/não é uma regexp segura em Ruby, /\A\d+\Z/é. (por exemplo, "42 \ nalgo texto" retornaria true)
Timothee A
Para esclarecer sobre o comentário de @TimotheeA, é seguro usar /^\d+$/se estiver lidando com linhas, mas neste caso é sobre o início e o fim de uma string /\A\d+\Z/.
Julio
1
As respostas não devem ser editadas para alterar a resposta real do respondente? mudar a resposta em uma edição se você não for o respondente parece ... possivelmente dissimulado e deveria estar fora dos limites.
Jaydel
2
\ Z permite ter \ n no final da string, então "123 \ n" passará na validação, independentemente de não ser totalmente numérico. Mas se você usar \ z, será regexp mais correto: / \ A \ d + \ z /
SunnyMagadan
15

Contar com a exceção levantada não é a solução mais rápida, legível ou confiável.
Eu faria o seguinte:

my_string.should =~ /^[0-9]+$/
Damien MATHIEU
fonte
1
Isso só funciona para inteiros positivos, no entanto. Valores como '-1', '0,0' ou '1_000' retornam falso, embora sejam valores numéricos válidos. Você está olhando para algo como / ^ [- .0-9] + $ /, mas que aceita erroneamente '- -'.
Jakob S
13
De Rails 'validates_numericality_of': raw_value.to_s = ~ / \ A [+ -]? \ D + \ Z /
Morten
NoMethodError: método indefinido `deveria 'para" asd ": String
sergserg
No rspec mais recente, isso se tornaexpect(my_string).to match(/^[0-9]+$/)
Damien MATHIEU
Eu gosto: my_string =~ /\A-?(\d+)?\.?\d+\Z/permite que você faça '.1', '-0,1' ou '12', mas não '' ou '-' ou '.'
Josh
8

A partir do Ruby 2.6.0, os métodos de cast numérico têm um exceptionargumento opcional [1] . Isso nos permite usar os métodos integrados sem usar exceções como fluxo de controle:

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

Portanto, você não precisa definir seu próprio método, mas pode verificar diretamente variáveis ​​como, por exemplo,

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

é assim que eu faço, mas também acho que deve haver uma maneira melhor

object.to_i.to_s == object || object.to_f.to_s == object
formiga
fonte
5
Ele não reconhece notação flutuante, por exemplo, 1.2e + 35.
hipertracker
1
Em Ruby 2.4.0 eu corri object = "1.2e+35"; object.to_f.to_s == objecte funcionou
Giovanni Benussi
6

não, você está apenas usando errado. seu is_number? tem um argumento. você chamou sem o argumento

você deveria estar fazendo is_number? (mystring)

corroído
fonte
Com base no is_number? método na questão, usando is_a? não está dando a resposta correta. Se mystringfor realmente uma String, mystring.is_a?(Integer)sempre será falso. Parece que ele quer um resultado comois_number?("12.4") #=> true
Jakob S
Jakob S está correto. mystring é sempre uma string, mas pode ser composta apenas por números. talvez minha pergunta devesse ser is_numeric? para não confundir o tipo de dados
Jamie Buchanan
6

Tl; dr: use uma abordagem regex. É 39 vezes mais rápido do que a abordagem de resgate na resposta aceita e também lida com 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

-

A resposta aceita por @Jakob S funciona na maior parte, mas detectar exceções pode ser muito lento. Além disso, a abordagem de resgate falha em uma string como "1.000".

Vamos definir os 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

E agora alguns casos de teste:

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

E um pequeno código para executar os casos de teste:

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

Aqui está o resultado dos casos de teste:

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

É hora de fazer alguns benchmarks de desempenho:

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

E os 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
fonte
Obrigado pelo benchmark. A resposta aceita tem a vantagem de aceitar entradas como 5.4e-29. Eu acho que sua regex pode ser ajustada para aceitá-los também.
Jodi
3
Tratar casos como 1.000 é muito difícil, pois depende da intenção do usuário. Existem muitas maneiras de os humanos formatarem números. 1.000 é aproximadamente igual a 1.000 ou aproximadamente igual a 1? A maior parte do mundo diz que é cerca de 1, não é uma forma de mostrar o número inteiro 1000.
James Moore
4

No rails 4, você precisa colocar require File.expand_path('../../lib', __FILE__) + '/ext/string' em seu config / application.rb

jcye
fonte
1
na verdade você não precisa fazer isso, você pode apenas colocar string.rb em "inicializadores" e funciona!
mahatmanich
3

Se preferir não usar exceções como parte da lógica, você pode tentar o seguinte:

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

Ou, se você quiser que funcione em todas as classes de objeto, substitua class Stringpor class Objectum convert self em uma string: !!(self.to_s =~ /^-?\d+(\.\d*)?$/)

Mark Schneider
fonte
Qual é o propósito de negar e fazer nil?zero é confiável no ruby, então você pode fazer apenas!!(self =~ /^-?\d+(\.\d*)?$/)
Arnold Roa
Usar !!certamente funciona. Pelo menos um guia de estilo Ruby ( github.com/bbatsov/ruby-style-guide ) sugeriu evitar !!em favor da .nil?legibilidade, mas já vi !!usado em repositórios populares e acho que é uma boa maneira de converter para booleano. Eu editei a resposta.
Mark Schneider
-3

use a seguinte função:

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

tão,

is_numeric? "1.2f" = falso

is_numeric? "1.2" = verdadeiro

is_numeric? "12f" = falso

is_numeric? "12" = verdadeiro

Rajesh Paul
fonte
Isso falhará se val for "0". Observe também que o método .trynão faz parte da biblioteca central do Ruby e só está disponível se você incluir o ActiveSupport.
GMA de
Na verdade, ele também falha "12", então seu quarto exemplo nesta questão está errado. "12.10"e "12.00"falhar também.
GMA de
-5

Quão idiota é essa solução?

def is_number?(i)
  begin
    i+0 == i
  rescue TypeError
    false
  end
end
Donvnielsen
fonte
1
Isso é abaixo do ideal porque usar '.respond_to? (: +)' É sempre melhor do que falhar e capturar uma exceção em uma chamada de método (: +) específica. Isso também pode falhar por uma série de razões pelas quais o Regex e os métodos de conversão não.
Sqeaky