Perché non usare Double o Float per rappresentare la valuta?


939

Mi è sempre stato detto di non rappresentare denaro doubleo floattipi, e questa volta ti pongo la domanda: perché?

Sono sicuro che ci sia un'ottima ragione, semplicemente non so cosa sia.


4
Vedi questa domanda SO: errori di arrotondamento?
Jeff Ogata,

80
Giusto per essere chiari, non dovrebbero essere usati per tutto ciò che richiede accuratezza, non solo la valuta.
Jeff,

152
Non dovrebbero essere usati per tutto ciò che richiede esattezza . Ma i 53 bit significativi del doppio (~ 16 cifre decimali) sono generalmente abbastanza buoni per cose che richiedono semplicemente precisione .
dan04,

21
@jeff Il tuo commento travisa completamente ciò per cui la virgola mobile binaria è buona e per cosa non è buona. Leggi la risposta di zneak di seguito e cancella il tuo commento fuorviante.
Pascal Cuoq,

Risposte:


985

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 doubleo floatprobabilmente 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 BigDecimalclasse e C # ha il decimaltipo.


3
@Fran Si verificheranno errori di arrotondamento e in alcuni casi in cui vengono utilizzate grandi quantità di valuta, i calcoli dei tassi di interesse possono essere notevolmente off
linuxuser27

5
... la maggior parte delle 10 frazioni di base, cioè. Ad esempio, 0.1 non ha una rappresentazione binaria in virgola mobile esatta. Quindi, 1.0 / 10 * 10potrebbe non essere uguale a 1.0.
Chris Jester-Young,

6
@ linuxuser27 Penso che Fran stesse cercando di essere divertente. Ad ogni modo, la risposta di zneak è la migliore che abbia mai visto, migliore della versione classica di Bloch.
Isaac Rabinovitch,

5
Ovviamente se conosci la precisione, puoi sempre arrotondare il risultato ed evitare così l'intero problema. Questo è molto più veloce e più semplice rispetto all'utilizzo di BigDecimal. Un'altra alternativa è quella di utilizzare la precisione fissa int o long.
Peter Lawrey,

2
@zneak sai che i numeri razionali sono un sottoinsieme dei numeri reali, giusto? I numeri reali IEEE-754 sono numeri reali. Capita solo di essere anche razionali.
Tim Seguine,

314

Da Bloch, J., Effective Java, 2nd ed, Item 48:

I tipi floate doublesono particolarmente inadatti ai calcoli monetari perché è impossibile rappresentare 0,1 (o qualsiasi altro potere negativo di dieci) come uno floato doubleesattamente.

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, into long per i calcoli monetari.

Sebbene BigDecimalabbia alcuni avvertimenti (vedi la risposta attualmente accettata).


6
Sono un po 'confuso dalla raccomandazione di usare int o long per i calcoli monetari. Come si rappresenta 1.03 come int o long? Ho provato "long a = 1.04;" e "long a = 104/100;" inutilmente.
Peter,

49
@Peter, usi long a = 104e conti in centesimi anziché in dollari.
zneak,

@zneak Che dire di quando una percentuale deve essere applicata come interesse composto o simile?
trusktr,

3
@trusktr, sceglierei il tipo decimale della tua piattaforma. In Java, quello è BigDecimal.
zneak,

13
@maaartinus ... e non pensi che usare il doppio per queste cose sia soggetto a errori? Ho visto che il problema dell'arrotondamento del galleggiante ha colpito duramente i sistemi reali . Anche nel settore bancario. Per favore, non raccomandarlo, o se lo fai, forniscilo come una risposta separata (in modo che possiamo sottovalutarlo: P)
eis

75

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.


2
Correlati, interessanti: Nella mia console js di Chrome: Math.round (.4999999999999999): 0 Math.round (.4999999999999999999): 1
Curtis Yallop

16
Questa risposta è fuorviante. 1,40 * 165 = 231. Qualsiasi numero diverso da esattamente 231 è sbagliato in senso matematico (e tutti gli altri sensi).
Karu,

2
@Karu Penso che sia per questo che Randy dice che i float sono cattivi ... La mia console Chrome JS mostra 230.99999999999997 come risultato. Questo è sbagliato, che è il punto sollevato nella risposta.
trusktr,

6
@Karu: Imho la risposta non è matematicamente sbagliata. È solo che ci sono 2 domande a cui viene data una risposta che non è la domanda che viene posta. La domanda alla quale il tuo compilatore risponde è 1.39999999 * 164.99999999 e così via, che matematicamente corretta è 230.99999 .... Ovviamente non è la domanda che è stata posta in primo luogo ....
markus

2
@CurtisYallop perché il valore doppio si chiude a 0,49999999999999999 è 0,5 Perché Math.round(0.49999999999999994)restituisce 1?
phuclv,

53

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

16
QUESTO!!!! Stavo cercando tutte le risposte per trovare questo FATTO RILEVANTE !!! Nei calcoli normali a nessuno importa se si è di una frazione di un centesimo, ma qui con numeri alti facilmente si perdono alcuni dollari per transazione!
Falco,

