Teste si la chaîne est un nombre dans Ruby on Rails


103

J'ai les éléments suivants dans mon contrôleur d'application:

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

et la condition suivante dans mon contrôleur:

if mystring.is_number?

end

La condition génère une undefined methoderreur. Je suppose que j'ai défini is_numberau mauvais endroit ...?


4
Je sais que beaucoup de gens sont ici à cause du cours de test Rails for Zombies de l'école de codes. Attendez juste qu'il continue d'expliquer. Les tests ne sont pas censés réussir --- il est normal que vous échouiez par erreur, vous pouvez toujours patcher des rails pour inventer des méthodes telles que self.is_number?
boulder_ruby

La réponse acceptée échoue sur des cas comme «1 000» et est 39 fois plus lente que l'utilisation d'une approche regex. Voir ma réponse ci-dessous.
pthamm

Réponses:


186

Créer une is_number?méthode.

Créez une méthode d'assistance:

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

Et puis appelez-le comme ceci:

my_string = '12.34'

is_number?( my_string )
# => true

Prolongez la Stringclasse.

Si vous voulez pouvoir appeler is_number?directement la chaîne au lieu de la passer en tant que paramètre à votre fonction d'assistance, vous devez la définir is_number?comme une extension de la Stringclasse, comme ceci:

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

Et puis vous pouvez l'appeler avec:

my_string.is_number?
# => true

2
C'est une mauvaise idée. "330.346.11" .to_f # => 330.346
epochwolf

11
Il n'y a pas to_fdans ce qui précède, et Float () ne présente pas ce comportement: Float("330.346.11")relancesArgumentError: invalid value for Float(): "330.346.11"
Jakob S

7
Si vous utilisez ce patch, je le renommerais en numérique ?, pour rester en ligne avec les conventions de dénomination ruby ​​(les classes numériques héritent de Numeric, les préfixes is_ sont javaish).
Konrad Reiche

10
Pas vraiment pertinent par rapport à la question initiale, mais je mettrais probablement le code dedans lib/core_ext/string.rb.
Jakob S

1
Je ne pense pas que le is_number?(string)bit fonctionne avec Ruby 1.9. Peut-être que cela fait partie de Rails ou 1.8? String.is_a?(Numeric)travaux. Voir aussi stackoverflow.com/questions/2095493/… .
Ross Attrill

30

Voici une référence pour les moyens courants de résoudre ce problème. Notez que celui que vous devriez utiliser dépend probablement du ratio de faux cas attendus.

  1. S'ils sont relativement rares, le casting est certainement le plus rapide.
  2. Si de faux cas sont courants et que vous ne faites que vérifier les entiers, la comparaison avec un état transformé est une bonne option.
  3. Si de faux cas sont courants et que vous vérifiez les flottants, l'expression rationnelle est probablement la voie à suivre

Si la performance n'a pas d'importance, utilisez ce que vous aimez. :-)

Détails de vérification des nombres entiers:

# 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

Détails de la vérification du flotteur:

# 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+$/n'est pas une expression régulière sûre dans Ruby, /\A\d+\Z/est. (par exemple, "42 \ nsome text" reviendrait true)
Timothee A

Pour clarifier le commentaire de @ TimotheeA, il est sûr de l'utiliser /^\d+$/s'il s'agit de lignes, mais dans ce cas, il s'agit du début et de la fin d'une chaîne /\A\d+\Z/.
Julio

1
Les réponses ne devraient-elles pas être modifiées pour changer la réponse réelle PAR le répondant? changer la réponse dans une modification si vous n'êtes pas le répondeur semble ... peut-être sournois et devrait être hors limites.
jaydel

2
\ Z permet d'avoir \ n à la fin de la chaîne, donc "123 \ n" passera la validation, même si ce n'est pas entièrement numérique. Mais si vous utilisez \ z, ce sera une expression rationnelle plus correcte: / \ A \ d + \ z /
SunnyMagadan

15

S'appuyer sur l'exception levée n'est pas la solution la plus rapide, lisible ou fiable.
Je ferais ce qui suit:

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

1
Cela ne fonctionne que pour les entiers positifs, cependant. Les valeurs telles que «-1», «0.0» ou «1_000» renvoient toutes false même s'il s'agit de valeurs numériques valides. Vous regardez quelque chose comme / ^ [- .0-9] + $ /, mais cela accepte à tort '- -'.
Jakob S

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

