2009-06-23 9 views
28

C'è un modo per controllare semplicemente se un valore stringa è un valore float valido. Chiamando to_f su una stringa lo convertirà in 0.0 se non è un valore numerico. E l'uso di Float() solleva un'eccezione quando viene passata una stringa float non valida che è più vicina a ciò che voglio, ma non voglio gestire le eccezioni di cattura. Quello che voglio veramente è un metodo come nan? che esiste nella classe Float, ma ciò non aiuta perché una stringa non numerica non può essere convertita in una variabile senza essere modificata in 0.0 (usando to_f).Determinare se una stringa è un valore float valida

"a".to_f => 0.0 

"a".to_f.nan? => false 

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

Esiste una soluzione semplice o è necessario scrivere codice per verificare se una stringa è un valore float valido?

risposta

26

Un fatto interessante circa il mondo di Ruby è l'esistenza del progetto Rubinius, che implementa Ruby e la sua libreria standard per lo più in puro Ruby. Di conseguenza, essi hanno un'implementazione puro Rubino del Kernel # galleggiante, che si presenta come:

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 

Questo vi dà un'espressione regolare che corrisponde al lavoro interno di Ruby fa quando si corre Float(), ma senza la eccezione. Così si potrebbe fare ora:

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

La cosa bella di questa soluzione è che, poiché Rubinius corre, e passa RubySpec, sai che questo regex gestisce i edge-casi che Ruby si gestisce, e si può chiamare to_f sul String senza paura!

+0

Ottima risposta! Nota: questa regex si è evoluta un po 'nell'implementazione di Rubinius, per i dettagli vedere le specifiche su https://github.com/rubinius/rubinius/blob/master/spec/ruby/core/string/to_f_spec.rb. Tieni presente inoltre che se stai usando questo per convalidare l'input dell'utente, potresti voler escludere il supporto per i trattini bassi e usare solo la regex di Rubinius per l'ispirazione :) Correzione – captainpete

+0

, Rubinius usa ancora la stessa espressione regolare per Float(). Trova il codice su https://github.com/rubinius/rubinius/blob/master/kernel/common/kernel19.rb – captainpete

34

Ecco un modo:

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 

Se si vuole evitare la scimmia-patch di stringa, si può sempre fare di questo un metodo di classe di qualche modulo che controlli, naturalmente:

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

Non dovresti evitare l'uso di salvataggio nella sua forma modificatore? – Benjamin

+1

Bella risposta. Come piccolo suggerimento, eviterei l'uso della doppia negazione come suggerito [qui] (http://www.rubydoc.info/github/bbatsov/rubocop/Rubocop/Cop/Style/DoubleNegation) –

1

Ho provato ad aggiungere questo come commento ma a quanto pare non c'è formattazione nei commenti:

d'altro canto, perché non utilizzarlo come funzione di conversione, come

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

che, naturalmente, è quello che non volevi fare in primo luogo. Immagino che la risposta sia "devi scrivere la tua funzione se davvero non vuoi usare la gestione delle eccezioni, ma perché lo faresti?"

+1

Volevo solo essere chiaro che l'uso di 0.0/0.0 è un hack sporco, ma se vuoi ottenere NaN è attualmente l'unico modo (che io sappia). Se fosse il mio programma, prenderei in considerazione l'utilizzo di nil. – Sam

3

Umm, se non si vuole eccezioni allora forse:

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

Dal OP specificamente chiesto una soluzione senza eccezioni. soluzione basata Regexp è marginalmente 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 

Risultati:

 
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

Il Float non lancia una routine nativa mentre l'espressione regolare è molto più lenta? –

+0

"La soluzione basata su Regexp è leggermente lenta" - ricontrolla i numeri 3/5 equivale al 60%. Non chiamerei perdere il 40% come calo marginale. –

+0

Inoltre, tieni presente che se il tuo casting aumenterà le eccezioni più frequentemente, allora non sarà molto più lento del regexp. Questo perché il salvataggio da un'eccezione è molto lento, come mostrato qui: 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 l''alternativa più veloce' restituirà false per '5,00' – Lambart

3

ho visto la discussione irrisolta su calco + eccezioni vs regex e io ho pensato di provare a punto di riferimento di tutto e di produrre una risposta obiettiva:

Ecco la sorgente per il caso migliore e peggiore di ciascun metodo tentato qui:

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 i seguenti risultati 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) 

Dato che ci occupiamo solo di algoritmi di tempo lineare, penso che usiamo misurazioni empiriche per effettuare generalizzazioni. È chiaro che la regex è più coerente e fluttuerà solo un bit in base alla lunghezza della stringa passata. Il cast è chiaramente più veloce quando non ci sono errori e molto più lentamente quando ci sono dei fallimenti.

Se confrontiamo i tempi di successo possiamo vedere che il cast best case è di circa 3 secondi più veloce rispetto al regex best case. Se dividiamo questo per la quantità di tempo nel caso peggiore, possiamo stimare il numero di esecuzioni che occorrerebbe per pareggiare con eccezioni rallentando il lancio verso il basso per far corrispondere le velocità di regex. Circa 6 secondi immessi da .3 ci danno circa 20. Quindi, se le prestazioni sono importanti e ci si aspetta che meno di 1 su 20 del test fallisca, utilizzare le eccezioni cast +.

JRuby 1.7.4 ha risultati completamente diversi:

   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 è solo marginalmente più veloce nel migliore dei casi (circa il 10%). Presumendo che questa differenza sia appropriata per fare generalizzazioni (non credo che lo sia), allora il break even point è tra 200 e 250 run con solo 1 che causa un'eccezione.

Quindi le eccezioni devono essere utilizzate solo quando si verificano cose veramente eccezionali, questa è una decisione per te e per il tuo codice base. Quando non vengono utilizzati, il codice in cui sono inseriti può essere più semplice e veloce.

Se le prestazioni non sono importanti, probabilmente dovresti solo seguire le convenzioni del tuo team o della base di codice e ignorare tutte le risposte.

2

Prova questa

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 

Questo supporta 1.5, 5, 123.456, 1_000 ma non 1 000, 1,000, ecc (per esempio stesso di String#to_f).

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

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

Problemi correlati