Bezpieczne analizowanie liczb całkowitych w Rubim


160

Powiedzmy '123', że mam ciąg znaków i chcę go zamienić na liczbę całkowitą 123.

Wiem, że można po prostu zrobić some_string.to_i, ale który konwertuje 'lolipops'do 0, który nie jest efektem mam na myśli. Chcę, żeby wybuchło mi w twarz, kiedy próbuję przerobić coś nieważnego, na przyjemny i bolesny Exception. W przeciwnym razie nie mogę odróżnić prawidłowego od 0czegoś, co w ogóle nie jest liczbą.

EDYCJA: Szukałem standardowego sposobu zrobienia tego, bez sztuczek regex.

Odpowiedzi:


234

Ruby ma wbudowaną funkcjonalność:

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

Jak zauważył w odpowiedzi Joseph Pecoraro , możesz chcieć szukać ciągów, które są prawidłowymi liczbami niedziesiętnymi, takimi jak te zaczynające się 0xod szesnastkowego i 0bbinarnego, oraz potencjalnie bardziej skomplikowanych liczb zaczynających się od zera, które zostaną przeanalizowane jako ósemkowe.

Ruby 1.9.2 dodał opcjonalny drugi argument dla radix, więc można uniknąć powyższego problemu:

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

27

To może zadziałać:

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

8
PSA: w Rubim ^i $ mają nieco inne znaczenia jako metachary niż w większości innych rodzajów wyrażeń regularnych. Prawdopodobnie masz zamiar użyć \Ai \Zzamiast tego.
pje

1
mówiąc pedantycznie, wzmianka o różnych zakotwiczeniach wyrażeń regularnych zgodnie z @pje może być niepoprawna w zależności od pożądanego zachowania. Zamiast tego rozważ użycie \zzamiast znaku\Z Z jako opisu zakotwiczenia Z wielką literą: „Dopasowuje koniec ciągu. Jeśli ciąg kończy się znakiem nowej linii, dopasowuje się tuż przed nową
Del

24

Należy również pamiętać o wpływie, jaki obecnie akceptowane rozwiązanie może mieć na analizę liczb szesnastkowych, ósemkowych i binarnych:

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

W liczbach Ruby, które rozpoczynają się 0xlub 0Xsą hex, 0balbo 0Bsą binarne, a tylko 0są ósemkowym. Jeśli nie jest to pożądane zachowanie, możesz chcieć połączyć to z niektórymi innymi rozwiązaniami, które najpierw sprawdzają, czy ciąg pasuje do wzorca. Podobnie jak /\d+/wyrażenia regularne itp.


1
Tego właśnie oczekiwałbym po konwersji
wvdschel

5
W Rubim 1.9 możesz przekazać podstawę jako drugi argument.
Andrew Grimm,

17

Kolejne nieoczekiwane zachowanie z zaakceptowanym rozwiązaniem (z 1.8, 1.9 jest ok):

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

więc jeśli nie masz pewności, co jest przekazywane, dodaj rozszerzenie .to_s.


7
test w Rubim 1.9. Integer (: foobar) => nie można przekonwertować symbolu na
liczbę

9

Podoba mi się odpowiedź Myrona, ale cierpi ona na chorobę Ruby: „Nie używam już Java / C #, więc nigdy więcej nie będę używać dziedziczenia” . Otwieranie dowolnej klasy może być najeżone niebezpieczeństwami i powinno być używane oszczędnie, zwłaszcza gdy jest to część podstawowej biblioteki Rubiego. Nie mówię, że nigdy go nie używaj, ale zwykle jest to łatwe do uniknięcia i dostępne są lepsze opcje, np

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

Wtedy, gdy chcesz użyć łańcucha, który może być liczbą, jasne jest, co robisz i nie przebijasz żadnej klasy podstawowej, np.

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.

Podczas inicjalizacji możesz dodać różnego rodzaju inne sprawdzenia, takie jak sprawdzanie liczb binarnych itp. Najważniejsze jest jednak to, że Ruby jest dla ludzi, a bycie dla ludzi oznacza przejrzystość . Nazywanie obiektu poprzez nazwę jego zmiennej i nazwę klasy sprawia, że ​​wszystko jest dużo jaśniejsze.


6

Musiałem sobie z tym poradzić w moim ostatnim projekcie, a moja realizacja była podobna, ale trochę inna:

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

Prawdopodobnie nie jest to najczystszy sposób, ale powinien działać.


1

Re: odpowiedź Chrisa

Twoja implementacja pozwala na przejście przez takie rzeczy jak „1a” lub „b2”. A może zamiast tego:

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

To daje:

100
1a is invalid
b2 is invalid
t is invalid

mówiąc pedantycznie, wzmianka o różnych zakotwiczeniach wyrażeń regularnych zgodnie z @pje i użytych może być niepoprawna w zależności od pożądanego zachowania. Zamiast tego zastanów się nad użyciem \zzamiast znaku\Z zakotwiczenia Z wielką literą: „Dopasowuje koniec ciągu. Jeśli ciąg kończy się znakiem nowej linii, pasuje tuż przed nową
Del
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.