Análisis de enteros seguros en Ruby

160

Tengo una cadena, por ejemplo '123', y quiero convertirla al número entero 123.

Sé que simplemente puedes hacerlo some_string.to_i, pero eso se convierte 'lolipops'en 0, que no es el efecto que tengo en mente. Quiero que me explote en la cara cuando trato de convertir algo inválido, con algo agradable y doloroso Exception. De lo contrario, no puedo distinguir entre un valor válido 0y algo que simplemente no es un número en absoluto.

EDITAR: Estaba buscando la forma estándar de hacerlo, sin trucos regex.

wvdschel
fuente

Respuestas:

234

Ruby tiene esta funcionalidad incorporada:

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

Como se señaló en la respuesta de Joseph Pecoraro , es posible que desee ver las cadenas que son números no decimales válidos, como los que comienzan con 0xhexadecimal y 0bbinario, y los números potencialmente más difíciles que comienzan con cero que se analizarán como octales.

Ruby 1.9.2 agregó un segundo argumento opcional para la raíz, por lo que se puede evitar el problema anterior:

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

Esto podría funcionar:

i.to_i if i.match(/^\d+$/)
Purfideas
fuente
8
PSA: en Ruby, ^y $ tienen significados sutilmente diferentes como metacars que en la mayoría de los otros sabores regexp. Probablemente quieras usar \Ay en su \Zlugar.
pje
1
para ser pedante, la mención de diferentes anclajes de expresiones regulares según @pje puede ser incorrecta dependiendo del comportamiento deseado. En su lugar, considere usar \zen lugar de, \Zya que la descripción para el ancla Z en mayúscula es: "Coincide con el final de la cadena. Si la cadena termina con una nueva línea, coincide justo antes de la nueva línea" - ruby-doc.org/core-2.1.1/Regexp .html
Del
24

También tenga en cuenta los efectos que la solución actual aceptada puede tener al analizar números hexadecimales, octales y binarios:

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

En Ruby, los números que comienzan con 0xo 0Xson hexadecimales, 0bo 0Bson binarios, y solo 0son octales. Si este no es el comportamiento deseado, es posible que desee combinarlo con algunas de las otras soluciones que verifican si la cadena coincide primero con un patrón. Como las /\d+/expresiones regulares, etc.

Joseph Pecoraro
fuente
1
Sin embargo
wvdschel
55
En Ruby 1.9, puede pasar la base como un segundo argumento.
Andrew Grimm
17

Otro comportamiento inesperado con la solución aceptada (con 1.8, 1.9 está bien):

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

así que si no está seguro de lo que se está pasando, asegúrese de agregar un .to_s.

Jaime Cham
fuente
77
prueba en Ruby 1.9. Entero (: foobar) => no se puede convertir el símbolo en entero (TypeError)
GutenYe
9

Me gusta la respuesta de Myron, pero sufre la enfermedad de Ruby de "Ya no uso Java / C #, así que nunca volveré a usar la herencia" . Abrir cualquier clase puede estar lleno de peligros y debe usarse con moderación, especialmente cuando es parte de la biblioteca principal de Ruby. No digo que nunca lo use, pero generalmente es fácil de evitar y que hay mejores opciones disponibles, por ejemplo

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

Luego, cuando desee usar una cadena que podría ser un número, está claro lo que está haciendo y no golpea ninguna clase central, por ejemplo

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.

Puede agregar todo tipo de otras comprobaciones en la inicialización, como verificar números binarios, etc. Sin embargo, lo principal es que Ruby es para las personas y ser para las personas significa claridad . Nombrar un objeto a través de su nombre de variable y su nombre de clase hace las cosas mucho más claras.

iain
fuente
6

Tuve que lidiar con esto en mi último proyecto, y mi implementación fue similar, pero un poco 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

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

Probablemente no sea la forma más limpia de hacerlo, pero debería funcionar.

Paul Wicks
fuente
1

Re: la respuesta de Chris

Su implementación deja pasar cosas como "1a" o "b2". ¿Qué tal esto en su lugar:

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

Esto produce:

100
1a is invalid
b2 is invalid
t is invalid
metavida
fuente
para ser pedante, la mención de diferentes anclas de expresiones regulares según @pje y usadas puede ser incorrecta dependiendo del comportamiento deseado. En su lugar, considere usar \zen lugar de, \Zya que la descripción para el ancla Z en mayúscula es: "Coincide con el final de la cadena. Si la cadena termina con una nueva línea, coincide justo antes de la nueva línea" - ruby-doc.org/core-2.1.1/Regexp .html
Del