Dlaczego nie użyć Double lub Float do reprezentowania waluty?

938

Zawsze mówiono mi, żebym nigdy nie reprezentował pieniędzy doubleani floattypów, i tym razem zadaję ci pytanie: dlaczego?

Jestem pewien, że istnieje bardzo dobry powód, po prostu nie wiem co to jest.

Fran Fitzpatrick
źródło
4
Zobacz to pytanie SO: Błędy zaokrąglania?
Jeff Ogata,
80
Żeby było jasne, nie należy ich używać do niczego, co wymaga dokładności - nie tylko do waluty.
Jeff
152
Nie należy ich używać do niczego, co wymaga dokładności . Ale 53 znaczące bity double (~ 16 cyfr dziesiętnych) są zwykle wystarczające dla rzeczy, które wymagają jedynie dokładności .
dan04
21
@jeff Twój komentarz całkowicie błędnie przedstawia, dla czego binarna zmiennoprzecinkowa jest dobra i do czego nie jest dobra. Przeczytaj odpowiedź zajrzyj poniżej i usuń wprowadzający w błąd komentarz.
Pascal Cuoq,

Odpowiedzi:

983

Ponieważ liczby zmiennoprzecinkowe i dublowanie nie mogą dokładnie odzwierciedlać 10 podstawowych wielokrotności, których używamy do pieniędzy. Ten problem dotyczy nie tylko Javy, ale każdego języka programowania, który używa 2 podstawowych typów zmiennoprzecinkowych.

W bazie 10 możesz zapisać 10,25 jako 1025 * 10 -2 (liczba całkowita razy potęga 10). Liczby zmiennoprzecinkowe IEEE-754 są różne, ale bardzo prostym sposobem na ich rozważenie jest pomnożenie przez potęgę dwóch. Na przykład, możesz patrzeć na 164 * 2-4 (liczba całkowita razy potęga dwóch), która jest również równa 10,25. Liczby te nie są reprezentowane w pamięci, ale implikacje matematyczne są takie same.

Nawet w bazie 10 notacja ta nie może dokładnie reprezentować najprostszych ułamków. Na przykład nie możesz reprezentować 1/3: reprezentacja dziesiętna się powtarza (0,3333 ...), więc nie ma skończonej liczby całkowitej, którą można pomnożyć przez potęgę 10, aby uzyskać 1/3. Możesz osiąść na długiej sekwencji 3 i małym wykładniku, takim jak 333333333 * 10-10 , ale nie jest to dokładne: jeśli pomnożysz to przez 3, nie dostaniesz 1.

Jednak w celu liczenia pieniędzy, przynajmniej w krajach, których pieniądze są wyceniane w granicach rzędu dolara amerykańskiego, zwykle wszystko, czego potrzebujesz, to móc przechowywać wielokrotności 10 -2 , więc to naprawdę nie ma znaczenia że 1/3 nie może być reprezentowana.

Problem z liczbami zmiennoprzecinkowymi i podwójnymi polega na tym, że zdecydowana większość liczb podobnych do pieniędzy nie ma dokładnej reprezentacji jako liczba całkowita razy potęga 2. W rzeczywistości jedyne wielokrotności 0,01 między 0 a 1 (które są znaczące przy rozdawaniu z pieniędzmi, ponieważ są to centy całkowite), które mogą być reprezentowane dokładnie jako binarna liczba zmiennoprzecinkowa IEEE-754, to 0, 0,25, 0,5, 0,75 i 1. Wszystkie pozostałe są wyłączone w niewielkiej ilości. Analogicznie do przykładu 0.333333, jeśli weźmiesz wartość zmiennoprzecinkową dla 0,1 i pomnożysz ją przez 10, nie otrzymasz 1.

Reprezentowanie pieniędzy jako doublelub floatprawdopodobnie będzie wyglądało dobrze na początku, gdy oprogramowanie zaokrągli drobne błędy, ale gdy wykonasz więcej dodawania, odejmowania, mnożenia i dzielenia na niedokładnych liczbach, błędy będą się komplikować, a otrzymasz wartości, które są widoczne niedokładne. To sprawia, że ​​liczba zmiennoprzecinkowa i liczba podwójna są nieodpowiednie do radzenia sobie z pieniędzmi, gdzie wymagana jest idealna dokładność dla wielokrotności bazowych mocy 10.

Rozwiązaniem, które działa w prawie każdym języku, jest użycie liczb całkowitych i policzenie centów. Na przykład 1025 to 10,25 USD. Kilka języków ma również wbudowane typy do obsługi pieniędzy. Między innymi Java ma BigDecimalklasę, a C # ma decimaltyp.

