Frage Warum nicht Double oder Float zur Darstellung der Währung verwenden?


Mir wurde schon immer gesagt noch nie um mit Geld zu vertreten double oder float Typen, und dieses Mal stelle ich Ihnen die Frage: Warum?

Ich bin mir sicher, dass es einen sehr guten Grund gibt, ich weiß einfach nicht, was es ist.


763
2017-09-16 19:23


Ursprung


Antworten:


Weil Floats und Doubles die Basis 10 Multiples, die wir für Geld verwenden, nicht genau darstellen können. Dieses Problem betrifft nicht nur Java, sondern jede Programmiersprache, die Basis-Gleitkommatypen verwendet.

In der Basis 10 können Sie 10,25 als 1025 * 10 schreiben-2 (eine ganze Zahl mal eine Potenz von 10). IEEE-754 Fließkommazahlen sind anders, aber eine sehr einfache Art, über sie nachzudenken, ist, stattdessen mit einer Zweierpotenz zu multiplizieren. Zum Beispiel könnten Sie sich 164 * 2 ansehen-4 (eine ganze Zahl mal eine Potenz von zwei), was auch gleich 10.25 ist. Das ist nicht so, wie die Zahlen im Gedächtnis dargestellt werden, aber die mathematischen Implikationen sind die gleichen.

Selbst in der Basis 10 kann diese Notation die meisten einfachen Brüche nicht genau darstellen. Zum Beispiel können Sie 1/3 nicht darstellen: Die dezimale Darstellung wiederholt sich (0.3333 ...), also gibt es keine endliche ganze Zahl, die Sie mit einer Potenz von 10 multiplizieren können, um 1/3 zu erhalten. Sie könnten sich auf eine lange Folge von Dreiern und einen kleinen Exponenten wie 333333333 * 10 festlegen-10, aber es ist nicht korrekt: Wenn Sie das mit 3 multiplizieren, erhalten Sie nicht 1.

Zum Zählen von Geld, zumindest für Länder, deren Geld in einer Größenordnung des US-Dollars geschätzt wird, ist es in der Regel jedoch nur möglich, ein Vielfaches von 10 zu speichern-2Es spielt also keine Rolle, dass 1/3 nicht dargestellt werden kann.

Das Problem mit Floats und Doubles ist, dass die große Mehrheit von geldähnlichen Zahlen haben keine exakte Darstellung als eine ganze Zahl mal eine Potenz von 2. Tatsächlich können die einzigen Vielfachen von 0,01 zwischen 0 und 1 (die bei Geldbeträgen von Bedeutung sind, weil sie ganzzahlige Cents sind) genau wie eine binäre Gleitkommazahl IEEE-754 dargestellt werden, sind 0, 0,25, 0,5, 0,75 und 1. Alle anderen sind um einen kleinen Betrag ausgeschaltet. Als eine Analogie zu dem Beispiel 0.333333, wenn Sie den Gleitkommawert für 0,1 nehmen und Sie ihn mit 10 multiplizieren, erhalten Sie nicht 1.

Geld darstellen als double oder float wird wahrscheinlich zuerst gut aussehen, da die Software die winzigen Fehler abrundet, aber wenn Sie mehr Additionen, Subtraktionen, Multiplikationen und Divisionen auf ungenauen Zahlen durchführen, werden sich Fehler addieren und Sie werden mit Werten enden, die sichtbar nicht genau sind. Dies macht Floats und Doubles für den Umgang mit Geld ungeeignet, wo eine perfekte Genauigkeit für ein Vielfaches von Basis 10 Potenzen erforderlich ist.

Eine Lösung, die in fast jeder Sprache funktioniert, besteht darin, ganze Zahlen zu verwenden und Cent zu zählen. Zum Beispiel wäre 1025 10,25 $. Mehrere Sprachen haben auch eingebaute Arten, um mit Geld umzugehen. Unter anderem hat Java das BigDecimal Klasse und C # hat die decimal Art.


788
2017-09-16 19:26



Von Bloch, J., Effektives Java, 2. Ausgabe, Punkt 48:

Das float und double Arten sind   besonders ungeeignet für monetäre Zwecke   Berechnungen, weil es unmöglich ist   um 0,1 darzustellen (oder irgendeinen anderen   negative Kraft von zehn) als float oder    double genau.

Angenommen, Sie haben 1,03 $   und du gibst 42c aus. Wie viel Geld   bist du gegangen?

System.out.println(1.03 - .42);

druckt aus 0.6100000000000001.

Der richtige Weg, um dieses Problem zu lösen, ist   benutzen BigDecimal, int oder long   für monetäre Berechnungen.


270
2017-09-16 19:52



