Zawsze mówiono mi, żebym nigdy nie reprezentował pieniędzy double
ani float
typów, i tym razem zadaję ci pytanie: dlaczego?
Jestem pewien, że istnieje bardzo dobry powód, po prostu nie wiem co to jest.
floating-point
currency
Fran Fitzpatrick
źródło
źródło
Odpowiedzi:
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
double
lubfloat
prawdopodobnie 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
BigDecimal
klasę, a C # madecimal
typ.źródło
1.0 / 10 * 10
może nie być taki sam jak 1.0.Z Bloch, J., Effective Java, wyd. 2, pozycja 48:
Chociaż
BigDecimal
ma pewne zastrzeżenia (zobacz aktualnie zaakceptowaną odpowiedź).źródło
long a = 104
i liczysz w centach zamiast w dolarach.BigDecimal
.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.
źródło
Math.round(0.49999999999999994)
zwraca 1?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
prowadzi do
źródło
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:
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.
źródło
float
,double
IBigDecimal
są przedstawicielami dokładnych wartości. Konwersja kodu na obiekt jest niedokładna, podobnie jak inne operacje. Same typy nie są niedokładne.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.
źródło
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.
źródło
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.
Wynik:
źródło
double
FP 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.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:
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:
Podczas korzystania z referencyjnego interfejsu API implementacji niezbędny kod jest znacznie prostszy:
Interfejs API obsługuje również obliczenia za pomocą MonetaryAmounts:
CurrencyUnit i MonetaryAmount
MonetaryAmount ma różne metody, które umożliwiają dostęp do przypisanej waluty, kwoty liczbowej, jej precyzji i nie tylko:
Kwoty pieniężne można zaokrąglać za pomocą operatora zaokrąglania:
Podczas pracy z kolekcjami MonetaryAmounts dostępnych jest kilka przydatnych narzędzi do filtrowania, sortowania i grupowania.
Niestandardowe operacje MonetaryAmount
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
źródło
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 .
źródło
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.
źródło
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.
źródło
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.
Przykłady korzystania z Joda Money:
źródło
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:
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!!!
źródło
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:
Użyj długiej liczby całkowitej i zamiast tego policz w centach.
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ń.
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.
źródło