skradać się
źródło
3
@Fran Wystąpią błędy zaokrąglania, a w niektórych przypadkach, gdy używane są duże ilości waluty, obliczenia stóp procentowych mogą być rażąco wyłączone
linuxuser27
5
... to znaczy większość 10 frakcji. Na przykład 0.1 nie ma dokładnej binarnej reprezentacji zmiennoprzecinkowej. Więc 1.0 / 10 * 10może nie być taki sam jak 1.0.
Chris Jester-Young,
6
@ linuxuser27 Myślę, że Fran próbował być zabawny. W każdym razie odpowiedź Zneaka jest najlepsza, jaką widziałem, nawet lepsza niż klasyczna wersja Blocha.
Isaac Rabinovitch
5
Oczywiście, jeśli znasz dokładność, zawsze możesz zaokrąglić wynik, a tym samym uniknąć całego problemu. Jest to o wiele szybsze i prostsze niż korzystanie z BigDecimal. Inną alternatywą jest użycie int lub long o stałej precyzji.
Peter Lawrey
2
@ zaglądasz, że wiesz, że liczby wymierne są podzbiorem liczb rzeczywistych, prawda? Liczby rzeczywiste IEEE-754 liczbami rzeczywistymi. Oni po prostu stało się też być racjonalne.
Tim Seguine
314

Z Bloch, J., Effective Java, wyd. 2, pozycja 48:

floatI doublerodzaje są szczególnie słabo nadaje się do obliczeń pieniężnych, ponieważ jest to niemożliwe do reprezentowania 0.1 (lub dowolną inną moc negatywną dziesięciu) jako floatlub doubledokładnie.

Załóżmy na przykład, że masz 1,03 USD i wydajesz 42 centów. Ile pieniędzy zostało?

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

drukuje 0.6100000000000001.

Właściwym sposobem rozwiązania tego problemu jest użycie BigDecimal, intlub long do obliczeń pieniężnych.

Chociaż BigDecimalma pewne zastrzeżenia (zobacz aktualnie zaakceptowaną odpowiedź).

dogbane
źródło
6
Jestem trochę zdezorientowany zaleceniem używania int lub long do obliczeń pieniężnych. Jak reprezentujesz 1.03 jako int lub long? Próbowałem „długo a = 1,04;” i „długo a = 104/100;” bez skutku.
Peter
49
@Peter, używasz long a = 104i liczysz w centach zamiast w dolarach.
zneak
@zneak Co powiesz na to, kiedy należy zastosować procent, taki jak powiększanie odsetek lub podobne?
trusktr
3
@trusktr, wybrałbym typ dziesiętny Twojej platformy. W Javie to jest BigDecimal.
zneak
13
@maaartinus ... a nie sądzisz, że stosowanie podwójnych metod do takich rzeczy jest podatne na błędy? Widziałem kwestia pływak zaokrąglanie hit rzeczywistych systemów ciężko . Nawet w bankowości. Proszę nie polecam go, lub jeśli nie, zapewnić, że jako oddzielny odpowiedź (tak możemy go downvote: P)
EIS
75

Nie jest to kwestia dokładności ani precyzji. Jest to kwestia spełnienia oczekiwań ludzi, którzy używają podstawy 10 do obliczeń zamiast podstawy 2. Na przykład użycie podwójnych danych do obliczeń finansowych nie daje odpowiedzi, które są „błędne” w sensie matematycznym, ale może dawać odpowiedzi, które są nie to, czego oczekuje się w sensie finansowym.

Nawet jeśli zaokrąglisz swoje wyniki w ostatniej chwili przed wyjściem, nadal możesz czasami uzyskać wynik za pomocą podwójnych wyników, które nie spełniają oczekiwań.

Za pomocą kalkulatora lub ręcznego obliczania wyników 1,40 * 165 = 231 dokładnie. Jednak wewnętrznie przy użyciu podwójnych, w moim środowisku kompilatora / systemu operacyjnego, jest on przechowywany jako liczba binarna blisko 230.99999 ... więc jeśli obetniesz numer, otrzymasz 230 zamiast 231. Możesz pomyśleć, że zaokrąglenie zamiast obcięcia byłoby dały pożądany wynik 231. To prawda, ale zaokrąglanie zawsze wiąże się z obcięciem. Jakąkolwiek technikę zaokrąglania użyjesz, nadal istnieją warunki brzegowe, takie jak ten, który zaokrągli w dół, gdy spodziewasz się, że zaokrągli w górę. Są na tyle rzadkie, że często nie można ich znaleźć podczas przypadkowych testów lub obserwacji. Być może trzeba będzie napisać kod, aby wyszukać przykłady ilustrujące wyniki, które nie zachowują się zgodnie z oczekiwaniami.

