Mi è sempre stato detto di non rappresentare denaro double
o float
tipi, e questa volta ti pongo la domanda: perché?
Sono sicuro che ci sia un'ottima ragione, semplicemente non so cosa sia.
Mi è sempre stato detto di non rappresentare denaro double
o float
tipi, e questa volta ti pongo la domanda: perché?
Sono sicuro che ci sia un'ottima ragione, semplicemente non so cosa sia.
Risposte:
Perché float e double non possono rappresentare accuratamente i multipli di base 10 che usiamo per soldi. Questo problema non è solo per Java, è per qualsiasi linguaggio di programmazione che utilizza tipi a virgola mobile di base 2.
Nella base 10, puoi scrivere 10.25 come 1025 * 10 -2 (un numero intero per una potenza di 10). I numeri in virgola mobile IEEE-754 sono diversi, ma un modo molto semplice di pensarci è invece moltiplicare per una potenza di due. Ad esempio, potresti vedere 164 * 2 -4 (un numero intero per una potenza di due), che è anche uguale a 10,25. Non è così che i numeri sono rappresentati in memoria, ma le implicazioni matematiche sono le stesse.
Anche nella base 10, questa notazione non può rappresentare accuratamente le frazioni più semplici. Ad esempio, non puoi rappresentare 1/3: la rappresentazione decimale si ripete (0.3333 ...), quindi non esiste un numero intero finito che puoi moltiplicare per una potenza di 10 per ottenere 1/3. Potresti accontentarti di una lunga sequenza di 3 e di un piccolo esponente, come 333333333 * 10 -10 , ma non è preciso: se lo moltiplichi per 3, non otterrai 1.
Tuttavia, allo scopo di contare i soldi, almeno per i paesi i cui soldi sono valutati in un ordine di grandezza del dollaro USA, di solito tutto ciò che serve è essere in grado di memorizzare multipli di 10 -2 , quindi non importa davvero che 1/3 non può essere rappresentato.
Il problema con float e doppi è che la stragrande maggioranza dei numeri simili a soldi non ha una rappresentazione esatta come un numero intero per una potenza di 2. In realtà, i soli multipli di 0,01 tra 0 e 1 (che sono significativi quando si tratta con denaro perché sono centesimi interi) che possono essere rappresentati esattamente come un numero in virgola mobile binario IEEE-754 sono 0, 0,25, 0,5, 0,75 e 1. Tutti gli altri sono disattivati di una piccola quantità. Come analogia all'esempio 0.333333, se prendi il valore in virgola mobile per 0,1 e lo moltiplichi per 10, non otterrai 1.
Rappresentare il denaro come double
o float
probabilmente avrà un bell'aspetto all'inizio quando il software completa i piccoli errori, ma man mano che esegui più addizioni, sottrazioni, moltiplicazioni e divisioni su numeri inesatti, gli errori si combinano e finirai con valori visibilmente non preciso. Ciò rende i galleggianti e i doppi inadeguati per gestire il denaro, dove è richiesta la massima precisione per multipli di potenze di base 10.
Una soluzione che funziona in quasi tutte le lingue è utilizzare numeri interi e contare i centesimi. Ad esempio, 1025 sarebbe $ 10,25. Diverse lingue hanno anche tipi integrati per gestire il denaro. Tra gli altri, Java ha la BigDecimal
classe e C # ha il decimal
tipo.
1.0 / 10 * 10
potrebbe non essere uguale a 1.0.
Da Bloch, J., Effective Java, 2nd ed, Item 48:
I tipi
float
edouble
sono particolarmente inadatti ai calcoli monetari perché è impossibile rappresentare 0,1 (o qualsiasi altro potere negativo di dieci) come unofloat
odouble
esattamente.Ad esempio, supponiamo di avere $ 1,03 e di spendere 42c. Quanti soldi ti rimangono?
System.out.println(1.03 - .42);
stampa
0.6100000000000001
.Il modo giusto per risolvere questo problema è quello di utilizzare
BigDecimal
,int
olong
per i calcoli monetari.
Sebbene BigDecimal
abbia alcuni avvertimenti (vedi la risposta attualmente accettata).
long a = 104
e conti in centesimi anziché in dollari.
BigDecimal
.
Questa non è una questione di precisione, né una questione di precisione. Si tratta di soddisfare le aspettative degli umani che usano la base 10 per i calcoli anziché la base 2. Ad esempio, l'uso del doppio per i calcoli finanziari non produce risposte "sbagliate" in senso matematico, ma può produrre risposte che sono non quello che ci si aspetta in senso finanziario.
Anche se si completano i risultati all'ultimo minuto prima dell'output, è comunque possibile ottenere occasionalmente un risultato utilizzando i doppi che non corrispondono alle aspettative.
Utilizzando una calcolatrice o calcolando i risultati a mano, 1,40 * 165 = 231 esattamente. Tuttavia, usando internamente i doppi, nel mio ambiente di compilatore / sistema operativo, viene memorizzato come un numero binario vicino a 230.99999 ... quindi se si tronca il numero, si ottiene 230 anziché 231. Si potrebbe pensare che l'arrotondamento invece di troncare sarebbe hanno dato il risultato desiderato di 231. È vero, ma l'arrotondamento comporta sempre il troncamento. Qualunque sia la tecnica di arrotondamento che usi, ci sono ancora condizioni al contorno come questa che si arrotondano quando ti aspetti che arrotonda. Sono abbastanza rari che spesso non saranno trovati attraverso test o osservazioni casuali. Potrebbe essere necessario scrivere del codice per cercare esempi che illustrino risultati che non si comportano come previsto.
Supponi di voler arrotondare qualcosa al penny più vicino. Quindi prendi il tuo risultato finale, moltiplica per 100, aggiungi 0,5, tronca, quindi dividi il risultato per 100 per tornare ai centesimi. Se il numero interno che hai memorizzato era 3.46499999 .... anziché 3.465, otterrai 3,46 invece 3,47 quando arrotondi il numero al penny più vicino. Ma i tuoi calcoli di base 10 potrebbero aver indicato che la risposta dovrebbe essere esattamente 3.465, che dovrebbe chiaramente arrotondare a 3,47, non a 3,46. Questo genere di cose accade occasionalmente nella vita reale quando si usano i doppi per i calcoli finanziari. È raro, quindi spesso passa inosservato come un problema, ma succede.
Se usi la base 10 per i tuoi calcoli interni invece dei doppi, le risposte sono sempre esattamente quello che ci si aspetta dagli umani, assumendo che non ci siano altri bug nel tuo codice.
Math.round(0.49999999999999994)
restituisce 1?
Sono turbato da alcune di queste risposte. Penso che doppi e float abbiano un posto nei calcoli finanziari. Certamente, quando si sommano e si sottraggono importi monetari non frazionari non si verificherà alcuna perdita di precisione quando si utilizzano classi intere o classi BigDecimal. Ma quando si eseguono operazioni più complesse, spesso si ottengono risultati che escono con più o più cifre decimali, indipendentemente da come si memorizzano i numeri. Il problema è come presentare il risultato.
Se il tuo risultato è al limite tra l'arrotondamento e l'arrotondamento e l'ultimo penny conta davvero, dovresti probabilmente dire allo spettatore che la risposta è quasi al centro, visualizzando più cifre decimali.
Il problema con i doppi, e ancora di più con i float, è quando vengono usati per combinare numeri grandi e numeri piccoli. A java,
System.out.println(1000000.0f + 1.2f - 1000000.0f);
risultati in
1.1875
Galleggianti e doppi sono approssimativi. Se crei un BigDecimal e passi un float nel costruttore, vedi cosa equivale effettivamente al float:
groovy:000> new BigDecimal(1.0F)
===> 1
groovy:000> new BigDecimal(1.01F)
===> 1.0099999904632568359375
questo probabilmente non è come si desidera rappresentare $ 1,01.
Il problema è che le specifiche IEEE non hanno un modo per rappresentare esattamente tutte le frazioni, alcune delle quali finiscono come frazioni ripetute, quindi si verificano errori di approssimazione. Poiché ai ragionieri piace che le cose escano esattamente al centesimo, e i clienti saranno infastiditi se pagano il conto e dopo che il pagamento è stato elaborato devono .01 e ricevono una commissione o non riescono a chiudere il conto, è meglio usare tipi esatti come decimale (in C #) o java.math.BigDecimal in Java.
Non è che l'errore non sia controllabile se giri: leggi questo articolo di Peter Lawrey . È solo più facile non dover arrotondare in primo luogo. La maggior parte delle applicazioni che gestiscono denaro non richiede molta matematica, le operazioni consistono nell'aggiungere cose o nell'allocare gli importi in diversi bucket. L'introduzione di virgola mobile e arrotondamento complica solo le cose.
float
, double
E BigDecimal
sono rappresentano precisi valori. La conversione da codice a oggetto non è esatta così come altre operazioni. I tipi stessi non sono inesatti.
Rischio di essere sottoposto a downgrade, ma penso che l'inadeguatezza dei numeri in virgola mobile per i calcoli delle valute sia sopravvalutata. Fintanto che ti assicuri di eseguire correttamente l'arrotondamento dei centesimi e di avere cifre abbastanza significative con cui lavorare per contrastare la mancata corrispondenza della rappresentazione binaria-decimale spiegata da zneak, non ci saranno problemi.
Le persone che calcolano con la valuta in Excel hanno sempre usato i float a doppia precisione (non esiste un tipo di valuta in Excel) e non ho ancora visto nessuno lamentarsi di errori di arrotondamento.
Certo, devi rimanere nei limiti della ragione; ad esempio, un semplice negozio online probabilmente non avrebbe mai avuto problemi con i float a doppia precisione, ma se si esegue ad esempio la contabilità o qualsiasi altra cosa che richieda l'aggiunta di una grande quantità (illimitata) di numeri, non si vorrebbe toccare i numeri in virgola mobile con un metro e mezzo palo.
Mentre è vero che il tipo a virgola mobile può rappresentare solo approssimativamente i dati decimali, è anche vero che se si arrotondano i numeri alla precisione necessaria prima di presentarli, si ottiene il risultato corretto. Generalmente.
Di solito perché il doppio tipo ha una precisione inferiore a 16 cifre. Se hai bisogno di una migliore precisione, non è un tipo adatto. Anche le approssimazioni possono accumularsi.
Va detto che anche se usi l'aritmetica in virgola fissa devi comunque arrotondare i numeri, se non fosse per il fatto che BigInteger e BigDecimal danno errori se ottieni numeri decimali periodici. Quindi c'è un'approssimazione anche qui.
Ad esempio COBOL, utilizzato storicamente per i calcoli finanziari, ha una precisione massima di 18 cifre. Quindi c'è spesso un arrotondamento implicito.
Concludendo, secondo me il doppio non è adatto principalmente per la sua precisione a 16 cifre, che può essere insufficiente, non perché è approssimativa.
Si consideri il seguente output del programma successivo. Mostra che dopo l'arrotondamento il doppio dà lo stesso risultato di BigDecimal fino alla precisione 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;
}
}
Il risultato del numero in virgola mobile non è esatto, il che li rende inadatti per qualsiasi calcolo finanziario che richiede un risultato esatto e non un'approssimazione. float e double sono progettati per calcoli tecnici e scientifici e molte volte non producono risultati esatti, anche i risultati del calcolo in virgola mobile possono variare da JVM a JVM. Guarda l'esempio seguente di BigDecimal e doppia primitiva che viene utilizzato per rappresentare il valore del denaro, è abbastanza chiaro che il calcolo in virgola mobile potrebbe non essere esatto e si dovrebbe usare BigDecimal per i calcoli finanziari.
// 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)));
Produzione:
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 binario al centesimo non avrebbe problemi a calcolare lo 0,5 centesimo come non lo sarebbe nemmeno il FP decimale. Se i calcoli in virgola mobile producono un valore di interesse, ad esempio 123,499941 ¢, tramite FP binario o FP decimale, il problema del doppio arrotondamento è lo stesso - nessun vantaggio in entrambi i casi. La tua premessa sembra assumere il valore matematicamente preciso e il FP decimale è lo stesso - qualcosa che anche il FP decimale non garantisce. 0,5 / 7,0 * 7,0 è un problema per FP binario e deicmal. IAC, la maggior parte sarà discutibile poiché mi aspetto che la prossima versione di C fornisca FP decimale.
Come detto in precedenza "La rappresentazione del denaro come double o float probabilmente avrà un bell'aspetto all'inizio poiché il software completa i piccoli errori, ma man mano che esegui ulteriori aggiunte, sottrazioni, moltiplicazioni e divisioni su numeri inesatti, perderai sempre più precisione man mano che gli errori si sommano. Ciò rende i float e i doppi inadeguati per gestire il denaro, dove è richiesta la massima precisione per multipli di potenze di base 10 ".
Finalmente Java ha un modo standard di lavorare con Currency And Money!
JSR 354: API per denaro e valuta
JSR 354 fornisce un'API per la rappresentazione, il trasporto e l'esecuzione di calcoli completi con denaro e valuta. Puoi scaricarlo da questo link:
JSR 354: Download dell'API per denaro e valuta
Le specifiche sono le seguenti:
- Un'API per la gestione, ad esempio, di importi e valute monetarie
- API per supportare implementazioni intercambiabili
- Fabbriche per la creazione di istanze delle classi di implementazione
- Funzionalità per calcoli, conversione e formattazione di importi monetari
- API Java per lavorare con Money and Currencies, che dovrebbe essere inclusa in Java 9.
- Tutte le classi e le interfacce delle specifiche si trovano nel pacchetto javax.money. *.
Esempi di esempio di JSR 354: API denaro e valuta:
Un esempio di creazione di MonetaryAmount e di stampa sulla console è simile al seguente:
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));
Quando si utilizza l'API di implementazione di riferimento, il codice necessario è molto più semplice:
MonetaryAmount monetaryAmount = Money.of(12345.67, "EUR");
MonetaryAmountFormat format = MonetaryFormats.getAmountFormat(Locale.getDefault());
System.out.println(format.format(monetaryAmount));
L'API supporta anche i calcoli con MonetaryAmounts:
MonetaryAmount monetaryAmount = Money.of(12345.67, "EUR");
MonetaryAmount otherMonetaryAmount = monetaryAmount.divide(2).add(Money.of(5, "EUR"));
CurrencyUnit e MonetaryAmount
// getting CurrencyUnits by locale
CurrencyUnit yen = MonetaryCurrencies.getCurrency(Locale.JAPAN);
CurrencyUnit canadianDollar = MonetaryCurrencies.getCurrency(Locale.CANADA);
MonetaryAmount ha vari metodi che consentono di accedere alla valuta assegnata, all'importo numerico, alla sua precisione e altro:
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;
Gli importi monetari possono essere arrotondati utilizzando un operatore di arrotondamento:
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
Quando si lavora con raccolte di MonetaryAmounts, sono disponibili alcuni metodi di utilità utili per filtrare, ordinare e raggruppare.
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"));
Operazioni MonetaryAmount personalizzate
// 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
risorse:
Gestire denaro e valute in Java con JSR 354
Esame dell'API Money and Currency di Java 9 (JSR 354)
Vedi anche: JSR 354 - Valuta e denaro
Se il tuo calcolo prevede vari passaggi, l'aritmetica di precisione arbitraria non ti coprirà al 100%.
L'unico modo affidabile per utilizzare una rappresentazione perfetta dei risultati (utilizzare un tipo di dati Frazione personalizzato che raggrupperà le operazioni di divisione nell'ultimo passaggio) e convertirà solo in notazione decimale nell'ultimo passaggio.
La precisione arbitraria non aiuterà perché ci possono sempre essere numeri con così tanti decimali o alcuni risultati come 0.6666666
... Nessuna rappresentazione arbitraria coprirà l'ultimo esempio. Quindi avrai piccoli errori in ogni passaggio.
Questi errori si sommano e potrebbero non essere più facili da ignorare. Questo si chiama Propagazione errori .
La maggior parte delle risposte ha evidenziato i motivi per cui non si dovrebbero usare i doppi per i calcoli di denaro e valuta. E sono totalmente d'accordo con loro.
Ciò non significa che i doppi non possano mai essere usati a tale scopo.
Ho lavorato su una serie di progetti con requisiti gc molto bassi e avere oggetti BigDecimal ha contribuito notevolmente a questo overhead.
È la mancanza di comprensione della doppia rappresentazione e la mancanza di esperienza nella gestione dell'accuratezza e della precisione che danno origine a questo saggio suggerimento.
Puoi farlo funzionare se sei in grado di gestire i requisiti di precisione e accuratezza del tuo progetto, che deve essere fatto in base a quale intervallo di doppi valori si tratta.
Puoi fare riferimento al metodo FuzzyCompare di guava per avere più idea. La tolleranza dei parametri è la chiave. Abbiamo affrontato questo problema per un'applicazione di trading di titoli e abbiamo fatto una ricerca esaustiva su quali tolleranze usare per valori numerici diversi in intervalli diversi.
Inoltre, potrebbero verificarsi situazioni in cui si è tentati di utilizzare i wrapper doppi come chiave della mappa con la mappa hash come implementazione. È molto rischioso perché Double.equals e il codice hash, ad esempio i valori "0,5" e "0,6 - 0,1", causeranno un grande casino.
Molte delle risposte inviate a questa domanda parlano dell'IEEE e degli standard che circondano l'aritmetica in virgola mobile.
Provenendo da un background non informatico (fisica e ingegneria), tendo a guardare i problemi da una prospettiva diversa. Per me, il motivo per cui non avrei usato un double o float in un calcolo matematico è che avrei perso troppe informazioni.
Quali sono le alternative? Ce ne sono molti (e molti altri di cui non sono a conoscenza!).
BigDecimal in Java è nativo del linguaggio Java. Apfloat è un'altra libreria di precisione arbitraria per Java.
Il tipo di dati decimali in C # è l'alternativa .NET di Microsoft per 28 cifre significative.
SciPy (Scientific Python) probabilmente può anche gestire calcoli finanziari (non ho provato, ma sospetto di sì).
La GNU Multiple Precision Library (GMP) e la GNU MFPR Library sono due risorse gratuite e open source per C e C ++.
Ci sono anche librerie di precisione numerica per JavaScript (!) E penso che PHP sia in grado di gestire calcoli finanziari.
Esistono anche soluzioni proprietarie (in particolare, credo, per Fortran) e open source per molti linguaggi informatici.
Non sono un informatico in allenamento. Tuttavia, tendo a BigDecimal in Java o decimale in C #. Non ho provato le altre soluzioni che ho elencato, ma probabilmente sono anche molto buone.
Per me, mi piace BigDecimal a causa dei metodi che supporta. Il decimale di C # è molto bello, ma non ho avuto la possibilità di lavorarci quanto mi piacerebbe. Faccio calcoli scientifici di mio interesse nel mio tempo libero e BigDecimal sembra funzionare molto bene perché posso impostare la precisione dei miei numeri in virgola mobile. Lo svantaggio di BigDecimal? A volte può essere lento, specialmente se stai usando il metodo divide.
Potresti, per velocità, consultare le librerie libere e proprietarie in C, C ++ e Fortran.
Per aggiungere le risposte precedenti, c'è anche la possibilità di implementare Joda-Money in Java, oltre a BigDecimal, quando si affronta il problema affrontato nella domanda. Il nome del modulo Java è org.joda.money.
Richiede Java SE 8 o versioni successive e non ha dipendenze.
Per essere più precisi, esiste una dipendenza in fase di compilazione ma non è richiesta.
<dependency>
<groupId>org.joda</groupId>
<artifactId>joda-money</artifactId>
<version>1.0.1</version>
</dependency>
Esempi di utilizzo di 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();
Documentazione: http://joda-money.sourceforge.net/apidocs/org/joda/money/Money.html
Esempi di implementazione: https://www.programcreek.com/java-api-examples/?api=org.joda.money.Money
Qualche esempio ... questo funziona (in realtà non funziona come previsto), su quasi tutti i linguaggi di programmazione ... Ho provato con Delphi, VBScript, Visual Basic, JavaScript e ora con 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!!!");
}
PRODUZIONE:
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 è una forma binaria di decimale con design diverso; sono due cose differenti. Ci sono piccoli errori tra due tipi quando convertiti l'uno nell'altro. Inoltre, float è progettato per rappresentare un numero infinito di valori per scopi scientifici. Ciò significa che è progettato per perdere la precisione a un numero estremamente piccolo ed estremo con quel numero fisso di byte. Il decimale non può rappresentare un numero infinito di valori, si limita a quel numero di cifre decimali. Quindi Float e Decimal hanno scopi diversi.
Esistono alcuni modi per gestire l'errore per il valore della valuta:
Usa numeri interi lunghi e conta invece in centesimi.
Usa la doppia precisione, mantieni le cifre significative su 15 solo in modo che i decimali possano essere simulati esattamente. Round prima di presentare i valori; Arrotondare spesso quando si eseguono calcoli.
Utilizzare una libreria decimale come Java BigDecimal, quindi non è necessario utilizzare double per simulare il decimale.
ps è interessante sapere che la maggior parte delle marche di calcolatrici scientifiche portatili funzionano su decimali anziché su virgola mobile. Quindi nessun reclamo segnala errori di conversione.