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 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 durch zu teilen, im Dualsystem. Da bekommt man dann , genauer oder als Bruch daraus oder im Zehnersystem .
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 angezeigt, aber jeder, der ein bisschen mit diesen Fließkommazahlen hantiert hat, weiß, dass irgendwann einmal etwas in der Art von oder statt herauskommt und dass bei typischen Additions- und Subtraktionsoperationen. Man kann sogar auf oder 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 für BigInteger so etwas wie 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
- 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.
Rundet zu 0 hin, für positive Zahlen gleich wie ROUND_FLOOR
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 mit dezimalen Nachkommastellen so runden, dass die mit einer der Restklassen oder 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.
Schreibe einen Kommentar