Załóżmy, że chcesz zaokrąglić coś do najbliższego grosza. Więc weź swój wynik końcowy, pomnóż przez 100, dodaj 0,5, obetnij, a następnie podziel wynik przez 100, aby wrócić do groszy. Jeśli przechowywany numer wewnętrzny to 3,46499999 .... zamiast 3,465, otrzymasz 3,46 zamiast 3,47, gdy zaokrąglisz liczbę do najbliższego grosza. Ale twoje 10 podstawowych obliczeń mogło wskazywać, że odpowiedź powinna wynosić dokładnie 3,465, co wyraźnie powinno zaokrąglić w górę do 3,47, a nie do 3,46. Tego rodzaju rzeczy zdarzają się czasami w prawdziwym życiu, gdy używasz dubletów do obliczeń finansowych. Jest to rzadkie, więc często pozostaje niezauważone, ale zdarza się.

Jeśli użyjesz podstawy 10 do wewnętrznych obliczeń zamiast podwójnych, odpowiedzi są zawsze dokładnie takie, jakich oczekują ludzie, zakładając, że nie ma innych błędów w twoim kodzie.

Randy D Oxentenko
źródło
2
Powiązane, interesujące: W mojej konsoli Chrome js: Math.round (.499999999999999999): 0 Math.round (.4999999999999999999): 1
Curtis Yallop
16
Ta odpowiedź jest myląca. 1,40 * 165 = 231. Każda liczba inna niż dokładnie 231 jest niepoprawna w sensie matematycznym (i wszystkich innych zmysłów).
Karu
2
@Karu Myślę, że właśnie dlatego Randy mówi, że zmiennoprzecinkowe są złe ... W rezultacie moja konsola Chrome JS pokazuje 230.9999999999999997. To jest złe, o co chodzi w odpowiedzi.
trusktr
6
@Karu: Imho odpowiedź nie jest matematycznie zła. Trzeba tylko odpowiedzieć na dwa pytania, które nie są zadawane. Pytanie, na które odpowiada twój kompilator, to 1.39999999 * 164,99999999 i tak dalej, na które poprawna matematycznie jest 230.99999999 ... Oczywiście nie jest to pytanie, które zostało zadane w pierwszej kolejności ...
Markus
2
@CurtisYallop, ponieważ podwójna wartość zamyka się do 0,4999999999999999999 wynosi 0,5 Dlaczego Math.round(0.49999999999999994)zwraca 1?
phuclv
53

Niepokoją mnie niektóre z tych odpowiedzi. Myślę, że debel i float mają swoje miejsce w obliczeniach finansowych. Z pewnością przy dodawaniu i odejmowaniu niefrakcjonalnych kwot pieniężnych nie wystąpi utrata precyzji podczas korzystania z klas całkowitych lub klas BigDecimal. Ale wykonując bardziej złożone operacje, często uzyskuje się wyniki, które wychodzą z kilku lub wielu miejsc po przecinku, bez względu na sposób przechowywania liczb. Problemem jest sposób prezentacji wyniku.

Jeśli twój wynik jest na granicy między zaokrągleniem w górę a zaokrągleniem w dół, a ten ostatni grosz naprawdę ma znaczenie, prawdopodobnie powinieneś powiedzieć widzowi, że odpowiedź jest prawie w środku - wyświetlając więcej miejsc po przecinku.

Problem z liczbami podwójnymi, a zwłaszcza z liczbami zmiennoprzecinkowymi, polega na tym, że są one używane do łączenia dużych i małych liczb. W java

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

prowadzi do

1.1875
Rob Scala
źródło
16
TO!!!! Szukałem wszystkich odpowiedzi, aby znaleźć ten WŁAŚCIWY FAKT !!! W normalnych obliczeniach nikogo to nie obchodzi, jeśli jesteś ułamkiem centa, ale tutaj przy wysokich liczbach łatwo tracisz kilka dolarów na transakcję!
Falco
22
A teraz wyobraź sobie, że ktoś otrzymuje dzienny przychód w wysokości 0,01% z jego 1 miliona dolarów - nie dostałby nic każdego dnia - a po roku nie dostałby 1000 dolarów, TO BĘDZIE MIAŁO
Falco
6
Problemem nie jest dokładność, ale liczba zmiennoprzecinkowa nie mówi, że staje się niedokładna. Liczba całkowita może zawierać tylko do 10 cyfr, liczba zmiennoprzecinkowa może pomieścić do 6 cyfr, nie popełniając niedokładności (po odpowiednim przecięciu). Pozwala na to, podczas gdy liczba całkowita zostaje przepełniona, a język taki jak Java ostrzega cię lub nie pozwala na to. Kiedy używasz podwójnego, możesz zwiększyć do 16 cyfr, co wystarcza w wielu przypadkach użycia.
sigi,
39

Liczba zmiennoprzecinkowa i liczba podwójna są przybliżone. Jeśli utworzysz BigDecimal i przekażesz liczbę zmiennoprzecinkową do konstruktora, zobaczysz, co tak naprawdę jest zmiennoprzecinkowe:

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

Prawdopodobnie nie tak chcesz reprezentować 1,01 USD.