22
E ora immagina che qualcuno ottenga entrate giornaliere dello 0,01% con i suoi 1 milione di dollari - non otterrebbe nulla ogni giorno - e dopo un anno non ha ottenuto 1000 dollari, QUESTO MIRÀ
Falco,

6
Il problema non è la precisione ma quel float non ti dice che diventa inaccurato. Un numero intero può contenere solo fino a 10 cifre che un float può contenere fino a 6 senza diventare inaccurato (quando lo si taglia di conseguenza). Lo consente mentre un numero intero ottiene un overflow e un linguaggio come Java ti avviserà o non lo consentirà. Quando si utilizza un doppio, è possibile andare fino a 16 cifre, il che è sufficiente per molti casi d'uso.
sigi,

39

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.


5
float, doubleE BigDecimalsono rappresentano precisi valori. La conversione da codice a oggetto non è esatta così come altre operazioni. I tipi stessi non sono inesatti.
chux - Ripristina Monica il

1
@chux: rileggendolo, penso che tu abbia ragione di migliorare la mia formulazione. Lo modificherò e riscriverò.
Nathan Hughes,

28

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.


3
Questa è in realtà una risposta abbastanza decente. Nella maggior parte dei casi è perfettamente corretto usarli.
Vahid Amiri,

2
Va notato che la maggior parte delle banche di investimento usa il doppio come la maggior parte dei programmi C ++. Alcuni usano a lungo ma quindi ha il suo problema di tracciabilità della scala.
Peter Lawrey,

20

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;
    }
}

15

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

3
Proviamo a qualcosa di diverso dall'addizione / sottrazione banale e dal mutplicaiton intero, se il codice calcolasse il tasso mensile di un prestito del 7%, entrambi i tipi non dovrebbero fornire un valore esatto e devono essere arrotondati allo 0,01 più vicino. L'arrotondamento all'unità monetaria più bassa fa parte dei calcoli monetari, l'uso di tipi decimali evita tale necessità con addizione / sottrazione - ma non molto altro.
chux - Ripristina Monica il

@ chux-ReinstateMonica: se si suppone che l'interesse si componga mensilmente, calcolare l'interesse ogni mese sommando il saldo giornaliero, moltiplicandolo per 7 (il tasso di interesse) e dividere, arrotondando al centesimo più vicino, per il numero di giorni in l'anno. Nessun arrotondamento ovunque tranne una volta al mese nell'ultimo passaggio.
supercat,

@supercat Il mio commento enfatizza l'uso di un FP binario dell'unità monetaria più piccola o di un FP decimale entrambi presentano problemi di arrotondamento simili, come nel tuo commento con "e dividi, arrotondando al penny più vicino". L'uso di un FP 2 base o 10 non fornisce alcun vantaggio in entrambi i casi.
chux - Ripristina Monica

@ chux-ReinstateMonica: nello scenario di cui sopra, se la matematica risolve che l'interesse dovrebbe essere esattamente uguale a un certo numero di mezzo centesimo, un programma finanziario corretto deve arrotondare in modo precisamente specificato. Se i calcoli in virgola mobile generano un valore di interesse, ad esempio $ 1,23499941, ma il valore matematicamente preciso prima dell'arrotondamento avrebbe dovuto essere di $ 1,235 e l'arrotondamento è specificato come "anche il più vicino", l'uso di tali calcoli in virgola mobile non causerà il risultato a essere fuori da $ 0,000059, ma piuttosto da $ 0,01, che ai fini contabili è semplicemente sbagliato.
supercat,

@supercat L'uso del doubleFP 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.
chux - Ripristina Monica

11

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:

  1. Un'API per la gestione, ad esempio, di importi e valute monetarie
  2. API per supportare implementazioni intercambiabili
  3. Fabbriche per la creazione di istanze delle classi di implementazione
  4. Funzionalità per calcoli, conversione e formattazione di importi monetari
  5. API Java per lavorare con Money and Currencies, che dovrebbe essere inclusa in Java 9.
  6. 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


5

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 .


4

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.


2

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.


1
Per quanto riguarda SciPy / Numpy, la precisione fissa (ovvero il decimale.Decimale di Python) non è supportata ( docs.scipy.org/doc/numpy-dev/user/basics.types.html ). Alcune funzioni non funzionano correttamente con Decimal (ad esempio isnan). Pandas si basa su Numpy ed è stato avviato presso AQR, un importante hedge fund quantitativo. Quindi hai la tua risposta per quanto riguarda i calcoli finanziari (non la contabilità della drogheria).
Comte

2

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


0

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!!!


3
Il problema non è che si verifichi un errore di arrotondamento, ma che non ci si occupa. Arrotonda il risultato a due cifre decimali (se vuoi centesimi) e il gioco è fatto.
maaartino

0

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:

  1. Usa numeri interi lunghi e conta invece in centesimi.

  2. 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.

  3. 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.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.