Testen Sie, ob der String eine Zahl in Ruby on Rails ist


103

Ich habe Folgendes in meinem Anwendungscontroller:

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

und die folgende Bedingung in meinem Controller:

if mystring.is_number?

end

Die Bedingung löst einen undefined methodFehler aus. Ich schätze, ich habe is_numberam falschen Ort definiert ...?


4
Ich weiß, dass viele Leute wegen der Rails for Zombies Testing-Klasse der Codeschool hier sind. Warten Sie nur, bis er es weiter erklärt. Die Tests sollten nicht bestanden werden. Es ist in Ordnung, wenn der Test fehlerhaft fehlschlägt. Sie können jederzeit Schienen patchen, um Methoden wie self.is_number zu erfinden.
Boulder_Ruby

Die akzeptierte Antwort schlägt in Fällen wie "1.000" fehl und ist 39-mal langsamer als bei Verwendung eines Regex-Ansatzes. Siehe meine Antwort unten.
Pthamm

Antworten:


186

is_number?Methode erstellen .

Erstellen Sie eine Hilfsmethode:

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

Und dann nenne es so:

my_string = '12.34'

is_number?( my_string )
# => true

Extend StringKlasse.

Wenn Sie is_number?die Zeichenfolge direkt aufrufen möchten, anstatt sie als Parameter an Ihre Hilfsfunktion zu übergeben, müssen Sie sie wie folgt is_number?als Erweiterung der StringKlasse definieren :

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

Und dann können Sie es nennen mit:

my_string.is_number?
# => true

2
Das ist eine schlechte Idee. "330.346.11" .to_f # => 330.346
Epochwolf

11
Es gibt kein to_foben, und Float () zeigt dieses Verhalten nicht: Float("330.346.11")RaisesArgumentError: invalid value for Float(): "330.346.11"
Jakob S

7
Wenn Sie diesen Patch verwenden, würde ich ihn in numerisch umbenennen, um den Ruby-Namenskonventionen zu entsprechen (numerische Klassen erben von numerischen, is_-Präfixe sind Java).
Konrad Reiche

10
Nicht wirklich relevant für die ursprüngliche Frage, aber ich würde wahrscheinlich den Code eingeben lib/core_ext/string.rb.
Jakob S

1
Ich denke nicht, dass das is_number?(string)Bit Ruby 1.9 funktioniert. Vielleicht ist das Teil von Rails oder 1.8? String.is_a?(Numeric)funktioniert. Siehe auch stackoverflow.com/questions/2095493/… .
Ross Attrill

30

Hier ist ein Benchmark für gängige Methoden zur Lösung dieses Problems. Beachten Sie, welche Sie wahrscheinlich verwenden sollten, hängt vom Verhältnis der erwarteten falschen Fälle ab.

  1. Wenn sie relativ ungewöhnlich sind, ist das Casting definitiv am schnellsten.
  2. Wenn falsche Fälle häufig sind und Sie nur nach Ints suchen, ist ein Vergleich mit einem transformierten Zustand eine gute Option.
  3. Wenn falsche Fälle häufig sind und Sie Floats überprüfen, ist Regexp wahrscheinlich der richtige Weg

Wenn die Leistung keine Rolle spielt, verwenden Sie, was Sie möchten. :-)

Details zur Ganzzahlprüfung:

# 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

Details zur Floatprüfung:

# 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+$/ist kein sicherer regulärer Ausdruck in Ruby /\A\d+\Z/. (zB "42 \ nsome text" würde zurückkehren true)
Timothee A

Um den Kommentar von @ TimotheeA zu verdeutlichen, ist es sicher, ihn zu verwenden, /^\d+$/wenn es sich um Zeilen handelt, aber in diesem Fall geht es also um den Anfang und das Ende eines Strings /\A\d+\Z/.
Julio

1
Sollten Antworten nicht bearbeitet werden, um die tatsächliche Antwort des Antwortenden zu ändern? Das Ändern der Antwort in einer Bearbeitung, wenn Sie nicht der Antwortende sind, scheint ... möglicherweise hinterhältig zu sein und sollte außerhalb der Grenzen liegen.
Jaydel

2
\ Z erlaubt es, \ n am Ende der Zeichenfolge zu haben, sodass "123 \ n" die Validierung besteht, unabhängig davon, ob es nicht vollständig numerisch ist. Aber wenn Sie \ z verwenden, wird es korrekter sein: / \ A \ d + \ z /
SunnyMagadan

15

Sich auf die angesprochene Ausnahme zu verlassen, ist nicht die schnellste, lesbarste und zuverlässigste Lösung.
Ich würde folgendes tun:

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

1
Dies funktioniert jedoch nur für positive ganze Zahlen. Werte wie '-1', '0.0' oder '1_000' geben alle false zurück, obwohl es sich um gültige numerische Werte handelt. Sie sehen so etwas wie / ^ [- .0-9] + $ /, aber das akzeptiert fälschlicherweise '- -'.
Jakob S

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

NoMethodError: undefinierte Methode "sollte" für "asd": String
sergserg

In der neuesten rspec wird diesexpect(my_string).to match(/^[0-9]+$/)
Damien MATHIEU

Ich mag: my_string =~ /\A-?(\d+)?\.?\d+\Z/Sie können '.1', '-0.1' oder '12' machen, aber nicht '' oder '-' oder '.'
Josh

