Verifica se la stringa è un numero in Ruby on Rails


103

Ho quanto segue nel mio controller dell'applicazione:

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

e la seguente condizione nel mio controller:

if mystring.is_number?

end

La condizione genera un undefined methoderrore. Immagino di essermi definito is_numbernel posto sbagliato ...?


4
So che molte persone sono qui a causa del corso Rails for Zombies Testing di codeschool. Aspetta solo che continui a spiegare. I test non dovrebbero passare --- va bene se il test fallisce per errore, puoi sempre patchare rails per inventare metodi come self.is_number?
boulder_ruby

La risposta accettata fallisce in casi come "1.000" ed è 39 volte più lenta rispetto all'utilizzo di un approccio regex. Vedi la mia risposta di seguito.
pthamm

Risposte:


186

Crea is_number?metodo.

Crea un metodo di supporto:

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

E poi chiamalo così:

my_string = '12.34'

is_number?( my_string )
# => true

Estendi Stringclasse.

Se vuoi essere in grado di chiamare is_number?direttamente sulla stringa invece di passarla come parametro alla tua funzione di supporto, allora devi definirla is_number?come un'estensione della Stringclasse, in questo modo:

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

E poi puoi chiamarlo con:

my_string.is_number?
# => true

2
Questa è una cattiva idea. "330.346.11" .to_f # => 330.346
epochwolf

11
Non c'è niente to_fin quanto sopra e Float () non mostra quel comportamento: Float("330.346.11")rilanciaArgumentError: invalid value for Float(): "330.346.11"
Jakob S

7
Se usi quella patch, la rinominerei in numeric ?, per rimanere in linea con le convenzioni di denominazione di ruby ​​(le classi numeriche ereditano da Numeric, i prefissi is_ sono javaish).
Konrad Reiche

10
Non molto rilevante per la domanda originale, ma probabilmente inserirò il codice lib/core_ext/string.rb.
Jakob S

1
Non credo che il is_number?(string)bit funzioni Ruby 1.9. Forse fa parte di Rails o 1.8? String.is_a?(Numeric)lavori. Vedi anche stackoverflow.com/questions/2095493/… .
Ross Attrill

30

Ecco un punto di riferimento per i modi comuni per affrontare questo problema. Nota quale dovresti usare probabilmente dipende dal rapporto tra i casi falsi previsti.

  1. Se sono relativamente rari, il casting è sicuramente più veloce.
  2. Se i casi falsi sono comuni e stai solo controllando gli int, il confronto con uno stato trasformato è una buona opzione.
  3. Se i casi falsi sono comuni e stai controllando i float, regexp è probabilmente la strada da percorrere

Se le prestazioni non contano, usa ciò che ti piace. :-)

Dettagli del controllo intero:

# 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

Dettagli del controllo del galleggiante:

# 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

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

12
/^\d+$/non è un'espressione regolare sicura in Ruby, lo /\A\d+\Z/è. (ad esempio, "42 \ nsome text" sarebbe tornato true)
Timoteo A

Per chiarire il commento di @ TimotheeA, è sicuro da usare /^\d+$/se si tratta di righe, ma in questo caso si tratta dell'inizio e della fine di una stringa, quindi /\A\d+\Z/.
Julio

1
Le risposte non dovrebbero essere modificate per cambiare la risposta effettiva DAL risponditore? cambiare la risposta in una modifica se non sei il risponditore sembra ... forse subdolo e dovrebbe essere fuori dai limiti.
jaydel

2
\ Z consente di avere \ n alla fine della stringa, quindi "123 \ n" passerà la convalida, indipendentemente dal fatto che non sia completamente numerico. Ma se usi \ z, sarà
un'espressione

15

Affidarsi all'eccezione sollevata non è la soluzione più veloce, leggibile né affidabile.
Farei quanto segue:

my_string.should =~ /^[0-9]+$/

1
Tuttavia, funziona solo per numeri interi positivi. Valori come "-1", "0,0" o "1_000" restituiscono false anche se sono valori numerici validi. Stai guardando qualcosa come / ^ [- .0-9] + $ /, ma questo accetta erroneamente '- -'.
Jakob S

13
Da Rails 'validates_numericality_of': raw_value.to_s = ~ / \ A [+ -]? \ D + \ Z /
Morten

