Rundung bei Geldbeträgen

English

Ein Teil der numerischen Berechnungen bezieht sich auf Geldbeträge. Es ist immer ganz schön, wenn diese Berechnungen stimmen oder wenn man zumindest durch solche Ungenauigkeiten kein Geld verliert. Natürlich gibt es Berechnungen, die von ungefähren Zahlwerten ausgehen dürfen, wenn es etwa um Berechnungen von prozentualen Renditen geht. Da können dann auch diese Fließkomma-Zahlen (double, Float oder wie sie auch heißen) zum Einsatz kommen, es muss aber natürlich auf numerische Probleme Rücksicht genommen werden, sonst können sich Rundungsfehler massiv hochschaukeln.

Oft ist es aber notwendig, mit den genauen Beträgen zu rechen. Nun kann man so etwas wie 3.23 sehr schön als Fließkommazahl schreiben, aber leider arbeiten diese Fließkommazahlen intern im Dualsystem und drücken also die Zehntel und Hundertstel in Brüchen mit Zweierpotenzen im Nenner aus. Man kann einmal den Versuch machen x=323_{10}=101000011_{2} durch y=100_{10}=1100100_2 zu teilen, im Dualsystem. Da bekommt man dann z=x/y=11.0011101011100001010001111010111000010100011110101\ldots_{2}, genauer z=11.00\overline{11101011100001010001} oder als Bruch daraus z=11_2+\frac{11101011100001010001_2}{100_2*(100000000000000000000_2-1_2)} oder im Zehnersystem z=3_{10}+\frac{964689_{10}}{4_{10}\cdot1048575_{10}}.

Man sieht also, dass diese unscheinbare Zahl mit nur zwei Stellen nach dem Komma im Dualsystem unendlich viele Stellen nach dem Komma bräuchte. In den üblichen Double oder Float-Typen sind aber nur 64 Bit insgesamt vorgesehen und es werden einige Stellen weggeworfen. Zum Glück funktioniert es meistens und die gespeicherte Zahl wird immer noch als 3.23 angezeigt, aber jeder, der ein bisschen mit diesen Fließkommazahlen hantiert hat, weiß, dass irgendwann einmal etwas in der Art von 3.299999999999 oder 3.2300000001 statt 3.23 herauskommt und dass bei typischen Additions- und Subtraktionsoperationen. Man kann sogar auf 3.22 oder 3.24 kommen, wenn man lange genug herumrechnet. Für viele Applikationen ist das unakzeptabel.

Eine einfache und oft sinnvolle Lösung ist es, mit Ganzzahlen zu rechnen und die Beträge in Cent, Rappen, Pfennigen u.s.w. auszudrücken. Dann ist das Rundungsproblem für reine Additionen und Subtraktionen vollständig gelöst oder zumindest für alle anderen Operationen etwas einfacher handhabbar. Man muss nur in der Software darauf achten, dass man die „Beträge in Cent“ und die „Beträge in Euro“ niemals, wirklich niemals durcheinanderbringt. In Scala würde man verschiedene Typen verwenden, so dass zur compile-Zeit diese Durchmischung unterbunden wird. Wichtig ist natürlich in jedem Fall, dass man nicht so etwas wie int von Java verwendet, wo der Überlauf zu unüberschaubaren Katastrophen führen kann. Und Abschätzungen über die oberen Grenzen des monetären Reichtums von Firmen und Einzelpersonen gezählt in einer potentiell inflationsgefährdeten Währung sind fast immer falsch. Hier zeigt sich, dass Java für Finanzapplikationen schlecht geeignet ist, weil man dann statt a = b + c * d für BigInteger so etwas wie a = b.{\rm add}(c.{\rm multiply}(d)) schreiben muss, was dazu führt, dass man die Formel nicht mehr „sieht“ und deshalb sehr viel mehr Fehler produziert. Eventuell kann man das Problem mit einem Präprozessor lösen oder es mit einer Library, die wenigstens so etwas wie UPN-Notation für die Berechnung ermöglicht, etwas entschärfen. Dann schreibt man etwa so etwas

Calculation calc = new Calculation();
calc.push(b)
calc.push(c)
calc.push(d)
calc.add()
calc.multiply()
a = calc.top()