NoMethodError: méthode non définie `should 'pour" asd ": String
sergserg

Dans le dernier rspec, cela devientexpect(my_string).to match(/^[0-9]+$/)
Damien MATHIEU

J'aime: my_string =~ /\A-?(\d+)?\.?\d+\Z/il vous permet de faire ".1", "-0.1" ou "12" mais pas "" ou "-" ou "."
Josh le

8

Depuis Ruby 2.6.0, les méthodes de exceptionconversion numériques ont un argument optionnel [1] . Cela nous permet d'utiliser les méthodes intégrées sans utiliser d'exceptions comme flux de contrôle:

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

Par conséquent, vous n'avez pas à définir votre propre méthode, mais vous pouvez vérifier directement des variables comme par exemple

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

7

c'est comme ça que je le fais, mais je pense aussi qu'il doit y avoir un meilleur moyen

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

5
Il ne reconnaît pas la notation flottante, par exemple 1.2e + 35.
hipertracker

1
Dans Ruby 2.4.0 j'ai couru object = "1.2e+35"; object.to_f.to_s == objectet ça a marché
Giovanni Benussi

6

non, vous l'utilisez mal. votre numéro_is? a un argument. tu l'as appelé sans l'argument

tu devrais faire is_number? (mystring)


Basé sur is_number? méthode dans la question, en utilisant is_a? ne donne pas la bonne réponse. Si mystringest effectivement une chaîne, mystring.is_a?(Integer)sera toujours faux. On dirait qu'il veut un résultat commeis_number?("12.4") #=> true
Jakob S

Jakob S a raison. mystring est en effet toujours une chaîne, mais peut être uniquement composée de nombres. peut-être que ma question aurait dû être is_numeric? pour ne pas confondre le type de données
Jamie Buchanan

6

Tl; dr: Utilisez une approche regex. Il est 39 fois plus rapide que l'approche de sauvetage dans la réponse acceptée et gère également des cas tels que "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 réponse acceptée par @Jakob S fonctionne pour la plupart, mais attraper des exceptions peut être très lent. De plus, l'approche de sauvetage échoue sur une chaîne comme "1,000".

Définissons les méthodes:

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

Et maintenant quelques cas de test:

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

Et un peu de code pour exécuter les cas de 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

Voici la sortie des cas de 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

Il est temps de faire quelques benchmarks de performance:

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

Et les résultats:

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

Merci pour la référence. La réponse acceptée a l'avantage d'accepter des entrées comme 5.4e-29. Je suppose que votre expression régulière pourrait être modifiée pour les accepter également.
Jodi

3
Gérer des cas comme 1000 est vraiment difficile, car cela dépend de l'intention de l'utilisateur. Il existe de très nombreuses façons pour les humains de formater les nombres. 1 000 est-il à peu près égal à 1 000 ou à peu près égal à 1? La plupart du monde dit que c'est environ 1, ce n'est pas un moyen d'afficher le nombre entier 1000.
James Moore

4

Dans les rails 4, vous devez mettre require File.expand_path('../../lib', __FILE__) + '/ext/string' dans votre config / application.rb


1
en fait, vous n'avez pas besoin de faire cela, vous pouvez simplement mettre string.rb dans "initializers" et ça marche!
mahatmanich

3

Si vous préférez ne pas utiliser d'exceptions dans le cadre de la logique, vous pouvez essayer ceci:

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

Ou, si vous voulez qu'il fonctionne sur toutes les classes d'objets, remplacez-le class Stringpar class Objectun self converti en chaîne: !!(self.to_s =~ /^-?\d+(\.\d*)?$/)


Quel est le but de nier et de faire nil?zéro est vrai sur rubis, vous pouvez donc le faire juste!!(self =~ /^-?\d+(\.\d*)?$/)
Arnold Roa

L'utilisation !!fonctionne certainement. Au moins un guide de style Ruby ( github.com/bbatsov/ruby-style-guide ) a suggéré d'éviter !!en faveur de la .nil?lisibilité, mais j'ai vu !!utilisé dans des référentiels populaires, et je pense que c'est un bon moyen de convertir en booléen. J'ai édité la réponse.
Mark Schneider

-3

utilisez la fonction suivante:

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

alors,

is_numeric? "1.2f" = faux

is_numeric? "1.2" = vrai

is_numeric? "12f" = faux

is_numeric? "12" = vrai


Cela échouera si val est "0". Notez également que la méthode .tryne fait pas partie de la bibliothèque principale de Ruby et n'est disponible que si vous incluez ActiveSupport.
GMA du

En fait, cela échoue également "12", donc votre quatrième exemple dans cette question est faux. "12.10"et "12.00"échouer aussi.
GMA le

-5

À quel point cette solution est-elle stupide?

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

1
Ceci est sous-optimal car utiliser '.respond_to? (: +)' Est toujours mieux que d'échouer et d'attraper une exception sur un appel de méthode spécifique (: +). Cela peut également échouer pour diverses raisons si les méthodes Regex et de conversion ne le font pas.
Sqeaky
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.