Problem polega na tym, że specyfikacja IEEE nie ma sposobu, aby dokładnie reprezentować wszystkie ułamki, niektóre z nich kończą się jako ułamki powtarzające się, co powoduje błędy aproksymacyjne. Ponieważ księgowi lubią rzeczy, które wychodzą dokładnie z pensa, a klienci będą zirytowani, jeśli zapłacą rachunek, a po przetworzeniu płatności są winni 0,01 i otrzymają opłatę lub nie mogą zamknąć swojego konta, lepiej użyć dokładne typy, takie jak dziesiętny (w języku C #) lub java.math.BigDecimal w Javie.

To nie jest tak, że błędu nie da się kontrolować, jeśli zaokrąglisz: zobacz ten artykuł Peter Lawrey . Po prostu łatwiej nie trzeba zaokrąglać. Większość aplikacji, które obsługują pieniądze, nie wymaga dużej matematyki, operacje polegają na dodawaniu rzeczy lub przydzielaniu kwot do różnych segmentów. Wprowadzenie zmiennoprzecinkowe i zaokrąglanie tylko komplikuje sprawy.

Nathan Hughes
źródło
5
float, doubleI BigDecimalsą przedstawicielami dokładnych wartości. Konwersja kodu na obiekt jest niedokładna, podobnie jak inne operacje. Same typy nie są niedokładne.
chux - Przywróć Monikę
1
@chux: czytając to ponownie, myślę, że masz rację, że moje sformułowania można poprawić. Zmienię to i przeredaguję.
Nathan Hughes
28

Zaryzykuję, że zostanę odrzucony, ale myślę, że nieodpowiednia liczba zmiennoprzecinkowa do obliczania waluty jest przereklamowana. Dopóki upewnisz się, że poprawnie wykonujesz zaokrąglanie centów i masz wystarczającą liczbę cyfr znaczących do pracy, aby przeciwdziałać niedopasowaniu reprezentacji binarno-dziesiętnej wyjaśnionemu przez zneak, nie będzie problemu.

Ludzie dokonujący obliczeń za pomocą waluty w Excelu zawsze używali liczb zmiennoprzecinkowych o podwójnej precyzji (w Excelu nie ma typu waluty) i nie spotkałem jeszcze nikogo, kto narzeka na błędy zaokrąglania.

Oczywiście musisz pozostać w granicach rozsądku; np. prosty sklep internetowy prawdopodobnie nigdy nie doświadczyłby żadnych problemów z liczbami zmiennoprzecinkowymi o podwójnej precyzji, ale jeśli robisz np. księgowość lub cokolwiek innego, co wymaga dodania dużej (nieograniczonej) liczby liczb, nie chciałbyś dotykać liczb zmiennoprzecinkowych dziesięciostopową stopą Polak.

escitalopram
źródło
3
To właściwie całkiem przyzwoita odpowiedź. W większości przypadków ich użycie jest całkowicie w porządku.
Vahid Amiri,
2
Należy zauważyć, że większość banków inwestycyjnych używa podwójnie, podobnie jak większość programów C ++. Niektóre używają długich, ale mają własny problem ze skalą śledzenia.
Peter Lawrey,
20

Chociaż prawdą jest, że typ zmiennoprzecinkowy może reprezentować tylko dane w przybliżeniu dziesiętne, prawdą jest również to, że jeśli zaokrągli się liczby z niezbędną dokładnością przed ich przedstawieniem, uzyska się poprawny wynik. Zazwyczaj.

Zwykle dlatego, że typ podwójny ma dokładność mniejszą niż 16 cyfr. Jeśli potrzebujesz większej precyzji, nie jest to odpowiedni typ. Mogą się również gromadzić przybliżenia.

Trzeba powiedzieć, że nawet jeśli używasz arytmetyki stałych punktów, nadal musisz zaokrąglać liczby, gdyby nie fakt, że BigInteger i BigDecimal dają błędy, jeśli otrzymujesz okresowe liczby dziesiętne. Istnieje więc przybliżenie również tutaj.

Na przykład COBOL, historycznie stosowany do obliczeń finansowych, ma maksymalną dokładność 18 cyfr. Dlatego często występuje niejawne zaokrąglenie.

Podsumowując, moim zdaniem podwójny jest nieodpowiedni głównie ze względu na jego 16-cyfrową precyzję, która może być niewystarczająca, nie dlatego, że jest przybliżona.

Rozważ następujące dane wyjściowe następnego programu. Pokazuje, że po zaokrągleniu podwójny daje ten sam wynik co BigDecimal do dokładności 16.

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;
    }
}
użytkownik1593165
źródło
15