Wer noch alte HP-Taschenrechner oder Forth kennt, wird das vielleicht mögen, aber für die meisten von uns ist auch das nicht wirklich die Lösung des Problems.

Üblich und sinnvoll ist aber in den meisten Fällen, so ein dezimaler Festkomma-Typ für diesen Anwendungsfall. In Java ist das BigDecimal, in Ruby LongDecimal. Ein Beispiel in Ruby

> sudo gem install long-decimal
Successfully installed long-decimal-1.00.01
1 gem installed
....
> irb
irb(main):001:0> require "long-decimal"
=> true
irb(main):002:0> x = LongDecimal("3.23")
=> LongDecimal(323, 2)
irb(main):003:0> y = LongDecimal("7.68")
=> LongDecimal(768, 2)
irb(main):004:0> z = LongDecimal("3.9291")
=> LongDecimal(39291, 4)
irb(main):005:0> x+y
=> LongDecimal(1091, 2)
irb(main):006:0> (x+y).to_s
=> "10.91"
irb(main):007:0> x+y*z
=> LongDecimal(33405488, 6)
irb(main):008:0> (x+y*z).to_s
=> "33.405488"
irb(main):009:0> 

Interessant ist, dass bei Addition und Subtraktion die Anzahl der benötigten Nachkommastellen gleich bleibt bzw. sich die größere Anzahl von Nachkommastellen durchsetzt. Bei Multiplikation und Division und sowieso bei komplexeren Operationen können aber viele Nachkommastellen entstehen. Da ist es entscheidend, richtig zu runden. LongDecimal kennt die folgenden Rundungsmethoden:

ROUND_UP
Rundet von 0 weg, für positive Zahlen gleich wie ROUND_CEILING
ROUND_DOWN

Rundet zu 0 hin, für positive Zahlen gleich wie ROUND_FLOOR

ROUND_CEILING
Rundet auf, also hin zu größeren, positiveren, weniger negativen Zahlen
ROUND_FLOOR
Rundet ab, also hin zu kleineren, negativeren, weniger positiven Zahlen
ROUND_HALF_UP
Rundet ab der Mitte weg von 0, unterhalb der Mitte zu 0 hin
ROUND_HALF_DOWN
Rundet bis einschließlich der Mitte zur 0 hin, oberhalb der Mitte von der 0 weg.
ROUND_HALF_CEILING
Rundet bis einschließlich der Mitte auf (Richtung unendlich) hin, sonst ab.
ROUND_HALF_FLOOR
Rundet ab der Mitte ab, sonst auf.
ROUND_HALF_EVEN
Rundet die Mitte so, dass dabei die letzte Stelle gerade wird.
ROUND_HALF_ODD
Rundet die Mitte so, dass dabei die letzte Stelle ungerade wird.
ROUND_UNNECESSARY
Rundet gar nicht und wirft eine Exception, wenn die weggerundeten Stellen nicht alles Nullen sind.

Welche man davon anwendet sollte man unbedingt mit den entsprechenden Spezialisten von der „Fachseite“ besprechen oder für den jeweiligen Anwendungsfall herausfinden. Als Fortsetzung vom obigen Beispiel sähe das etwa so aus:

irb(main):035:0> t=(x+y*z)
=> LongDecimal(33405488, 6)
irb(main):036:0> 
irb(main):037:0* t.round_to_scale(2, LongDecimal::ROUND_UP).to_s
=> "33.41"
irb(main):038:0> t.round_to_scale(2, LongDecimal::ROUND_DOWN).to_s
=> "33.40"
irb(main):039:0> t.round_to_scale(2, LongDecimal::ROUND_CEILING).to_s
=> "33.41"
irb(main):040:0> t.round_to_scale(2, LongDecimal::ROUND_FLOOR).to_s
=> "33.40"
irb(main):041:0> t.round_to_scale(2, LongDecimal::ROUND_HALF_UP).to_s
=> "33.41"
irb(main):042:0> t.round_to_scale(2, LongDecimal::ROUND_HALF_DOWN).to_s
=> "33.41"
irb(main):043:0> t.round_to_scale(2, LongDecimal::ROUND_HALF_CEILING).to_s
=> "33.41"
irb(main):044:0> t.round_to_scale(2, LongDecimal::ROUND_HALF_FLOOR).to_s
=> "33.41"
irb(main):045:0> t.round_to_scale(2, LongDecimal::ROUND_HALF_EVEN).to_s
=> "33.41"
irb(main):046:0> t.round_to_scale(2, LongDecimal::ROUND_UNNECESSARY).to_s
ArgumentError: mode ROUND_UNNECESSARY not applicable, remainder 5488 is not zero
        from /usr/local/lib/ruby/gems/1.9.1/gems/long-decimal-1.00.01/lib/long-decimal.rb:507:in `round_to_scale_helper'
        from /usr/local/lib/ruby/gems/1.9.1/gems/long-decimal-1.00.01/lib/long-decimal.rb:858:in `round_to_scale'
        from (irb):46
        from /usr/local/bin/irb:12:in `
' irb(main):047:0>

Eine Besonderheit ist, dass in manchen Ländern die kleinste Münze nicht mehr gebräuchlich ist. In der Schweiz ist zum Beispiel der kleinstmögliche Betrag 5 Rappen (0.05 CHF), nicht 1 Rappen. Nun kann man zwar die Bank dazu bringen, Überweisungen zu machen, deren Endziffer nicht 0 oder 5 ist, aber es ist doch verbreitet, Rechungen auf Vielfache von 5 Rappen zu runden. Man kann das Lösen, indem man den Betrag mit 20 multipliziert, dann auf die gewünschte Art rundet und zum Schluss wieder durch 20 dividiert und das Ergebnis auf 2 Nachkommastellen rundet. In Ruby geht das aber einfacher, weil LongDecimal eine „Restklassenrundung“ kennt. Man kann also sagen, man möchte eine Zahl x mit n dezimalen Nachkommastellen so runden, dass die mit 10^n\cdot x einer der Restklassen \overline{0} \mod 10 oder \overline{5} \mod 10 entspricht. In dem Fall ist es einfach die Endziffer. Man kann eine beliebige Menge von Endziffern vorgeben und sogar die 0 verbieten, wobei dann noch festgelegt werden muss, wie die 0 gerundet werden soll, was die üblichen Rundungsmodi nicht vollständig erklären können. Bei Interesse kann ich darauf noch näher eingehen, es ist aber für diesen praktischen Fall nicht relevant, weil die Endziffer 0 natürlich erlaubt ist. Für diesen praktischen Anwendungsfall funktioniert es also so:


irb(main):003:0> t.round_to_allowed_remainders(2, [0, 5], 10, LongDecimal::ROUND_UP).to_s
=> "33.45"
irb(main):005:0> t.round_to_allowed_remainders(2, [0, 5], 10, LongDecimal::ROUND_DOWN).to_s
=> "33.40"
irb(main):006:0> t.round_to_allowed_remainders(2, [0, 5], 10, LongDecimal::ROUND_CEILING).to_s
=> "33.45"
irb(main):007:0> t.round_to_allowed_remainders(2, [0, 5], 10, LongDecimal::ROUND_FLOOR).to_s
=> "33.40"
irb(main):008:0> t.round_to_allowed_remainders(2, [0, 5], 10, LongDecimal::ROUND_HALF_UP).to_s
=> "33.40"
irb(main):009:0> t.round_to_allowed_remainders(2, [0, 5], 10, LongDecimal::ROUND_HALF_DOWN).to_s
=> "33.40"
irb(main):010:0> t.round_to_allowed_remainders(2, [0, 5], 10, LongDecimal::ROUND_HALF_CEILING).to_s
=> "33.40"
irb(main):011:0> t.round_to_allowed_remainders(2, [0, 5], 10, LongDecimal::ROUND_HALF_FLOOR).to_s
=> "33.40"

Man kann nun also in der Finanzapplikation für jede Währung Rundungsmethoden definieren.

LongDecimal kann noch etwas mehr. So ist es möglich, Logarithmen, e-Funktion, Quadratwurzel und Kubikwurzel auf eine gewünschte Anzahl von Nachkommastellen genau zu berechnen und die entsprechenden Algorithmen sind daraufhin optimiert, das Ergebnis möglichst schnell, aber natürlich mit der erforderlichen Genauigkeit zu berechnen.

Share Button

Beteilige dich an der Unterhaltung

1 Kommentar

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

*