Безопасный целочисленный синтаксический анализ в Ruby

У меня есть строка, скажем '123', и я хочу преобразовать ее в 123.

Я знаю, что вы просто можете сделать some_string.to_i, но преобразуете 'lolipops' в 0, что не является эффектом, который я имею в виду. Я хочу, чтобы это взорвалось в моем лице, когда я пытаюсь преобразовать что-то недействительное, с приятным и болезненным Exception. В противном случае я не могу различить действительный 0 и то, что просто не является числом.

РЕДАКТИРОВАТЬ: Я искал стандартный способ сделать это, без трюка regex.

Ответ 1

Ruby имеет встроенную функциональность:

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

Как отмечено в ответе Joseph Pecoraro, вы можете посмотреть строки, которые являются действительными не десятичными числами, например, начиная с 0x для hex и 0b для двоичных и потенциально более сложных чисел, начиная с нуля, который будет анализироваться как восьмеричный.

Ruby 1.9.2 добавил необязательный второй аргумент для radix, поэтому выше можно избежать:

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

Ответ 2

Также помните об изменениях, которые может иметь текущее принятое решение при разборе шестнадцатеричных, восьмеричных и двоичных чисел:

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

В номерах Ruby, начинающихся с 0x или 0x, являются hex, 0b или 0b являются двоичными, а просто 0 являются восьмеричными. Если это не желаемое поведение, вы можете объединить это с некоторыми другими решениями, которые проверяют, соответствует ли строка шаблону первым. Как и регулярные выражения /\d+/ и т.д.

Ответ 3

Это может сработать:

i.to_i if i.match(/^\d+$/)

Ответ 4

Другое неожиданное поведение с принятым решением (с 1.8, 1.9 в порядке):

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

поэтому, если вы не уверены, что передается, убедитесь, что вы добавили .to_s.

Ответ 5

Мне нравится ответ Майрона, но он страдает от болезни Ruby: "Я больше не использую Java/С#, поэтому я больше не буду использовать наследование". Открытие любого класса может быть чревато опасностью и должно использоваться экономно, особенно, когда оно входит в основную библиотеку Ruby. Я не говорю, что никогда не использовал его, но его обычно легко избежать и что есть лучшие варианты, например,

class IntegerInString < String

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

Затем, когда вы хотите использовать строку, которая может быть номером, она очищает то, что вы делаете, и вы не clobber любого основного класса, например.

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

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

Вы можете добавить в инициализацию всевозможные проверки, например, проверить двоичные числа и т.д. Главное, что Ruby для людей и для людей означает Ясность. Именование объекта через его имя переменной и его имя класса делают вещи намного яснее.

Ответ 6

Мне пришлось иметь дело с этим в моем последнем проекте, и моя реализация была похожа, но немного другая:

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

Ответ 7

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

Вероятно, это не самый чистый способ сделать это, но он должен работать.

Ответ 8

Re: Крис отвечает

Ваша реализация позволяет вещам типа "1a" или "b2". Как об этом:

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

Выводится:

100
1a is invalid
b2 is invalid
t is invalid