Wynik liczby zmiennoprzecinkowej nie jest dokładny, co czyni je nieodpowiednimi do jakichkolwiek obliczeń finansowych, które wymagają dokładnego wyniku, a nie przybliżenia. zmiennoprzecinkowe i podwójne są przeznaczone do obliczeń inżynierskich i naukowych i wiele razy nie dają dokładnych wyników, również wyniki obliczeń zmiennoprzecinkowych mogą się różnić od JVM do JVM. Spójrz na poniższy przykład BigDecimal i podwójnego prymitywu, który jest używany do reprezentowania wartości pieniężnej, jest całkiem jasne, że obliczenia zmiennoprzecinkowe mogą nie być dokładne i należy używać BigDecimal do obliczeń finansowych.

    // 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)));

Wynik:

difference between 2.0 and 1.1 using double is: 0.8999999999999999
difference between 2.0 and 1.1 using BigDecimal is: 0.9
Dheeraj Arora
źródło
3
Spróbujmy czegoś innego niż trywialne dodawanie / odejmowanie i mnożenie liczb całkowitych. Jeśli kod oblicza miesięczną stopę pożyczki w wysokości 7%, oba typy nie będą musiały podać dokładnej wartości i zaokrąglić do najbliższej wartości 0,01. Zaokrąglanie do najniższej jednostki pieniężnej jest częścią obliczeń pieniężnych. Używanie typów dziesiętnych pozwala uniknąć tej potrzeby z dodawaniem / odejmowaniem - ale niewiele więcej.
chux - Przywróć Monikę
@ chux-ReinstateMonica: Jeśli odsetki mają być naliczane co miesiąc, obliczaj odsetki każdego miesiąca poprzez zsumowanie salda dziennego, pomnóż go przez 7 (stopa procentowa) i podziel, zaokrąglając do najbliższego grosz, przez liczbę dni w rok. Żadnego zaokrąglania nigdzie, z wyjątkiem raz w miesiącu na ostatnim etapie.
supercat
@ supercat Mój komentarz podkreśla, że ​​użycie binarnego FP najmniejszej jednostki pieniężnej lub dziesiętnego FP wiąże się z podobnymi problemami z zaokrąglaniem - jak w twoim komentarzu z „i dziel, zaokrąglając do najbliższego grosza”. Użycie podstawowego 2 lub 10 FP nie zapewnia żadnej przewagi w żadnym scenariuszu.
chux - Przywróć Monikę
@ chux-ReinstateMonica: W powyższym scenariuszu, jeśli matematyka okaże się, że odsetki powinny być dokładnie równe pewnej liczbie pół centów, prawidłowy program finansowy musi zaokrąglić w ściśle określony sposób. Jeśli obliczenia zmiennoprzecinkowe dają wartość odsetkową np. 1,23499941 USD, ale matematycznie dokładna wartość przed zaokrągleniem powinna wynosić 1,235 USD, a zaokrąglenie jest określone jako „najbliższe nawet”, użycie takich obliczeń zmiennoprzecinkowych nie spowoduje, że wynik będzie być wyłączony o 0,000059 $, ale raczej o całe 0,01 $, co dla celów księgowych jest po prostu błędne.
supercat
@ superuper Używanie binarnych doubleFP na centach nie miałoby problemów z obliczeniem do 0,5 centów, podobnie jak dziesiętny FP. Jeśli obliczenia zmiennoprzecinkowe dają wartość odsetek np. 123.499941 ¢, albo przez FP binarny, albo FP dziesiętny, problem podwójnego zaokrąglania jest taki sam - nie ma żadnej przewagi. Twoje założenie wydaje się przyjmować matematycznie dokładną wartość, a dziesiętne FP są takie same - coś nawet dziesiętnego FP nie gwarantuje. 0,5 / 7,0 * 7,0 jest problemem dla FP binarnych i deicmal. IAC, większość z nich będzie dyskusyjna, ponieważ oczekuję, że następna wersja C zapewni dziesiętne FP.
chux - Przywróć Monikę
11

Jak powiedziano wcześniej: „Reprezentacja pieniędzy jako liczba podwójna lub zmiennoprzecinkowa na początku będzie prawdopodobnie dobrze wyglądać, ponieważ oprogramowanie uzupełnia drobne błędy, ale w miarę wykonywania kolejnych dodań, odejmowań, mnożenia i podziałów na niedokładnych liczbach tracisz coraz większą precyzję gdy sumują się błędy. To sprawia, że ​​liczba zmiennoprzecinkowa i liczba podwójna są nieodpowiednie do radzenia sobie z pieniędzmi, gdzie wymagana jest idealna dokładność dla wielokrotności bazowych mocy 10 ”.

Wreszcie Java ma standardowy sposób pracy z walutami i pieniędzmi!

JSR 354: API pieniędzy i walut

JSR 354 zapewnia interfejs API do reprezentowania, transportu i wykonywania kompleksowych obliczeń za pomocą pieniędzy i waluty. Możesz pobrać go z tego linku:

JSR 354: Pobieranie interfejsu API pieniędzy i walut

