Analyse sûre des entiers dans Ruby

160

J'ai une chaîne, disons '123', et je veux la convertir en entier 123.

Je sais que vous pouvez le faire simplement some_string.to_i, mais cela se transforme 'lolipops'en 0, ce qui n'est pas l'effet que j'ai à l'esprit. Je veux que ça me saute au visage quand j'essaye de convertir quelque chose d'invalide, avec un joli et douloureux Exception. Sinon, je ne peux pas faire la distinction entre un valide 0et quelque chose qui n'est tout simplement pas un nombre.

EDIT: Je cherchais la manière standard de le faire, sans ruse regex.

wvdschel
la source

Réponses:

234

Ruby a cette fonctionnalité intégrée:

Integer('1001')                                    # => 1001  
Integer('1001 nights')  
# ArgumentError: invalid value for Integer: "1001 nights"  

Comme indiqué dans la réponse de Joseph Pecoraro , vous voudrez peut-être rechercher des chaînes qui sont des nombres non décimaux valides, comme celles commençant par 0xfor hex et 0bfor binary, et des nombres potentiellement plus délicats commençant par zéro qui seront analysés en octal.

Ruby 1.9.2 a ajouté un deuxième argument optionnel pour radix afin que le problème ci-dessus puisse être évité:

Integer('23')                                     # => 23
Integer('0x23')                                   # => 35
Integer('023')                                    # => 19
Integer('0x23', 10)
# => #<ArgumentError: invalid value for Integer: "0x23">
Integer('023', 10)                                # => 23
Slartibartfast
la source
27

Cela pourrait fonctionner:

i.to_i if i.match(/^\d+$/)
Purfideas
la source
8
PSA: en Ruby, ^et $ ont des significations subtilement différentes en tant que métachars que dans la plupart des autres saveurs de regexp. Vous voulez probablement utiliser \Aet à la \Zplace.
pje
1
pour être pédant, la mention de différentes ancres regex selon @pje peut être incorrecte en fonction du comportement souhaité. Au lieu de cela, envisagez d'utiliser \zà la place de \Zcar la description de l'ancre Z majuscule est: "Correspond à la fin de la chaîne. Si la chaîne se termine par une nouvelle ligne, elle correspond juste avant la nouvelle ligne" - ruby-doc.org/core-2.1.1/Regexp .html
Del
24

Soyez également conscient des effets que la solution actuellement acceptée peut avoir sur l'analyse des nombres hexadécimaux, octaux et binaires:

>> Integer('0x15')
# => 21  
>> Integer('0b10')
# => 2  
>> Integer('077')
# => 63

Dans Ruby, les nombres commençant par 0xou 0Xsont hexadécimaux, 0bou 0Bbinaires, et 0sont simplement octaux. Si ce n'est pas le comportement souhaité, vous pouvez combiner cela avec certaines des autres solutions qui vérifient si la chaîne correspond d'abord à un modèle. Comme les /\d+/expressions régulières, etc.

Joseph Pecoraro
la source
1
C'est ce que j'attendais de la conversion
wvdschel
5
Dans Ruby 1.9, vous pouvez passer la base comme second argument.
Andrew Grimm
17

Un autre comportement inattendu avec la solution acceptée (avec 1.8, 1.9 est ok):

>> Integer(:foobar)
=> 26017
>> Integer(:yikes)
=> 26025

donc si vous n'êtes pas sûr de ce qui est transmis, assurez-vous d'ajouter un fichier .to_s.

Jaime Cham
la source
7
test dans Ruby 1.9. Integer (: foobar) => impossible de convertir le symbole en entier (TypeError)
GutenYe
9

J'aime la réponse de Myron mais il souffre de la maladie de Ruby "Je n'utilise plus Java / C # donc je n'utiliserai plus jamais l'héritage" . L'ouverture de n'importe quelle classe peut être dangereuse et doit être utilisée avec parcimonie, en particulier lorsqu'elle fait partie de la bibliothèque principale de Ruby. Je ne dis pas de ne jamais l'utiliser, mais c'est généralement facile à éviter et qu'il existe de meilleures options disponibles, par exemple

class IntegerInString < String

  def initialize( s )
    fail ArgumentError, "The string '#{s}' is not an integer in a string, it's just a string." unless s =~ /^\-?[0-9]+$/
    super
  end
end

Ensuite, lorsque vous souhaitez utiliser une chaîne qui pourrait être un nombre, ce que vous faites est clair et vous ne frappez aucune classe de base, par exemple

n = IntegerInString.new "2"
n.to_i
# => 2

IntegerInString.new "blob"
ArgumentError: The string 'blob' is not an integer in a string, it's just a string.

Vous pouvez ajouter toutes sortes d'autres vérifications dans l'initialisation, comme la vérification des nombres binaires, etc. L'essentiel cependant, c'est que Ruby est pour les gens et être pour les gens signifie la clarté . Nommer un objet via son nom de variable et son nom de classe rend les choses beaucoup plus claires.

iain
la source
6

J'ai dû gérer cela dans mon dernier projet, et ma mise en œuvre était similaire, mais un peu différente:

class NotAnIntError < StandardError 
end

class String
  def is_int?    
    self =~ /^-?[0-9]+$/
  end

  def safe_to_i
    return self.to_i if is_int?
    raise NotAnIntError, "The string '#{self}' is not a valid integer.", caller
  end
end

class Integer
  def safe_to_i
    return self
  end            
end

class StringExtensions < Test::Unit::TestCase

  def test_is_int
    assert "98234".is_int?
    assert "-2342".is_int?
    assert "02342".is_int?
    assert !"+342".is_int?
    assert !"3-42".is_int?
    assert !"342.234".is_int?
    assert !"a342".is_int?
    assert !"342a".is_int?
  end

  def test_safe_to_i
    assert 234234 == 234234.safe_to_i
    assert 237 == "237".safe_to_i
    begin
      "a word".safe_to_i
      fail 'safe_to_i did not raise the expected error.'
    rescue NotAnIntError 
      # this is what we expect..
    end
  end

end

la source
2
someString = "asdfasd123"
number = someString.to_i
if someString != number.to_s
  puts "oops, this isn't a number"
end

Ce n'est probablement pas la façon la plus propre de le faire, mais cela devrait fonctionner.

Paul Wicks
la source
1

Re: la réponse de Chris

Votre implémentation laisse passer des choses comme "1a" ou "b2". Que diriez-vous plutôt de ceci:

def safeParse2(strToParse)
  if strToParse =~ /\A\d+\Z/
    strToParse.to_i
  else
    raise Exception
  end
end

["100", "1a", "b2", "t"].each do |number|
  begin
    puts safeParse2(number)
  rescue Exception
    puts "#{number} is invalid"
  end
end

Cela produit:

100
1a is invalid
b2 is invalid
t is invalid
metavida
la source
pour être pédant, la mention de différentes ancres regex selon @pje et utilisées peut être incorrecte en fonction du comportement souhaité. Au lieu de cela, envisagez d'utiliser \zà la place de \Zcar la description de l'ancre Z majuscule est: "Correspond à la fin de la chaîne. Si la chaîne se termine par une nouvelle ligne, elle correspond juste avant la nouvelle ligne" - ruby-doc.org/core-2.1.1/Regexp .html
Del