Ruby-Programmierung: Zahlen

Zurück zum Inhaltsverzeichnis.

Diese Seite behandelt Zahlen, dabei ist es wichtig zunächst zwischen verschiedenen Arten von Zahlen zu unterscheiden. Es gibt ganze Zahlen (Integer) oder gebrochene Zahlen (Floating Point Numbers). Diese beiden Grundtypen sind bei einem Computer die Wichtigsten und finden häufig Anwendung. Ein Computer kann keine reellen Zahlen verarbeiten, da er dafür unendlich viel Speicherplatz bräuchte. Man kann also beispielsweise Pi zwar beliebig genau bestimmen (speicherplatz- und zeitbegrenzt), wird aber nie einen exakten Wert für Pi errechnen. Da Sie das grundsätzliche Rechnen mit Zahlen bereits kennengelernt haben, wird in diesem Kapitel lediglich auf einige Tücken eingegangen, die im Zusammenhang mit Zahlen auftreten können.

Integer Bearbeiten

Bei der Arithmetik mit ganzen Zahlen findet keine automatische Typanpassung statt. Das kann insbesondere beim Dividieren von ganzen Zahlen zu auf den ersten Blick unerwarteten Fehlern führen:

puts 5 / 2
puts 5 % 2
puts 5 / 2.0

Naiv würde man annehmen, dass puts 5 / 2 als Ergebnis 2.5 ausgibt. Das ist aber nicht der Fall, da es sich hier um ganzzahlige Division handelt, die auch als Ergebnis nur ganzzahlige Werte liefert. Das Resultat entspricht dem Resultat der Division mit Rest 5 / 2 = 2 Rest 1. Daher gibt es für ganzzahlige Arithmetik auch zwei Divisionsoperatoren: / für das Ergebnis der eigentlichen Division und % für den Rest der Division. Dieser Fallstrick ist insbesondere zu beachten, wenn Ihnen der Typ der Werte nicht bekannt ist, da Sie zum Beispiel in Variablen gespeichert sind. Dann müssen Sie bei jeder Division aufpassen, dass Sie entweder mit ganzen Zahlen rechnen oder eine manuelle Typkonvertierung vornehmen. Denn das Ergebnis ist dann ebenfalls eine gebrochene Zahl, wie im Beispiel puts 5 / 2.0. Eine Methode mit Typkonvertierungen kann dann zum Beispiel so aussehen:

def div(a, b)
  a / b.to_f
end

def div2(a, b)
  a / Float(b)
end

Floating Point Numbers Bearbeiten

Eine Gleitkommazahl wird auf Computern mit einer begrenzten Anzahl an Speicher binär dargestellt. In Ruby sind das typischerweise 64 Bit. Für die Ausführung des Programms ist es also notwendig, dass Ruby die Werte, die wir geschrieben haben (typischerweise im Dezimalsystem), in das Binärsystem umrechnet. Da nur endlich viele Zahlen dargestellt werden können, kann es bei diesem Vorgang zu Rundungsfehlern kommen, sodass Gleitkommazahlen nicht beliebig genau sind. Dies kann beim Prüfen auf Gleichheit zu Problemen führen:

if 2.2 - 1.2 == 1.0
  puts "Gleich."
else
  puts "Ungleich."
end