Specyfikacja składa się z następujących rzeczy:

  1. Interfejs API do obsługi np. Kwot pieniężnych i walut
  2. Interfejsy API do obsługi wymiennych implementacji
  3. Fabryki do tworzenia instancji klas implementacji
  4. Funkcjonalność do obliczeń, konwersji i formatowania kwot pieniężnych
  5. Java API do pracy z pieniędzmi i walutami, które planuje się włączyć do Java 9.
  6. Wszystkie klasy specyfikacji i interfejsy znajdują się w pakiecie javax.money. *.

Przykładowe przykłady JSR 354: API pieniędzy i walut:

Przykład utworzenia MonetaryAmount i wydrukowania go na konsoli wygląda następująco:

MonetaryAmountFactory<?> amountFactory = Monetary.getDefaultAmountFactory();
MonetaryAmount monetaryAmount = amountFactory.setCurrency(Monetary.getCurrency("EUR")).setNumber(12345.67).create();
MonetaryAmountFormat format = MonetaryFormats.getAmountFormat(Locale.getDefault());
System.out.println(format.format(monetaryAmount));

Podczas korzystania z referencyjnego interfejsu API implementacji niezbędny kod jest znacznie prostszy:

MonetaryAmount monetaryAmount = Money.of(12345.67, "EUR");
MonetaryAmountFormat format = MonetaryFormats.getAmountFormat(Locale.getDefault());
System.out.println(format.format(monetaryAmount));

Interfejs API obsługuje również obliczenia za pomocą MonetaryAmounts:

MonetaryAmount monetaryAmount = Money.of(12345.67, "EUR");
MonetaryAmount otherMonetaryAmount = monetaryAmount.divide(2).add(Money.of(5, "EUR"));

CurrencyUnit i MonetaryAmount

// getting CurrencyUnits by locale
CurrencyUnit yen = MonetaryCurrencies.getCurrency(Locale.JAPAN);
CurrencyUnit canadianDollar = MonetaryCurrencies.getCurrency(Locale.CANADA);

MonetaryAmount ma różne metody, które umożliwiają dostęp do przypisanej waluty, kwoty liczbowej, jej precyzji i nie tylko:

MonetaryAmount monetaryAmount = Money.of(123.45, euro);
CurrencyUnit currency = monetaryAmount.getCurrency();
NumberValue numberValue = monetaryAmount.getNumber();

int intValue = numberValue.intValue(); // 123
double doubleValue = numberValue.doubleValue(); // 123.45
long fractionDenominator = numberValue.getAmountFractionDenominator(); // 100
long fractionNumerator = numberValue.getAmountFractionNumerator(); // 45
int precision = numberValue.getPrecision(); // 5

// NumberValue extends java.lang.Number. 
// So we assign numberValue to a variable of type Number
Number number = numberValue;

Kwoty pieniężne można zaokrąglać za pomocą operatora zaokrąglania:

CurrencyUnit usd = MonetaryCurrencies.getCurrency("USD");
MonetaryAmount dollars = Money.of(12.34567, usd);
MonetaryOperator roundingOperator = MonetaryRoundings.getRounding(usd);
MonetaryAmount roundedDollars = dollars.with(roundingOperator); // USD 12.35

Podczas pracy z kolekcjami MonetaryAmounts dostępnych jest kilka przydatnych narzędzi do filtrowania, sortowania i grupowania.

List<MonetaryAmount> amounts = new ArrayList<>();
amounts.add(Money.of(2, "EUR"));
amounts.add(Money.of(42, "USD"));
amounts.add(Money.of(7, "USD"));
amounts.add(Money.of(13.37, "JPY"));
amounts.add(Money.of(18, "USD"));

Niestandardowe operacje MonetaryAmount

// A monetary operator that returns 10% of the input MonetaryAmount
// Implemented using Java 8 Lambdas
MonetaryOperator tenPercentOperator = (MonetaryAmount amount) -> {
  BigDecimal baseAmount = amount.getNumber().numberValue(BigDecimal.class);
  BigDecimal tenPercent = baseAmount.multiply(new BigDecimal("0.1"));
  return Money.of(tenPercent, amount.getCurrency());
};

MonetaryAmount dollars = Money.of(12.34567, "USD");

// apply tenPercentOperator to MonetaryAmount
MonetaryAmount tenPercentDollars = dollars.with(tenPercentOperator); // USD 1.234567

Zasoby:

Obsługa pieniędzy i walut w Javie z JSR 354

Patrząc na interfejs API pieniędzy i walut Java 9 (JSR 354)

Zobacz także: JSR 354 - Waluta i pieniądze

Affy
źródło
5

Jeśli twoje obliczenia obejmują różne etapy, arytmetyka dowolnej precyzji nie obejmie cię w 100%.