Dies ist keine Frage der Genauigkeit, noch ist es eine Frage der Präzision. Es geht darum, die Erwartungen von Menschen zu erfüllen, die Basis 10 für Berechnungen anstelle von Basis 2 verwenden. Zum Beispiel führt die Verwendung von Doppelpunkten für Finanzberechnungen nicht zu Antworten, die im mathematischen Sinne "falsch" sind, aber sie kann Antworten liefern nicht was im finanziellen Sinn erwartet wird.

Selbst wenn Sie Ihre Ergebnisse in der letzten Minute vor der Ausgabe abrunden, können Sie immer noch gelegentlich ein Ergebnis erhalten, wenn Sie Doubles verwenden, die nicht den Erwartungen entsprechen.

Mit einem Taschenrechner, oder Berechnung der Ergebnisse von Hand, 1.40 * 165 = 231 genau. Bei der Verwendung von Doubles auf meiner Compiler- / Betriebssystemumgebung wird es jedoch als Binärzahl nahe 230.99999 gespeichert. Wenn Sie also die Zahl abschneiden, erhalten Sie 230 anstelle von 231. Sie könnten also annehmen, dass das Runden statt Abschneiden das wäre haben das gewünschte Ergebnis von 231 gegeben. Das stimmt, aber das Runden beinhaltet immer Verkürzung. Welche Rundungstechnik Sie auch verwenden, es gibt immer noch Randbedingungen wie diese, die abgerundet werden, wenn Sie erwarten, dass sie sich zusammenrunden. Sie sind selten genug, dass sie oft nicht durch zufällige Tests oder Beobachtungen gefunden werden. Möglicherweise müssen Sie Code schreiben, um nach Beispielen zu suchen, die Ergebnisse darstellen, die sich nicht wie erwartet verhalten.

Angenommen, Sie möchten etwas auf den nächsten Cent runden. Also nimmst du dein Endergebnis, multiplizierst mit 100, addierst 0.5, trennst und teilst das Ergebnis dann durch 100, um wieder zu den Pennies zu kommen. Wenn die interne Nummer 3,46499999 war .... statt 3,465, erhalten Sie 3,46 statt 3,47, wenn Sie die Zahl auf den nächsten Cent runden. Aber Ihre Basis-10-Berechnungen haben möglicherweise ergeben, dass die Antwort 3,465 genau sein sollte, was deutlich auf 3,47 und nicht auf 3,46 schließen sollte. Diese Art von Dingen passieren gelegentlich im wirklichen Leben, wenn Sie Doppel für finanzielle Berechnungen verwenden. Es ist selten, so dass es oft unbemerkt bleibt, aber es passiert.

Wenn Sie die Basis 10 für Ihre internen Berechnungen anstelle von Doppeln verwenden, sind die Antworten immer genau das, was von Menschen erwartet wird, vorausgesetzt, es gibt keine anderen Fehler in Ihrem Code.


63
2017-09-12 15:11



Ich bin beunruhigt über einige dieser Antworten. Ich denke Doppelgänger und Schwimmer haben einen Platz in finanziellen Berechnungen. Wenn Sie nicht-gebrochene Währungsbeträge addieren und subtrahieren, wird es sicher keinen Genauigkeitsverlust geben, wenn Sie Integer-Klassen oder BigDecimal-Klassen verwenden. Wenn Sie jedoch komplexere Operationen ausführen, erhalten Sie oft Ergebnisse, die mehrere oder viele Dezimalstellen überschreiten, unabhängig davon, wie Sie die Zahlen speichern. Das Problem ist, wie Sie das Ergebnis präsentieren.

Wenn Ihr Ergebnis an der Grenze zwischen Aufrunden und Abrunden liegt und der letzte Penny wirklich wichtig ist, sollten Sie dem Zuschauer wahrscheinlich sagen, dass die Antwort fast in der Mitte liegt - indem Sie mehr Dezimalstellen anzeigen.

Das Problem mit Doubles und mehr mit Floats ist, wenn sie verwendet werden, um große Zahlen und kleine Zahlen zu kombinieren. In Java,

System.out.println(1000000.0f + 1.2f - 1000000.0f);

Ergebnisse in

1.1875

40
2018-04-03 14:54



Floats und Doubles sind ungefähre Werte. Wenn Sie ein BigDecimal erstellen und ein Float in den Konstruktor übergeben, sehen Sie, was der Float eigentlich gleich ist:

groovy:000> new BigDecimal(1.0F)
===> 1
groovy:000> new BigDecimal(1.01F)
===> 1.0099999904632568359375

Dies ist wahrscheinlich nicht, wie Sie $ 1,01 darstellen möchten.

