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.
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.
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
lub float
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 # ma decimal
typ.
1.0 / 10 * 10
może nie być taki sam jak 1.0.
Z Bloch, J., Effective Java, wyd. 2, pozycja 48:
float
Idouble
rodzaje 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) jakofloat
lubdouble
dokł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
,int
lublong
do obliczeń pieniężnych.
Chociaż BigDecimal
ma pewne zastrzeżenia (zobacz aktualnie zaakceptowaną odpowiedź).
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.
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
System.out.println(1000000.0f + 1.2f - 1000000.0f);
prowadzi do
1.1875
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.
float
, double
I BigDecimal
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.
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;
}
}
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
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:
- Interfejs API do obsługi np. Kwot pieniężnych i walut
- Interfejsy API do obsługi wymiennych implementacji
- Fabryki do tworzenia instancji klas implementacji
- Funkcjonalność do obliczeń, konwersji i formatowania kwot pieniężnych
- Java API do pracy z pieniędzmi i walutami, które planuje się włączyć do Java 9.
- 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
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 .
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.
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.
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
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!!!
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.