Jedyny niezawodny sposób na wykorzystanie doskonałej reprezentacji wyników (użyj niestandardowego typu danych Ułamek, który spowoduje podział operacji wsadowych do ostatniego kroku) i konwersję na zapis dziesiętny w ostatnim kroku.

Arbitralna precyzja nie pomoże, ponieważ zawsze mogą istnieć liczby, które mają tyle miejsc po przecinku, lub niektóre wyniki, takie jak 0.6666666... Żadna arbitralna reprezentacja nie obejmie ostatniego przykładu. Będziesz miał małe błędy na każdym kroku.

Błędy te będą się sumować, mogą w końcu przestać być łatwe do zignorowania. Nazywa się to Propagacją błędów .

Kemal Dağ
źródło
4

W większości odpowiedzi podkreślono powody, dla których nie należy używać dubletów do obliczania pieniędzy i walut. I całkowicie się z nimi zgadzam.

Nie oznacza to jednak, że do tego celu nigdy nie można użyć podwójnych.

Pracowałem nad wieloma projektami o bardzo niskich wymaganiach gc, a posiadanie obiektów BigDecimal było dużym wkładem w to obciążenie.

To brak zrozumienia podwójnej reprezentacji i brak doświadczenia w radzeniu sobie z dokładnością i precyzją powodują, że ta mądra sugestia.

Możesz sprawić, by działało, jeśli jesteś w stanie sprostać wymaganiom dotyczącym precyzji i dokładności swojego projektu, co należy zrobić w oparciu o zakres podwójnych wartości.

Możesz zapoznać się z metodą FuzzyCompare guawy, aby uzyskać więcej pomysłów. Kluczem jest tolerancja parametru. Rozwiązaliśmy ten problem w przypadku aplikacji do obrotu papierami wartościowymi i przeprowadziliśmy dogłębne badanie, jakie tolerancje należy zastosować dla różnych wartości liczbowych w różnych zakresach.

Ponadto mogą wystąpić sytuacje, w których masz ochotę użyć podwójnego opakowania jako klucza mapy, a implementacją jest mapa mieszania. Jest to bardzo ryzykowne, ponieważ Double.equals i kod skrótu na przykład wartości „0,5” i „0,6 - 0,1” spowodują wielki bałagan.

Dev Amitabh
źródło
2

Wiele odpowiedzi na to pytanie dotyczy IEEE i standardów otaczających arytmetykę zmiennoprzecinkową.

Pochodząc z innych niż informatyka podstaw (fizyka i inżynieria), patrzę na problemy z innej perspektywy. Dla mnie powodem, dla którego nie użyłbym podwójnego lub zmiennoprzecinkowego w obliczeniach matematycznych, jest to, że straciłbym zbyt dużo informacji.

Jakie są alternatywy? Jest ich wiele (i wielu innych nie jestem świadom!).

BigDecimal w Javie jest rodzimy dla języka Java. Apfloat to kolejna biblioteka o dowolnej precyzji dla Java.

Dziesiętny typ danych w C # jest alternatywą Microsoft .NET dla 28 znaczących liczb.

SciPy (Python naukowy) prawdopodobnie może również obsługiwać obliczenia finansowe (nie próbowałem, ale podejrzewam, że tak).

Biblioteka GNU Multiple Precision Library (GMP) i GNU MFPR Library to dwa bezpłatne i otwarte zasoby dla C i C ++.

Istnieją również biblioteki precyzji numerycznych dla JavaScript (!) I myślę, że PHP może obsługiwać obliczenia finansowe.

Istnieją również autorskie rozwiązania (szczególnie, jak sądzę, dla Fortran) oraz rozwiązania typu open source dla wielu języków komputerowych.

Z wykształcenia nie jestem informatykiem. Jednak mam tendencję do skłaniania się do BigDecimal w Javie lub dziesiętnych w C #. Nie wypróbowałem innych wymienionych przeze mnie rozwiązań, ale prawdopodobnie są one również bardzo dobre.

Dla mnie podoba mi się BigDecimal ze względu na obsługiwane przez niego metody. Dziesiętny C # jest bardzo ładny, ale nie miałem okazji pracować z nim tak bardzo, jak bym chciał. W wolnych chwilach wykonuję obliczenia naukowe, a BigDecimal wydaje się działać bardzo dobrze, ponieważ mogę ustawić dokładność liczb zmiennoprzecinkowych. Wada BigDecimal? Czasami może być powolny, szczególnie jeśli używasz metody dzielenia.

Możesz, dla szybkości, zajrzeć do darmowych i zastrzeżonych bibliotek w językach C, C ++ i Fortran.

rybak
źródło
1
Jeśli chodzi o SciPy / Numpy, stała precyzja (tj. Liczba dziesiętna w Pythonie) nie jest obsługiwana ( docs.scipy.org/doc/numpy-dev/user/basics.types.html ). Niektóre funkcje nie będą działać poprawnie w trybie dziesiętnym (na przykład isnan). Panda opiera się na Numpy i została zainicjowana w AQR, jednym z głównych ilościowych funduszy hedgingowych. Masz swoją odpowiedź na temat obliczeń finansowych (nie rachunkowości spożywczej).
comte
2