Das Problem ist, dass die IEEE-Spezifikation keine Möglichkeit bietet, alle Brüche exakt darzustellen, einige von ihnen enden als sich wiederholende Brüche, so dass Sie mit Approximationsfehlern enden. Da Buchhalter mögen Dinge genau auf den Pfennig kommen, und Kunden werden genervt sein, wenn sie ihre Rechnung bezahlen und nachdem die Zahlung verarbeitet wird, schulden sie .01 und sie bekommen eine Gebühr oder können ihr Konto nicht schließen, es ist besser zu verwenden exakte Typen wie dezimal (in C #) oder java.math.BigDecimal in Java.

Es ist nicht so, dass der Fehler nicht kontrollierbar ist, wenn du rumkommst: siehe diesen Artikel von Peter Lawrey. Es ist einfach einfacher, nicht an erster Stelle zu sein. Die meisten Anwendungen, die mit Geld umgehen, erfordern nicht viel Mathematik, die Operationen bestehen darin, Dinge hinzuzufügen oder Beträge verschiedenen Buckets zuzuordnen. Die Einführung von Fließkomma und Rundung macht die Dinge nur komplizierter.


35
2017-09-16 19:29



Das Ergebnis der Fließkommazahl ist nicht exakt, was sie für eine finanzielle Berechnung ungeeignet macht, die ein genaues Ergebnis und keine Annäherung erfordert. float und double sind für die technische und wissenschaftliche Berechnung konzipiert und liefern oft kein exaktes Ergebnis. Auch das Ergebnis der Gleitkommaberechnung kann von JVM zu JVM variieren. Sehen Sie sich das folgende Beispiel von BigDecimal und Double Primitive an, das zur Darstellung von Geldwerten verwendet wird. Es ist ziemlich klar, dass die Gleitkommaberechnung nicht exakt sein kann und BigDecimal für Finanzberechnungen verwendet werden sollte.

    // floating point calculation
    final double amount1 = 2.0;
    final double amount2 = 1.1;
    System.out.println("difference between 2.0 and 1.1 using double is: " + (amount1 - amount2));

    // Use BigDecimal for financial calculation
    final BigDecimal amount3 = new BigDecimal("2.0");
    final BigDecimal amount4 = new BigDecimal("1.1");
    System.out.println("difference between 2.0 and 1.1 using BigDecimal is: " + (amount3.subtract(amount4)));

Ausgabe:

difference between 2.0 and 1.1 using double is: 0.8999999999999999
difference between 2.0 and 1.1 using BigDecimal is: 0.9

15
2017-08-11 20:18



Es ist zwar richtig, dass Fließkomma-Typ nur ungefähre Dezimalzahlen darstellen kann, aber wenn man Zahlen vor der Präsentation auf die notwendige Genauigkeit rundet, erhält man das korrekte Ergebnis. Gewöhnlich.

Normalerweise, weil der Doppeltyp eine Genauigkeit von weniger als 16 Ziffern hat. Wenn Sie eine bessere Genauigkeit benötigen, ist es kein geeigneter Typ. Auch Annäherungen können sich ansammeln.

Es muss gesagt werden, dass Sie auch dann, wenn Sie Festkommaarithmetik verwenden, Zahlen runden müssen, wenn BigInteger und BigDecimal keine Fehler geben, wenn Sie periodische Dezimalzahlen erhalten. Also gibt es auch hier eine Annäherung.

Zum Beispiel hat COBOL, historisch für Finanzberechnungen verwendet, eine maximale Genauigkeit von 18 Zahlen. Es kommt also oft zu einer impliziten Rundung.

Zusammenfassend ist das Double meines Erachtens vor allem wegen seiner 16-stelligen Genauigkeit ungeeignet, was nicht ausreichen kann, nicht weil es ungefähr ist.

Betrachten Sie die folgende Ausgabe des nachfolgenden Programms. Es zeigt, dass nach der Doppelung das gleiche Ergebnis wie BigDecimal bis zur Genauigkeit 16 erhalten wird.

Precision 14
------------------------------------------------------
BigDecimalNoRound             : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound                 : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal                    : 56789.012345 / 1111111111 = 0.000051110111115611
Double                        : 56789.012345 / 1111111111 = 0.000051110111115611

Precision 15
------------------------------------------------------
BigDecimalNoRound             : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound                 : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal                    : 56789.012345 / 1111111111 = 0.0000511101111156110
Double                        : 56789.012345 / 1111111111 = 0.0000511101111156110

Precision 16
------------------------------------------------------
BigDecimalNoRound             : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound                 : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal                    : 56789.012345 / 1111111111 = 0.00005111011111561101
Double                        : 56789.012345 / 1111111111 = 0.00005111011111561101

Precision 17
------------------------------------------------------
BigDecimalNoRound             : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound                 : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal                    : 56789.012345 / 1111111111 = 0.000051110111115611011
Double                        : 56789.012345 / 1111111111 = 0.000051110111115611013

Precision 18
------------------------------------------------------
BigDecimalNoRound             : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound                 : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal                    : 56789.012345 / 1111111111 = 0.0000511101111156110111
Double                        : 56789.012345 / 1111111111 = 0.0000511101111156110125

Precision 19
------------------------------------------------------
BigDecimalNoRound             : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound                 : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal                    : 56789.012345 / 1111111111 = 0.00005111011111561101111
Double                        : 56789.012345 / 1111111111 = 0.00005111011111561101252

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.math.MathContext;

public class Exercise {
    public static void main(String[] args) throws IllegalArgumentException,
            SecurityException, IllegalAccessException,
            InvocationTargetException, NoSuchMethodException {
        String amount = "56789.012345";
        String quantity = "1111111111";
        int [] precisions = new int [] {14, 15, 16, 17, 18, 19};
        for (int i = 0; i < precisions.length; i++) {
            int precision = precisions[i];
            System.out.println(String.format("Precision %d", precision));
            System.out.println("------------------------------------------------------");
            execute("BigDecimalNoRound", amount, quantity, precision);
            execute("DoubleNoRound", amount, quantity, precision);
            execute("BigDecimal", amount, quantity, precision);
            execute("Double", amount, quantity, precision);
            System.out.println();
        }
    }

    private static void execute(String test, String amount, String quantity,
            int precision) throws IllegalArgumentException, SecurityException,
            IllegalAccessException, InvocationTargetException,
            NoSuchMethodException {
        Method impl = Exercise.class.getMethod("divideUsing" + test, String.class,
                String.class, int.class);
        String price;
        try {
            price = (String) impl.invoke(null, amount, quantity, precision);
        } catch (InvocationTargetException e) {
            price = e.getTargetException().getMessage();
        }
        System.out.println(String.format("%-30s: %s / %s = %s", test, amount,
                quantity, price));
    }

    public static String divideUsingDoubleNoRound(String amount,
            String quantity, int precision) {
        // acceptance
        double amount0 = Double.parseDouble(amount);
        double quantity0 = Double.parseDouble(quantity);

        //calculation
        double price0 = amount0 / quantity0;

        // presentation
        String price = Double.toString(price0);
        return price;
    }

    public static String divideUsingDouble(String amount, String quantity,
            int precision) {
        // acceptance
        double amount0 = Double.parseDouble(amount);
        double quantity0 = Double.parseDouble(quantity);

        //calculation
        double price0 = amount0 / quantity0;

        // presentation
        MathContext precision0 = new MathContext(precision);
        String price = new BigDecimal(price0, precision0)
                .toString();
        return price;
    }

    public static String divideUsingBigDecimal(String amount, String quantity,
            int precision) {
        // acceptance
        BigDecimal amount0 = new BigDecimal(amount);
        BigDecimal quantity0 = new BigDecimal(quantity);
        MathContext precision0 = new MathContext(precision);

        //calculation
        BigDecimal price0 = amount0.divide(quantity0, precision0);

        // presentation
        String price = price0.toString();
        return price;
    }

    public static String divideUsingBigDecimalNoRound(String amount, String quantity,
            int precision) {
        // acceptance
        BigDecimal amount0 = new BigDecimal(amount);
        BigDecimal quantity0 = new BigDecimal(quantity);

        //calculation
        BigDecimal price0 = amount0.divide(quantity0);

        // presentation
        String price = price0.toString();
        return price;
    }
}

14
2017-10-26 19:03



Ich werde riskieren, downvoted, aber ich denke, dass die Ungeeignetheit von Gleitkommazahlen für Währungsberechnungen überbewertet wird. Solange Sie sichergehen, dass Sie die Cent-Rundung korrekt durchführen und genügend signifikante Stellen haben, um mit der durch zneak erklärten binären Dezimaldarstellung umzugehen, wird es kein Problem geben.

Leute, die mit einer Währung in Excel rechnen, haben immer Gleitkommazahlen mit doppelter Genauigkeit benutzt (es gibt keinen Währungstyp in Excel) und ich habe noch niemanden gesehen, der sich über Rundungsfehler beschwert.

Natürlich musst du in der Vernunft bleiben; z.B. ein einfacher Webshop würde wahrscheinlich niemals ein Problem mit Doppelpräzisionsschwimmern erfahren, aber wenn Sie z.B. Buchhaltung oder irgendetwas anderes, das erfordert, eine große (unbeschränkte) Menge von Zahlen hinzuzufügen, Sie würden Fließkommazahlen mit einem zehn-Fuß-Pfosten nicht berühren wollen.


10
2018-01-20 11:56