Sprawdź, czy łańcuch jest liczbą w Ruby on Rails

103

Mam następujący kontroler aplikacji:

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

i następujący warunek w moim kontrolerze:

if mystring.is_number?

end

Warunek generuje undefined methodbłąd. Zgaduję, że zdefiniowałem is_numberw złym miejscu ...?

Jamie Buchanan
źródło
4
Wiem, że jest tu dużo ludzi z powodu zajęć z Rails for Zombies w szkole kodowej. Poczekaj tylko, aż będzie dalej wyjaśniał. Testy nie powinny przejść pomyślnie - w porządku, jeśli test zakończy się niepowodzeniem przez pomyłkę, zawsze możesz załatać szyny, aby wymyślić metody takie jak self.is_number?
boulder_ruby
Zaakceptowana odpowiedź zawodzi w przypadkach takich jak „1000” i jest 39 razy wolniejsza niż w przypadku użycia wyrażenia regularnego. Zobacz moją odpowiedź poniżej.
pthamm

Odpowiedzi:

186

Utwórz is_number?metodę.

Utwórz metodę pomocniczą:

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

A potem nazwij to tak:

my_string = '12.34'

is_number?( my_string )
# => true

Rozszerz Stringklasę.

Jeśli chcesz mieć możliwość wywołania is_number?bezpośrednio ciągu zamiast przekazywania go jako parametru do funkcji pomocniczej, musisz zdefiniować is_number?jako rozszerzenie Stringklasy, na przykład:

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

A potem możesz to nazwać:

my_string.is_number?
# => true
Jakob S
źródło
2
To jest zły pomysł. "330.346.11" .to_f # => 330,346
epochwolf
11
Nie ma to_fw powyższym, a Float () nie wykazuje tego zachowania: Float("330.346.11")podbijaArgumentError: invalid value for Float(): "330.346.11"
Jakob S
7
Jeśli użyjesz tej łatki, zmieniłbym jej nazwę na numeric?, Aby zachować zgodność z konwencjami nazewnictwa ruby ​​(klasy numeryczne dziedziczą po Numeric, przedrostki is_ są javaish).
Konrad Reiche
10
Niezbyt istotne dla pierwotnego pytania, ale prawdopodobnie umieściłbym kod lib/core_ext/string.rb.
Jakob S
1
Nie sądzę, żeby is_number?(string)bit działał Ruby 1.9. Może to część Railsów lub 1.8? String.is_a?(Numeric)Pracuje. Zobacz także stackoverflow.com/questions/2095493/… .
Ross Attrill
30

Oto punkt odniesienia dla typowych sposobów rozwiązania tego problemu. Zwróć uwagę, który z nich powinien być używany, zależy prawdopodobnie od oczekiwanego współczynnika fałszywych przypadków.

  1. Jeśli są stosunkowo rzadkie, rzucanie jest zdecydowanie najszybsze.
  2. Jeśli fałszywe przypadki są częste i po prostu sprawdzasz wartości int, dobrym rozwiązaniem jest porównanie ze stanem przekształconym.
  3. Jeśli częste są fałszywe przypadki i sprawdzasz zmiennoprzecinkowe, prawdopodobnie najlepszym rozwiązaniem jest regexp

Jeśli wydajność nie ma znaczenia, użyj tego, co lubisz. :-)

Szczegóły sprawdzania liczb całkowitych:

# 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

Szczegóły sprawdzania pływaka:

# 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
źródło
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
źródło
12
/^\d+$/nie jest bezpiecznym wyrażeniem regularnym w Rubim, /\A\d+\Z/jest. (np. zwróci „42 \ nktóry tekst” true)
Timothee A
Aby wyjaśnić komentarz @ TimotheeA, można go bezpiecznie używać w /^\d+$/przypadku linii, ale w tym przypadku dotyczy to początku i końca łańcucha /\A\d+\Z/.
Julio,
1
Czy odpowiedzi nie powinny być edytowane w celu zmiany rzeczywistej odpowiedzi PRZEZ respondenta? zmiana odpowiedzi w zmianie, jeśli nie jesteś osobą odpowiadającą, wydaje się ... prawdopodobnie podstępna i powinna być poza zakresem.
jaydel
2
\ Z pozwala na \ n na końcu łańcucha, więc "123 \ n" przejdzie walidację, niezależnie od tego, że nie jest w pełni numeryczny. Ale jeśli użyjesz \ z, będzie to bardziej poprawne
wyrażenie regularne
15

Poleganie na podniesionym wyjątku nie jest najszybszym, czytelnym ani niezawodnym rozwiązaniem.
Zrobiłbym co następuje:

my_string.should =~ /^[0-9]+$/
Damien MATHIEU
źródło
1
Działa to jednak tylko dla dodatnich liczb całkowitych. Wszystkie wartości takie jak „-1”, „0,0” lub „1_000” zwracają fałsz, mimo że są to prawidłowe wartości liczbowe. Patrzysz na coś takiego jak / ^ [- .0-9] + $ /, ale to błędnie akceptuje „- -”.
Jakob S,
13
Z Rails 'validates_numericality_of': raw_value.to_s = ~ / \ A [+ -]? \ D + \ Z /
Morten
NoMethodError: undefined method `should 'for' asd ': String
sergserg
W najnowszym raporcie jest toexpect(my_string).to match(/^[0-9]+$/)
Damien MATHIEU
Podoba mi się: my_string =~ /\A-?(\d+)?\.?\d+\Z/pozwala ci zrobić „.1”, „-0,1” lub „12”, ale nie „”, „-” lub „”.
Josh
8

Począwszy od Rubiego 2.6.0, numeryczne metody rzutowania mają opcjonalny exception-argument [1] . Dzięki temu możemy używać wbudowanych metod bez używania wyjątków jako przepływu sterowania:

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

Dlatego nie musisz definiować własnej metody, ale możesz bezpośrednio sprawdzać zmienne, takie jak np

if Float(my_var, exception: false)
  # do something if my_var is a float
end
Timitry
źródło
7

tak to robię, ale myślę, że musi być lepszy sposób

object.to_i.to_s == object || object.to_f.to_s == object
antpaw
źródło
5
Nie rozpoznaje notacji pływającej, np. 1.2e + 35.
hipertracker
1
W Ruby 2.4.0 pobiegłem object = "1.2e+35"; object.to_f.to_s == objecti zadziałało
Giovanni Benussi
6

nie, po prostu źle go używasz. twój numer_jest? ma argument. nazwałeś to bez argumentu

powinieneś zrobić is_number? (mystring)

skorodowane
źródło
Na podstawie is_number? metoda w pytaniu, używając is_a? nie podaje poprawnej odpowiedzi. Jeśli mystringrzeczywiście jest Stringiem, mystring.is_a?(Integer)zawsze będzie false. Wygląda na to, że chce wyniku takiego jakis_number?("12.4") #=> true
Jakob S
Jakob S ma rację. mystring jest rzeczywiście zawsze łańcuchem, ale może składać się tylko z liczb. może moje pytanie powinno brzmieć is_numeric? aby nie pomylić typu danych
Jamie Buchanan
6

Tl; dr: Użyj podejścia regex. Jest 39 razy szybsza niż metoda ratunkowa w przyjętej odpowiedzi, a także obsługuje przypadki typu „1000”

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

-

Zaakceptowana odpowiedź @Jakob S w większości działa, ale wyłapywanie wyjątków może być naprawdę powolne. Ponadto metoda ratunkowa zawodzi w przypadku łańcucha takiego jak „1000”.

Zdefiniujmy metody:

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

A teraz kilka przypadków testowych:

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

I mały kod do uruchamiania przypadków testowych:

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

Oto wynik przypadków testowych:

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

Czas na testy wydajności:

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

A wyniki:

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
źródło
Dzięki za test porównawczy. Zaakceptowana odpowiedź ma tę zaletę, że akceptuje takie dane wejściowe jak 5.4e-29. Domyślam się, że twoje wyrażenie regularne można zmodyfikować, aby akceptować również te.
Jodi,
3
Obsługa przypadków takich jak 1000 jest naprawdę trudna, ponieważ zależy od intencji użytkownika. Ludzie mogą formatować liczby na wiele, wiele sposobów. Czy 1000 jest mniej więcej równe 1000, czy mniej więcej równe 1? Większość świata mówi, że to około 1, a nie sposób na pokazanie liczby całkowitej 1000.
James Moore
4

W railsach 4 musisz umieścić require File.expand_path('../../lib', __FILE__) + '/ext/string' swój config / application.rb

jcye
źródło
1
właściwie nie musisz tego robić, możesz po prostu umieścić string.rb w "inicjalizatorach" i to działa!
mahatmanich
3

Jeśli wolisz nie używać wyjątków jako części logiki, możesz spróbować tego:

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

Albo, jeśli chcesz go do pracy we wszystkich klas obiektów, wymienić class Stringz class Objectna siebie Konwersja na ciąg znaków: !!(self.to_s =~ /^-?\d+(\.\d*)?$/)

Mark Schneider
źródło
Co celem negować i zrobić nil?zera jest trurthy na rubin, więc można zrobić tylko!!(self =~ /^-?\d+(\.\d*)?$/)
Arnold Roa
Używanie z !!pewnością działa. Co najmniej jeden przewodnik po stylu Ruby ( github.com/bbatsov/ruby-style-guide ) sugerował unikanie !!na korzyść .nil?czytelności, ale widziałem, że jest !!używany w popularnych repozytoriach i myślę, że jest to dobry sposób na konwersję na boolean. Zmieniłem odpowiedź.
Mark Schneider
-3

użyj następującej funkcji:

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

więc,

is_numeric? "1.2f" = fałsz

is_numeric? "1.2" = prawda

is_numeric? "12f" = fałsz

is_numeric? "12" = prawda

Rajesh Paul
źródło
To się nie powiedzie, jeśli val tak "0". Zwróć również uwagę, że metoda .trynie jest częścią podstawowej biblioteki Ruby i jest dostępna tylko wtedy, gdy używasz ActiveSupport.
GMA
W rzeczywistości również zawodzi "12", więc Twój czwarty przykład w tym pytaniu jest błędny. "12.10"i "12.00"też nie.
GMA
-5

Jak głupie jest to rozwiązanie?

def is_number?(i)
  begin
    i+0 == i
  rescue TypeError
    false
  end
end
donvnielsen
źródło
1
Jest to nieoptymalne, ponieważ użycie '.respond_to? (: +)' Jest zawsze lepsze niż niepowodzenie i przechwycenie wyjątku w wywołaniu określonej metody (: +). Może to również się nie powieść z różnych powodów, w których nie ma Regex i metod konwersji.
Sqeaky