Безопасный синтаксический анализ целых чисел в Ruby
-
09-06-2019 - |
Вопрос
Скажем, у меня есть строка '123'
, и я хочу преобразовать его в 123
.
Я знаю, что ты можешь просто сделать some_string.to_i
, но это преобразует 'lolipops'
Для 0
, это не тот эффект, который я имею в виду.Я хочу, чтобы это взорвалось у меня перед носом, когда я попытаюсь преобразовать что-то недействительное, с приятным и болезненным Exception
.В противном случае я не могу отличить действительный 0
и что-то, что вообще не является числом.
Редактировать: Я искал стандартный способ сделать это, без обмана с регулярными выражениями.
Решение
В Ruby встроена такая функциональность:
Integer('1001') # => 1001
Integer('1001 nights')
# ArgumentError: invalid value for Integer: "1001 nights"
Как отмечено в ответе Джозеф Пекораро, возможно, вам захочется понаблюдать за строками, которые являются допустимыми десятичными числами, например, начинающимися с 0x
для шестигранных и 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
Другие советы
Это может сработать:
i.to_i if i.match(/^\d+$/)
Также имейте в виду, какое влияние текущее принятое решение может оказать на анализ шестнадцатеричных, восьмеричных и двоичных чисел:
>> Integer('0x15')
# => 21
>> Integer('0b10')
# => 2
>> Integer('077')
# => 63
В Ruby числа , начинающиеся с 0x
или 0X
являются шестнадцатеричными, 0b
или 0B
являются двоичными, и просто 0
являются восьмеричными.Если это нежелательное поведение, вы можете комбинировать его с некоторыми другими решениями, которые сначала проверяют, соответствует ли строка шаблону.Как в /\d+/
регулярные выражения и т.д.
Еще одно неожиданное поведение при принятом решении (с 1.8, 1.9 все в порядке):
>> Integer(:foobar)
=> 26017
>> Integer(:yikes)
=> 26025
поэтому, если вы не уверены, что передается, обязательно добавьте .to_s
.
Мне нравится ответ Майрона, но он страдает рубиновой болезнью "Я больше не использую Java / C #, поэтому никогда больше не собираюсь использовать наследование".Открытие любого класса может быть сопряжено с опасностью, и его следует использовать с осторожностью, особенно когда это часть основной библиотеки Ruby.Я не говорю, что никогда не используйте это, но обычно этого легко избежать, и что есть лучшие варианты, например
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
Затем, когда вы хотите использовать строку, которая может быть числом, становится ясно, что вы делаете, и вы не загромождаете какой-либо базовый класс, например
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.
Вы можете добавить всевозможные другие проверки при инициализации, например проверку наличия двоичных чисел и т.д.Но главное, что Ruby предназначен для людей, а быть для людей означает ясность.Присвоение имени объекту через имя его переменной и его имя класса создает вещи многое понятнее.
Мне пришлось иметь дело с этим в моем последнем проекте, и моя реализация была похожей, но немного другой:
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
Вероятно, не самый чистый способ сделать это, но он должен сработать.
Ре: Ответ Криса
Ваша реализация пропускает такие вещи, как "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