NoMethodError: metodo non definito `should 'per" asd ": String
sergserg

Nell'ultimo rspec, questo diventaexpect(my_string).to match(/^[0-9]+$/)
Damien MATHIEU

Mi piace: my_string =~ /\A-?(\d+)?\.?\d+\Z/ti permette di fare ".1", "-0.1" o "12" ma non "" o "-" o "."
Josh

8

A partire da Ruby 2.6.0, i metodi cast numerici hanno un exceptionargomento opzionale [1] . Ciò ci consente di utilizzare i metodi integrati senza utilizzare eccezioni come flusso di controllo:

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

Pertanto, non è necessario definire il proprio metodo, ma è possibile controllare direttamente variabili come ad es

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

7

è così che lo faccio, ma penso anche che ci debba essere un modo migliore

object.to_i.to_s == object || object.to_f.to_s == object

5
Non riconosce la notazione mobile, ad esempio 1.2e + 35.
hipertracker

1
In Ruby 2.4.0 ho corso object = "1.2e+35"; object.to_f.to_s == objecte ha funzionato
Giovanni Benussi

6

no, lo stai solo usando in modo sbagliato. il tuo is_number? ha un argomento. l'hai chiamato senza discutere

dovresti fare is_number? (mystring)


Basato su is_number? metodo nella domanda, utilizzando is_a? non sta dando la risposta corretta. Se mystringè davvero una stringa, mystring.is_a?(Integer)sarà sempre falso. Sembra che voglia un risultato comeis_number?("12.4") #=> true
Jakob S

Jakob S ha ragione. mystring è in effetti sempre una stringa, ma può essere composta solo da numeri. forse la mia domanda avrebbe dovuto essere is_numeric? per non confondere il tipo di dati
Jamie Buchanan

6

Tl; dr: usa un approccio regex. È 39 volte più veloce dell'approccio di salvataggio nella risposta accettata e gestisce anche casi come "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 risposta accettata da @Jakob S funziona per la maggior parte, ma catturare le eccezioni può essere molto lento. Inoltre, l'approccio di salvataggio fallisce su una stringa come "1.000".

Definiamo i metodi:

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 ora alcuni casi di test:

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

E un piccolo codice per eseguire i casi di test:

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

Ecco l'output dei casi di test:

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

È ora di fare alcuni benchmark delle prestazioni:

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 i risultati:

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

Grazie per il benchmark. La risposta accettata ha il vantaggio di accettare input come 5.4e-29. Immagino che la tua regex potrebbe essere modificata per accettare anche quelle.
Jodi

3
Gestire casi come 1.000 è davvero difficile, poiché dipende dall'intenzione dell'utente. Ci sono molti, molti modi per gli esseri umani di formattare i numeri. 1.000 è circa uguale a 1000 o circa uguale a 1? La maggior parte del mondo dice che si tratta di 1, non è un modo per mostrare il numero intero 1000.
James Moore,

4

In rails 4, devi inserire il require File.expand_path('../../lib', __FILE__) + '/ext/string' tuo config / application.rb


1
in realtà non hai bisogno di farlo, potresti semplicemente mettere string.rb in "inizializzatori" e funziona!
mahatmanich

3

Se preferisci non utilizzare le eccezioni come parte della logica, potresti provare questo:

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

Oppure, se vuoi che funzioni su tutte le classi di oggetti, sostituisci class Stringcon class Objectun convert self in una stringa: !!(self.to_s =~ /^-?\d+(\.\d*)?$/)


Qual è lo scopo di negare e fare nil?zero è vero su rubino, quindi puoi fare solo!!(self =~ /^-?\d+(\.\d*)?$/)
Arnold Roa

Usare !!sicuramente funziona. Almeno una guida allo stile di Ruby ( github.com/bbatsov/ruby-style-guide ) ha suggerito di evitare !!a favore della .nil?leggibilità, ma l'ho vista !!usata in repository popolari e penso che sia un ottimo modo per convertire in booleano. Ho modificato la risposta.
Mark Schneider

-3

utilizzare la seguente funzione:

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

così,

is_numeric? "1.2f" = falso

is_numeric? "1.2" = vero

is_numeric? "12f" = falso

is_numeric? "12" = vero


Questo fallirà se val è "0". Tieni inoltre presente che il metodo .trynon fa parte della libreria principale di Ruby ed è disponibile solo se includi ActiveSupport.
GMA

In effetti, fallisce anche per "12", quindi il tuo quarto esempio in questa domanda è sbagliato. "12.10"e "12.00"fallire anche.
GMA

-5

Quanto è stupida questa soluzione?

def is_number?(i)
  begin
    i+0 == i
  rescue TypeError
    false
  end
end

1
Questo non è ottimale perché usare '.respond_to? (: +)' È sempre meglio che fallire e catturare un'eccezione su una specifica chiamata al metodo (: +). Ciò potrebbe anche non riuscire per una serie di motivi se i metodi Regex e di conversione non lo fanno.
Sqeaky
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.