Sichere Ganzzahlanalyse in Ruby

160

Ich habe beispielsweise eine Zeichenfolge '123'und möchte sie in eine Ganzzahl konvertieren 123.

Ich weiß, dass Sie es einfach tun können some_string.to_i, aber das wandelt sich 'lolipops'in um 0, was nicht der Effekt ist, an den ich denke . Ich möchte, dass es mir ins Gesicht sprengt, wenn ich versuche, etwas Ungültiges mit einem schönen und schmerzhaften umzuwandeln Exception. Ansonsten kann ich nicht zwischen einem gültigen 0und etwas unterscheiden, das überhaupt keine Zahl ist.

EDIT: Ich habe nach der Standardmethode gesucht, ohne Regex-Tricks.

wvdschel
quelle

Antworten:

234

Ruby hat diese Funktionalität eingebaut:

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

Wie in der Antwort von Joseph Pecoraro erwähnt , möchten Sie möglicherweise nach Zeichenfolgen suchen, die gültige Nicht-Dezimalzahlen sind, z. B. mit 0xhexadezimal und 0bbinär, und möglicherweise schwierigere Zahlen, die mit null beginnen und als oktal analysiert werden.

Ruby 1.9.2 hat ein optionales zweites Argument für radix hinzugefügt, damit das obige Problem vermieden werden kann:

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

Dies könnte funktionieren:

i.to_i if i.match(/^\d+$/)
Purfideas
quelle
8
PSA: in Ruby ^und $ haben subtil andere Bedeutungen als Metachars als in den meisten anderen Regexp-Geschmacksrichtungen. Sie wollen wahrscheinlich \Aund \Zstattdessen verwenden.
pje
1
Um pedantisch zu sein, kann die Erwähnung verschiedener Regex-Anker gemäß @pje je nach gewünschtem Verhalten falsch sein. Verwenden Sie stattdessen \zanstelle von \Zals Beschreibung für den großgeschriebenen Z-Anker: "Entspricht dem Ende der Zeichenfolge. Wenn die Zeichenfolge mit einer neuen Zeile endet, stimmt sie kurz vor der neuen Zeile
Del
24

Beachten Sie auch die Auswirkungen, die die derzeit akzeptierte Lösung auf das Parsen von Hex-, Oktal- und Binärzahlen haben kann:

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

In Ruby-Zahlen, die mit 0xoder 0Xhexadezimal 0boder 0Bbinär beginnen und nur 0oktal sind. Wenn dies nicht das gewünschte Verhalten ist, können Sie dies mit einigen anderen Lösungen kombinieren, die prüfen, ob die Zeichenfolge zuerst mit einem Muster übereinstimmt. Wie die /\d+/regulären Ausdrücke usw.

Joseph Pecoraro
quelle
1
Das würde ich allerdings von der Konvertierung erwarten
wvdschel
5
In Ruby 1.9 können Sie die Basis als zweites Argument übergeben.
Andrew Grimm
17

Ein weiteres unerwartetes Verhalten mit der akzeptierten Lösung (mit 1.8, 1.9 ist in Ordnung):

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

Wenn Sie also nicht sicher sind, was übergeben wird, stellen Sie sicher, dass Sie a hinzufügen .to_s.

Jaime Cham
quelle
7
Test in Ruby 1.9. Integer (: foobar) => Symbol kann nicht in Integer (TypeError)
konvertiert werden
9

Ich mag Myrons Antwort, aber sie leidet unter der Ruby-Krankheit "Ich verwende kein Java / C # mehr, daher werde ich nie wieder Vererbung verwenden" . Das Öffnen einer Klasse kann mit Gefahren verbunden sein und sollte sparsam eingesetzt werden, insbesondere wenn es Teil von Rubys Kernbibliothek ist. Ich sage nicht, dass Sie es niemals verwenden sollten, aber es ist normalerweise leicht zu vermeiden und es gibt bessere Optionen, z

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

Wenn Sie dann eine Zeichenfolge verwenden möchten, die eine Zahl sein kann, ist klar, was Sie tun, und Sie blockieren keine Kernklasse, z

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.

Sie können bei der Initialisierung alle möglichen anderen Überprüfungen hinzufügen, z. B. das Überprüfen auf Binärzahlen usw. Die Hauptsache ist jedoch, dass Ruby für Menschen ist und für Menschen zu sein bedeutet Klarheit . Das Benennen eines Objekts über seinen Variablennamen und seinen Klassennamen macht die Dinge viel klarer.

iain
quelle
6

Ich musste mich in meinem letzten Projekt damit befassen, und meine Implementierung war ähnlich, aber etwas anders:

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

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

Wahrscheinlich nicht der sauberste Weg, sollte aber funktionieren.

Paul Wicks
quelle
1

Re: Chris Antwort

Ihre Implementierung lässt Dinge wie "1a" oder "b2" durch. Wie wäre es stattdessen damit:

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

Dies gibt aus:

100
1a is invalid
b2 is invalid
t is invalid
Metavida
quelle
Um pedantisch zu sein, kann die Erwähnung verschiedener Regex-Anker gemäß @pje und verwendet je nach gewünschtem Verhalten falsch sein. Verwenden Sie stattdessen \zanstelle von \Zals Beschreibung für den großgeschriebenen Z-Anker: "Entspricht dem Ende der Zeichenfolge. Wenn die Zeichenfolge mit einer neuen Zeile endet, stimmt sie kurz vor der neuen Zeile
Del