Analyse sécurisée d'entiers dans Ruby
-
09-06-2019 - |
Question
J'ai une chaîne, dis '123'
, et je veux le convertir en 123
.
Je sais que tu peux simplement le faire some_string.to_i
, mais cela convertit 'lolipops'
à 0
, ce qui n'est pas l'effet que j'ai en tête.Je veux que ça m'explose au visage quand j'essaie de convertir quelque chose d'invalide, avec un bruit agréable et douloureux Exception
.Sinon, je ne peux pas faire la distinction entre un valide 0
et quelque chose qui n'est tout simplement pas un nombre.
MODIFIER: Je cherchais la manière standard de le faire, sans supercherie des regex.
La solution
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 souhaiterez peut-être surveiller les chaînes qui sont des nombres non décimaux valides, tels que ceux commençant par 0x
pour l'hexagone et 0b
pour les nombres binaires et potentiellement plus délicats commençant par zéro qui seront analysés comme octaux.
Ruby 1.9.2 a ajouté un deuxième argument facultatif 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
Autres conseils
Cela pourrait fonctionner :
i.to_i if i.match(/^\d+$/)
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
En Ruby, les nombres commençant par 0x
ou 0X
sont hexagonaux, 0b
ou 0B
sont binaires, et juste 0
sont octaux.Si ce n'est pas le comportement souhaité, vous souhaiterez peut-être le combiner avec certaines des autres solutions qui vérifient d'abord si la chaîne correspond à un modèle.Comme le /\d+/
expressions régulières, etc.
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 .to_s
.
J'aime la réponse de Myron mais il souffre de la maladie Ruby de "Je n'utilise plus Java/C# donc je n'utiliserai plus jamais l'héritage".L'ouverture de n'importe quel cours peut être pleine de dangers et doit être utilisée avec parcimonie, en particulier quand il fait partie de la bibliothèque principale de Ruby.Je ne dis pas de ne jamais l'utiliser, mais il est généralement facile de l'éviter et il existe de meilleures options disponibles, par ex.
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 n'encombrez aucune classe principale, par ex.
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 lors de l'initialisation, comme la vérification des nombres binaires, etc.L'essentiel cependant, c'est que Ruby est pour les gens et qu'être pour les gens signifie clarté.Nommer un objet via son nom de variable et son nom de classe fait des choses beaucoup plus clair.
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
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.
Concernant: La réponse de Chris
Votre implémentation laisse passer des éléments 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