Man erwartet die Ausgabe "Gleich.", doch leider wird man enttäuscht. Ändert man hingegen den Vergleich zu if 2.0 - 1.0 == 1.0, funktioniert alles. Dieser Verlust an Genauigkeit hat mehrere Implikationen. Zum einen begründet sich nun, warum es überhaupt die ganzzahlige Division gibt, denn das Ergebnis der ganzzahligen Division ist exakt, wenn man bedenkt, den Rest zu beachten. Bei Gleitkommazahlen kann es hingegen zu Problemen kommen. Zweitens ist es nicht sinnvoll und manchmal grob fahrlässig, Gleitkommazahlen zu verwenden, wenn es auf Genauigkeit ankommt. Ein typisches Beispiel ist das Rechnen mit Geldbeträgen, denn obiges Skript könnte auch zur Berechnung des Kontostandes dienen, aber das Ergebnis wäre nicht, dass der Kunde noch einen Euro hat, sondern eben nur ungefähr einen Euro, bei einem kleinen Fehler. Bei Geldbeträgen eignet es sich daher viel mehr, mit ganzen Zahlen und der jeweils kleinsten Geldeinheit zu rechnen. Drittens ist der Vergleich von zwei Gleitkommazahlen auf Gleichheit offensichtlich nicht sinnvoll, da er je nach interner Darstellung der Zahlen (die wir nicht kennen und die sich ändern kann) zu anderen Ergebnissen kommt. Lediglich der Vergleich, ob zwei Gleitkommazahlen innerhalb eines bestimmten Bereiches dicht aneinander liegen ist möglich:

def equal_float?(a, b, accuracy=0.01)
  avg = (a + b) / 2.0
  dif = a - b
  # Absolutwert von dif benutzen.
  dif.abs / avg < accuracy
end

puts equal_float?(2.0,2.1)              #=> False
puts equal_float?(100.0,100.1)          #=> True
puts equal_float?(100.0,100.1,0.00001)  #=> False

Dieses Skript vergleicht zwei Gleitkommazahlen darauf, ob der Quotient von Differenz und Mittelwert unterhalb einer bestimmten Schranke liegt. Durch Einfügen der Zeile puts fp_gleichheit?(2.2-1.2,1.0,0.00001) erkennt man, dass der Rundungsfehler sehr klein ist und die Methode das gewünschte Ergebnis liefert. Eine Alternative bietet da die Klasse BigDecimal, die das exakte Rechnen im Dezimalsystem ermöglicht.

Rational Bearbeiten

Die Klasse Rational erlaubt das Rechnen mit exakten Brüchen indem sie Zähler und Nenner als ganze Zahlen intern speichert.

Es gibt verschiedene Möglichkeiten um Objekte der Klasse Rational zu erstellen:

# Variante (1)
a = Rational(2, 3)
b = Rational(125, 1000)
# Variante (2)
a = '2/3'.to_r
b = '0.125'.to_r
# Variante (3)
a = 2r/3
b = 0.125r

Rechnen kann man mit ihnen wie mit den anderen Zahlen. In aller Regel werden sie einiges langsamer sein, als einfache Gleitkommazahlen, man sollte sie also nur einsetzen, wenn man sie auch wirklich braucht. Auch sind für sie verschiedene Operationen, z. B. die Wurzel nicht implementiert.[Bem 1] Man kann aber am Ende einer Kette von Berechnungen das Resultat wieder in eine Gleitkommazahl umrechnen lassen und dann auf dieses die gewünschte Operation ausführen.

z = Rational(2, 3)
puts z.to_f.sqrt

BigDecimal Bearbeiten

Das äquivalente Skript mit der Klasse BigDecimal sieht wie folgt aus:

require 'bigdecimal'

if (BigDecimal.new("2.2") - BigDecimal("1.2")) == BigDecimal("1.0")
  puts "Gleich."
else
  puts "Ungleich."
end

Mit BigDecimal opfert man Kompaktheit des Codes gegen genaue Berechnungen, man sollte es also überall dort anwenden, wo man die Genauigkeit benötigt und ansonsten Gleitkommazahlen oder ganze Zahlen verwenden. Auch ist das Rechnen mit BigDecimal wesentlich langsamer als die Alternativen.

Bemerkungen Bearbeiten

  1. Dies hat damit zu tun, dass z. B. die Wurzel für positive rationale Zahlen oftmals irrational ist. Beispielsweise ist bekannt, dass   nicht als Bruch geschrieben werden kann. Für andere Operationen wie Sinus, Tangens, Exponential- und Logarithmenfunktion hat man dasselbe Problem.