Análise inteira segura no Ruby

160

Eu tenho uma string, digamos '123', e quero convertê-la para o número inteiro 123.

Eu sei que você pode simplesmente fazer some_string.to_i, mas isso se converte 'lolipops'em 0, o que não é o efeito que tenho em mente. Quero explodir na minha cara quando tento converter algo inválido, com uma agradável e dolorosa Exception. Caso contrário, não consigo distinguir entre um válido 0e algo que simplesmente não é um número.

Edição: Eu estava procurando a maneira padrão de fazê-lo, sem truques regex.

wvdschel
fonte

Respostas:

234

O Ruby tem essa funcionalidade incorporada:

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

Conforme observado na resposta de Joseph Pecoraro , convém observar as strings que são números não decimais válidos, como aqueles que começam com 0xhex e 0bbinário, e números potencialmente mais complicados, começando com zero e que serão analisados ​​como octais.

O Ruby 1.9.2 adicionou o segundo argumento opcional para o 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
Slartibartfast
fonte
27

Isso pode funcionar:

i.to_i if i.match(/^\d+$/)
Purfideas
fonte
8
PSA: em Ruby, ^e $ têm significados sutilmente diferentes como metacarpos do que na maioria dos outros sabores de expressões regulares. Você provavelmente pretende usar \Ae em \Zvez disso.
pje 16/05
1
para ser pedante, a menção de diferentes âncoras regex conforme @pje pode estar incorreta, dependendo do comportamento desejado. Em vez disso, considere usar \zno lugar de, \Zpois a descrição da âncora Z maiúscula é: "Corresponde ao final da string. Se a string termina com uma nova linha, ela corresponde imediatamente antes da nova linha" - ruby-doc.org/core-2.1.1/Regexp .html
Del
24

Também esteja ciente 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

Nos números Ruby, que começam com 0xou 0Xsão hexadecimais 0bou 0Bbinários e 0são octais. Se esse não for o comportamento desejado, convém combiná-lo com algumas das outras soluções que verificam se a sequência corresponde primeiro a um padrão. Como as /\d+/expressões regulares, etc.

Joseph Pecoraro
fonte
1
Isso é o que eu esperaria da conversão embora
wvdschel 08/08/09
5
No Ruby 1.9, você pode passar a base como um segundo argumento.
Andrew Grimm
17

Outro comportamento inesperado com a solução aceita (com 1,8, 1,9 está ok):

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

portanto, se você não tiver certeza do que está sendo transmitido, adicione a .to_s.

Jaime Cham
fonte
7
teste em Ruby 1.9. Inteiro (: foobar) => não é possível converter o símbolo em Inteiro (TypeError)
GutenYe
9

I como a resposta de Myron mas sofre da doença de Ruby de "eu já não usar Java / C # para que eu nunca vou herança uso novamente" . A abertura de qualquer classe pode estar repleta de perigos e deve ser usada com moderação, especialmente quando faz parte da biblioteca principal do Ruby. Não estou dizendo que nunca o use, mas geralmente é fácil evitar e que existem melhores opções disponíveis, por exemplo

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 possa ser um número, fica claro o que você está fazendo e você não derruba 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 verificar números binários etc. O principal, porém, é que Ruby é para pessoas e ser para pessoas significa clareza . Nomear um objeto por meio de seu nome de variável e nome de classe torna as coisas muito mais claras.

iain
fonte
6

Eu tive que lidar com isso no 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

fonte
2
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.

Paul Wicks
fonte
1

Re: Resposta de Chris

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 gera:

100
1a is invalid
b2 is invalid
t is invalid
metavida
fonte
para ser pedante, a menção de diferentes âncoras regex de acordo com @pje e usadas pode estar incorreta, dependendo do comportamento desejado. Em vez disso, considere usar \zno lugar de, \Zpois a descrição da âncora Z maiúscula é: "Corresponde ao final da string. Se a string termina com uma nova linha, ela corresponde imediatamente antes da nova linha" - ruby-doc.org/core-2.1.1/Regexp .html
Del