Aby dodać poprzednie odpowiedzi, istnieje również opcja implementacji Joda-Money w Javie, oprócz BigDecimal, podczas rozwiązywania problemu opisanego w pytaniu. Nazwa modułu Java to org.joda.money.

Wymaga Java SE 8 lub nowszej i nie ma żadnych zależności.

Mówiąc dokładniej, istnieje zależność od czasu kompilacji, ale nie jest ona wymagana.

<dependency>
  <groupId>org.joda</groupId>
  <artifactId>joda-money</artifactId>
  <version>1.0.1</version>
</dependency>

Przykłady korzystania z Joda Money:

  // create a monetary value
  Money money = Money.parse("USD 23.87");

  // add another amount with safe double conversion
  CurrencyUnit usd = CurrencyUnit.of("USD");
  money = money.plus(Money.of(usd, 12.43d));

  // subtracts an amount in dollars
  money = money.minusMajor(2);

  // multiplies by 3.5 with rounding
  money = money.multipliedBy(3.5d, RoundingMode.DOWN);

  // compare two amounts
  boolean bigAmount = money.isGreaterThan(dailyWage);

  // convert to GBP using a supplied rate
  BigDecimal conversionRate = ...;  // obtained from code outside Joda-Money
  Money moneyGBP = money.convertedTo(CurrencyUnit.GBP, conversionRate, RoundingMode.HALF_UP);

  // use a BigMoney for more complex calculations where scale matters
  BigMoney moneyCalc = money.toBigMoney();

Dokumentacja: http://joda-money.sourceforge.net/apidocs/org/joda/money/Money.html

Przykłady realizacji: https://www.programcreek.com/java-api-examples/?api=org.joda.money.Money

Tadija Malić
źródło
0

Kilka przykładów ... to działa (właściwie nie działa zgodnie z oczekiwaniami) na prawie każdym języku programowania ... Próbowałem z Delphi, VBScript, Visual Basic, JavaScript, a teraz z Java / Android:

    double total = 0.0;

    // do 10 adds of 10 cents
    for (int i = 0; i < 10; i++) {
        total += 0.1;  // adds 10 cents
    }

    Log.d("round problems?", "current total: " + total);

    // looks like total equals to 1.0, don't?

    // now, do reverse
    for (int i = 0; i < 10; i++) {
        total -= 0.1;  // removes 10 cents
    }

    // looks like total equals to 0.0, don't?
    Log.d("round problems?", "current total: " + total);
    if (total == 0.0) {
        Log.d("round problems?", "is total equal to ZERO? YES, of course!!");
    } else {
        Log.d("round problems?", "is total equal to ZERO? NO... thats why you should not use Double for some math!!!");
    }

WYNIK:

round problems?: current total: 0.9999999999999999 round problems?: current total: 2.7755575615628914E-17 round problems?: is total equal to ZERO? NO... thats why you should not use Double for some math!!!

WilliamK
źródło
3
Problem nie polega na tym, że zdarza się błąd zaokrąglenia, ale że się nim nie zajmuje. Zaokrąglij wynik do dwóch miejsc po przecinku (jeśli chcesz centów) i gotowe.
maaartinus
0

Float jest binarną formą dziesiętną o innym wzorze; To są dwie różne rzeczy. Pomiędzy dwoma typami występuje niewiele błędów po ich konwersji. Ponadto zmiennoprzecinkowe ma na celu reprezentowanie nieskończonej dużej liczby wartości naukowych. Oznacza to, że został zaprojektowany z myślą o utracie precyzji do ekstremalnie małej i ekstremalnie dużej liczby przy tej stałej liczbie bajtów. Dziesiętny nie może reprezentować nieskończonej liczby wartości, ogranicza się tylko do tej liczby cyfr dziesiętnych. Zatem zmiennoprzecinkowe i dziesiętne służą do różnych celów.

Istnieje kilka sposobów zarządzania błędem wartości waluty:

  1. Użyj długiej liczby całkowitej i zamiast tego policz w centach.

  2. Używaj podwójnej precyzji, utrzymuj tylko cyfry znaczące do 15, aby dziesiętne można dokładnie symulować. Zaokrąglij przed przedstawieniem wartości; Okrążaj często podczas wykonywania obliczeń.

  3. Użyj biblioteki dziesiętnej, takiej jak Java BigDecimal, więc nie musisz używać podwójnej do symulacji dziesiętnej.

ps ciekawie jest wiedzieć, że większość marek przenośnych kalkulatorów naukowych działa na liczbach dziesiętnych zamiast pływakowych. Więc żadna skarga nie zawiera błędów konwersji.

Chris Tsang
źródło