Безпечний розбір цілих чисел у Ruby


160

Скажімо '123', у мене є рядок, і я хочу перетворити його на ціле число 123.

Я знаю, що ви можете просто зробити some_string.to_i, але це перетворюється 'lolipops'на це 0, що я не маю на увазі. Я хочу, щоб воно спалахнуло мені в обличчя, коли я намагаюся перетворити щось недійсне, приємне і болісне Exception. Інакше я не можу розрізнити дійсне 0і те, що просто зовсім не є числом.

EDIT: Я шукав стандартний спосіб зробити це без хитрості.

Відповіді:


234

У 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

27

Це може спрацювати:

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

8
PSA: в Ruby ^і $ мають тонко різні значення як метахари, ніж у більшості інших ароматів регулярного вибору. Ви, мабуть, маєте на увазі використовувати \Aі \Zзамість цього.
п’ят

1
щоб бути педантичним, згадка про різні регекс-анкери відповідно до @pje може бути невірною залежно від бажаної поведінки. Замість цього розгляньте використання \zзамість \Zопису з великого якоря Z: "Збігається кінець рядка. Якщо рядок закінчується новим
Del

24

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

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

У Ruby числа, які починаються з 0xабо 0Xшістнадцяткові, 0bабо 0Bдвійкові, і просто 0вісімкові. Якщо це не бажана поведінка, можливо, ви захочете поєднати це з деякими іншими рішеннями, які перевіряють, чи спочатку рядок відповідає шаблону. Як і /\d+/регулярні вирази тощо.


1
Саме цього я і очікував від конверсії
wvdschel

5
У Ruby 1.9 ви можете передавати базу як другий аргумент.
Ендрю Грімм

17

Ще одна несподівана поведінка з прийнятим рішенням (з 1,8, 1,9 нормально):

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

тому якщо ви не впевнені, що передано, переконайтеся, що ви додали .to_s.


7
тест в Ruby 1.9. Integer (: foobar) => не може перетворити символ у цілий (TypeError)
GutenYe

9

Мені подобається відповідь Майрона, але вона страждає на хворобу Рубі: "Я більше не використовую 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 - це для людей, а для людей - це ясність . Іменування об'єкта через його ім'я змінної та його клас робить речі набагато зрозумілішими.


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

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

Напевно, це не найчистіший спосіб зробити це, але треба працювати.


1

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

щоб бути педантичним, згадка про різні регекс-анкери відповідно до @pje та використаних може бути невірною залежно від бажаної поведінки. Замість цього розгляньте використання \zзамість \Zопису з великого якоря Z: "Збігається кінець рядка. Якщо рядок закінчується новим
Del
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.