Análise segura de números inteiros em Ruby
-
09-06-2019 - |
Pergunta
Eu tenho uma corda, digamos '123'
, e quero convertê-lo para 123
.
Eu sei que você pode simplesmente fazer some_string.to_i
, mas isso converte 'lolipops'
para 0
, que não é o efeito que tenho em mente.Quero que isso exploda na minha cara quando tento converter algo inválido, com um sentimento agradável e doloroso. Exception
.Caso contrário, não consigo distinguir entre um válido 0
e algo que simplesmente não é um número.
EDITAR: Eu estava procurando a maneira padrão de fazer isso, sem truques de regex.
Solução
Ruby tem esta funcionalidade incorporada:
Integer('1001') # => 1001
Integer('1001 nights')
# ArgumentError: invalid value for Integer: "1001 nights"
Conforme observado na resposta de José Pecoraro, talvez você queira observar strings que sejam números não decimais válidos, como aqueles que começam com 0x
para hexadecimal e 0b
para números binários e potencialmente mais complicados começando com zero que serão analisados como octais.
Ruby 1.9.2 adicionou um segundo argumento opcional para radix para que o problema acima possa ser evitado:
Integer('23') # => 23
Integer('0x23') # => 35
Integer('023') # => 19
Integer('0x23', 10)
# => #<ArgumentError: invalid value for Integer: "0x23">
Integer('023', 10) # => 23
Outras dicas
Isso pode funcionar:
i.to_i if i.match(/^\d+$/)
Esteja ciente também dos efeitos que a solução atualmente aceita pode ter na análise de números hexadecimais, octais e binários:
>> Integer('0x15')
# => 21
>> Integer('0b10')
# => 2
>> Integer('077')
# => 63
Em números Ruby que começam com 0x
ou 0X
são hexadecimais, 0b
ou 0B
são binários e apenas 0
são octais.Se este não for o comportamento desejado, você pode querer combiná-lo com algumas das outras soluções que verificam primeiro se a string corresponde a um padrão.Como o /\d+/
expressões regulares, etc.
Outro comportamento inesperado com a solução aceita (com 1.8, 1.9 está ok):
>> Integer(:foobar)
=> 26017
>> Integer(:yikes)
=> 26025
então, se você não tiver certeza do que está sendo transmitido, adicione um .to_s
.
Gosto da resposta de Myron, mas ela sofre da doença Ruby de "Eu não uso mais Java/C#, então nunca mais usarei herança".Abrir qualquer aula pode ser muito perigoso e deve ser usado com moderação, especialmente quando faz parte da biblioteca principal do Ruby.Não estou dizendo para nunca usá-lo, mas geralmente é fácil de evitar e existem opções melhores disponíveis, por 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
Então, quando você deseja usar uma string que pode ser um número, fica claro o que você está fazendo e você não destrói nenhuma classe principal, por exemplo.
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.
Você pode adicionar todos os tipos de outras verificações na inicialização, como verificação de números binários, etc.O principal, porém, é que Ruby é para pessoas e ser para pessoas significa clareza.Nomeando um objeto através do nome de sua variável e seu nome de classe faz as coisas muito mais claro.
Tive que lidar com isso em meu último projeto, e minha implementação foi semelhante, mas um pouco diferente:
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
Provavelmente não é a maneira mais limpa de fazer isso, mas deve funcionar.
Sua implementação permite coisas como "1a" ou "b2".Que tal isso:
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
Isso produz:
100
1a is invalid
b2 is invalid
t is invalid