8

Ab Ruby 2.6.0 verfügen die numerischen Cast-Methoden über ein optionales exceptionArgument [1] . Dies ermöglicht es uns, die integrierten Methoden zu verwenden, ohne Ausnahmen als Kontrollfluss zu verwenden:

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

Daher müssen Sie keine eigene Methode definieren, sondern können Variablen wie z

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

7

So mache ich das, aber ich denke auch, dass es einen besseren Weg geben muss

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

5
Es erkennt keine schwebende Notation, z. B. 1.2e + 35.
Hipertracker

1
In Ruby 2.4.0 lief ich object = "1.2e+35"; object.to_f.to_s == objectund es funktionierte
Giovanni Benussi

6

Nein, du benutzt es einfach falsch. Ihre is_number? hat ein Argument. du hast es ohne das Argument genannt

du solltest is_number machen? (mystring)


Basierend auf der is_number? Methode in der Frage, mit is_a? gibt nicht die richtige Antwort. Wenn mystringes sich tatsächlich um einen String handelt, ist mystring.is_a?(Integer)dieser immer falsch. Es sieht so aus, als ob er ein Ergebnis wieis_number?("12.4") #=> true
Jakob S

Jakob S ist richtig. mystring ist zwar immer ein String, kann aber nur aus Zahlen bestehen. Vielleicht hätte meine Frage is_numeric sein sollen? um den Datentyp nicht zu verwechseln
Jamie Buchanan

6

Tl; dr: Verwenden Sie einen Regex-Ansatz. Es ist 39x schneller als der Rettungsansatz in der akzeptierten Antwort und behandelt auch Fälle wie "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

- -

Die akzeptierte Antwort von @Jakob S funktioniert größtenteils, aber das Abfangen von Ausnahmen kann sehr langsam sein. Außerdem schlägt der Rettungsansatz bei einer Zeichenfolge wie "1.000" fehl.

Definieren wir die Methoden:

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

Und jetzt einige Testfälle:

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

Und ein kleiner Code zum Ausführen der Testfälle:

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

Hier ist die Ausgabe der Testfälle:

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

Zeit für einige Leistungsbenchmarks:

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

Und die Ergebnisse:

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

Danke für den Benchmark. Die akzeptierte Antwort hat den Vorteil, Eingaben wie zu akzeptieren 5.4e-29. Ich denke, Ihre Regex könnte optimiert werden, um diese auch zu akzeptieren.
Jodi

3
Die Bearbeitung von Fällen wie 1.000 ist sehr schwierig, da dies von der Absicht des Benutzers abhängt. Es gibt viele, viele Möglichkeiten für Menschen, Zahlen zu formatieren. Ist 1.000 ungefähr gleich 1000 oder ungefähr gleich 1? Der größte Teil der Welt sagt, es geht um 1, nicht um die ganze Zahl 1000 anzuzeigen.
James Moore

4

In Rails 4 müssen Sie require File.expand_path('../../lib', __FILE__) + '/ext/string' Ihre config / application.rb eingeben


1
Eigentlich brauchen Sie das nicht zu tun, Sie könnten einfach string.rb in "Initialisierer" setzen und es funktioniert!
Mahatmanich

3

Wenn Sie keine Ausnahmen als Teil der Logik verwenden möchten, können Sie Folgendes versuchen:

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

Wenn Sie möchten, dass es über alle Objektklassen hinweg funktioniert, ersetzen Sie es class Stringdurch class Objectein konvertiertes Selbst in eine Zeichenfolge: !!(self.to_s =~ /^-?\d+(\.\d*)?$/)


Was der Zweck ist, zu negieren und nil?Null zu tun, ist auf Rubin trurthy, also können Sie gerade tun!!(self =~ /^-?\d+(\.\d*)?$/)
Arnold Roa

Verwenden !!funktioniert sicherlich. Mindestens ein Ruby- Styleguide ( github.com/bbatsov/ruby-style-guide ) schlug vor, dies !!aus Gründen der .nil?Lesbarkeit zu vermeiden , aber ich habe !!es in beliebten Repositorys verwendet gesehen, und ich denke, es ist eine gute Möglichkeit, in Boolesche Werte zu konvertieren. Ich habe die Antwort bearbeitet.
Mark Schneider

-3

Verwenden Sie die folgende Funktion:

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

so,

is_numeric? "1.2f" = falsch

is_numeric? "1.2" = wahr

is_numeric? "12f" = falsch

is_numeric? "12" = wahr


Dies schlägt fehl, wenn val ist "0". Beachten Sie außerdem, dass die Methode .trynicht Teil der Ruby-Kernbibliothek ist und nur verfügbar ist, wenn Sie ActiveSupport einschließen.
GMA

Tatsächlich schlägt es auch fehl "12", sodass Ihr viertes Beispiel in dieser Frage falsch ist. "12.10"und "12.00"scheitern auch.
GMA

-5

Wie dumm ist diese Lösung?

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

1
Dies ist nicht optimal, da die Verwendung von '.respond_to? (: +)' Immer besser ist als das Fehlschlagen und Abfangen einer Ausnahme bei einem bestimmten Methodenaufruf (: +). Dies kann auch aus verschiedenen Gründen fehlschlagen, wenn die Regex- und Konvertierungsmethoden dies nicht tun.
Sqeaky
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.