web-dev-qa-db-fra.com

Pourquoi ne pas utiliser Double ou Float pour représenter la devise?

On m'a toujours dit jamais de représenter l'argent avec double ou float types, et cette fois je vous pose la question: pourquoi?

Je suis sûr qu'il y a une très bonne raison, je ne sais tout simplement pas ce que c'est.

867
Fran Fitzpatrick

Parce que les flottants et les doubles ne peuvent pas représenter avec précision les multiples de base 10 que nous utilisons pour de l'argent. Ce problème ne concerne pas uniquement Java, il concerne tout langage de programmation utilisant des types à virgule flottante de base 2.

En base 10, vous pouvez écrire 10.25 comme 1025 * 10-2 (un entier fois une puissance de 10). Les nombres à virgule flottante IEEE-754 sont différents, mais une façon très simple d'y penser est de les multiplier par une puissance de deux. Par exemple, vous pourriez regarder 164 * 2-4 (un entier multiplié par une puissance de deux), ce qui est également égal à 10,25. Ce n'est pas ainsi que les nombres sont représentés en mémoire, mais les implications mathématiques sont les mêmes.

Même en base 10, cette notation ne peut pas représenter avec précision les fractions les plus simples. Par exemple, vous ne pouvez pas représenter 1/3: la représentation décimale se répète (0,3333 ...), il n'y a donc aucun entier fini que vous pouvez multiplier par une puissance de 10 pour obtenir 1/3. Vous pourriez choisir une longue séquence de 3 et un petit exposant, comme 333333333 * 10-dix, mais ce n’est pas exact: si vous multipliez cela par 3, vous n’obtiendrez pas 1.

Toutefois, pour compter l'argent, du moins pour les pays dont la valeur est comprise dans un ordre de grandeur du dollar américain, il suffit généralement de pouvoir stocker des multiples de 10.-2donc peu importe qu'un tiers ne puisse être représenté.

Le problème des flottants et des doubles est que la grande majorité des nombres de type argent n’a pas une représentation exacte sous la forme d’un nombre entier multiplié par une puissance de 2. En fait, les seuls multiples de 0,01 compris entre 0 et 1 (qui sont significatifs lorsqu'il s'agit d'argent, car ils sont exprimés en cents entiers) pouvant être représentés exactement comme un nombre à virgule flottante IEEE-754 sont 0, 0,25, 0,5, 0,75. et 1. Tous les autres sont dérangés par une petite quantité. Par analogie avec l'exemple 0.333333, si vous prenez la valeur en virgule flottante de 0,1 et que vous la multipliez par 10, vous n'obtiendrez pas 1.

Représenter de l’argent sous la forme double ou float paraîtra bien au début car le logiciel élimine les petites erreurs, mais à mesure que vous effectuez des additions, des soustractions, des multiplications et des divisions sur des nombres inexacts, les erreurs ne feront que s'aggraver et vous vous retrouverez avec des valeurs sont visiblement pas précis. Cela rend les flotteurs et les doublons inadéquats pour traiter de l'argent, où une précision parfaite est requise pour des multiples de puissances de base 10.

Une solution qui fonctionne dans à peu près n'importe quelle langue consiste à utiliser des entiers et à compter des centimes. Par exemple, 1025 équivaudrait à 10,25 dollars. Plusieurs langues ont également des types intégrés pour gérer l'argent. Entre autres, Java a la classe BigDecimal et C # a le type decimal .

906
zneak

De Bloch, J., Effective Java, 2e éd, point 48:

Les types float et double conviennent particulièrement mal aux calculs monétaires car il est impossible de représenter 0,1 (ou toute autre puissance négative de dix) en tant que float ou double exactement.

Par exemple, supposons que vous avez 1,03 $ et que vous dépensez 42 cents. Combien d'argent vous reste-t-il?

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

imprime 0.6100000000000001.

La bonne façon de résoudre ce problème consiste à utiliser BigDecimal, int ou long pour les calculs monétaires.

Bien que BigDecimal présente certaines réserves (veuillez vous reporter à la réponse actuellement acceptée).

293
dogbane

Ce n’est pas une question de précision, ni de précision. Il s’agit de répondre aux attentes des humains qui utilisent la base 10 pour les calculs au lieu de la base 2. Par exemple, l’utilisation de doubles pour les calculs financiers ne donne pas des réponses "fausses" au sens mathématique, mais peut produire des réponses qui sont: pas ce qui est attendu au sens financier.

Même si vous arrondissez vos résultats à la dernière minute avant la sortie, vous pouvez parfois obtenir un résultat en utilisant des doublons qui ne correspondent pas aux attentes.

En utilisant une calculatrice ou en calculant les résultats à la main, 1.40 * 165 = 231 exactement. Cependant, en utilisant des doublons en interne sur mon compilateur/environnement d’exploitation, il est stocké sous forme de nombre binaire proche de 230,99999 ... donc si vous tronquez ce nombre, vous obtenez 230 au lieu de 231. Vous pouvez en déduire que l’arrondi au lieu de la troncature ont donné le résultat souhaité de 231. Cela est vrai, mais l’arrondi implique toujours une troncature. Quelle que soit la technique d'arrondi que vous utilisiez, il existe toujours des conditions aux limites comme celle-ci qui s'arrondiront quand vous vous attendez à ce qu'elles arrivent. Ils sont assez rares pour ne pas être retrouvés lors de tests ou d'observations occasionnels. Vous devrez peut-être écrire du code pour rechercher des exemples illustrant des résultats qui ne se comportent pas comme prévu.

Supposons que vous vouliez arrondir quelque chose au cent le plus proche. Donc, vous prenez votre résultat final, multipliez par 100, ajoutez 0,5, tronquez, puis divisez le résultat par 100 pour revenir à quelques centimes. Si le numéro interne que vous avez enregistré était 3.46499999 .... au lieu de 3.465, vous obtiendrez 3,46 au lieu de 3,47 lorsque vous arrondissez le nombre au cent le plus proche. Mais vos calculs en base 10 ont peut-être indiqué que la réponse devrait être 3,465 exactement, ce qui devrait clairement arrondir à 3,47, pas à 3,46. Ce genre de choses se produit parfois dans la vie réelle lorsque vous utilisez des doubles pour les calculs financiers. C'est rare, alors ça passe souvent inaperçu, mais ça arrive.

Si vous utilisez la base 10 pour vos calculs internes au lieu de doubler, les réponses correspondent toujours exactement à ce que les humains attendent, en supposant qu'aucun autre bogue dans votre code.

69
Randy D Oxentenko

Je suis troublé par certaines de ces réponses. Je pense que les doubles et les floats ont leur place dans les calculs financiers. Bien sûr, lors de l’addition et de la soustraction de montants monétaires non fractionnaires, il n’y aura aucune perte de précision lorsqu’on utilise des classes entières ou des classes BigDecimal. Mais lorsque vous effectuez des opérations plus complexes, vous obtenez souvent des résultats avec plusieurs décimales, quelle que soit la manière dont vous stockez les nombres. La question est de savoir comment vous présentez le résultat.

Si votre résultat se situe à la limite entre arrondi supérieur et inférieur et que le dernier centime compte vraiment, vous devriez probablement dire au téléspectateur que la réponse est presque au centre - en affichant davantage de décimales.

Le problème des doubles, et plus encore des flottants, vient du fait qu’ils sont utilisés pour combiner des nombres grands et petits. En Java,

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

résulte en

1.1875
47
Rob Scala

Les flotteurs et les doubles sont approximatifs. Si vous créez un BigDecimal et passez un float au constructeur, vous voyez ce que le float est réellement égal à:

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

ce n'est probablement pas comme vous voulez représenter 1,01 $.

Le problème est que la spécification IEEE ne permet pas de représenter exactement toutes les fractions. Certaines finissent par répéter des fractions, ce qui entraîne des erreurs d’approximation. Étant donné que les comptables aiment que les choses déboursent exactement le sou, et que les clients soient agacés s’ils paient leur facture et que, une fois le paiement traité, ils doivent .01 et qu’ils se voient facturer des frais ou ne puissent pas fermer leur compte, il est préférable d’utiliser types exacts tels que decimal (en C #) ou Java.math.BigDecimal en Java.

Ce n'est pas que l'erreur ne soit pas contrôlable si vous arrondissez: voir cet article de Peter Lawrey . Il est simplement plus facile de ne pas avoir à arrondir en premier lieu. La plupart des applications qui gèrent de l'argent ne nécessitent pas beaucoup de calculs, les opérations consistent à ajouter des éléments ou à allouer des montants à différents compartiments. L'introduction de la virgule flottante et de l'arrondi ne fait que compliquer les choses.

36
Nathan Hughes

Je vais risquer d'être rétrogradé, mais je pense que l'inadéquation des nombres en virgule flottante pour les calculs de devise est surestimée. Tant que vous vous assurez de bien arrondir les centimes et de disposer de suffisamment de chiffres significatifs pour contrer l'inadéquation représentation binaire-décimale expliquée par zneak, il n'y aura aucun problème.

Les personnes calculant avec une devise dans Excel ont toujours utilisé des flottants à double précision (il n’existe aucun type de devise dans Excel) et je n’ai encore vu personne se plaindre d’erreurs d’arrondi.

Bien sûr, vous devez rester dans les limites de la raison; par exemple. un simple magasin en ligne n’aurait probablement jamais de problème avec les flottants à double précision, mais si vous le faites par ex. comptabilité ou toute autre chose nécessitant l’ajout d’un grand nombre de chiffres (sans restriction), vous ne voudriez pas toucher les nombres à virgule flottante avec un poteau de dix pieds.

22
escitalopram

S'il est vrai que le type à virgule flottante ne peut représenter que des données approximativement décimales, il est également vrai que si l'on arrondit les nombres à la précision nécessaire avant de les présenter, on obtient le résultat correct. Habituellement.

Habituellement parce que le type double a une précision inférieure à 16 chiffres. Si vous avez besoin d'une meilleure précision, ce n'est pas un type approprié. Des approximations peuvent également s'accumuler.

Il faut dire que même si vous utilisez une arithmétique en virgule fixe, vous devez toujours arrondir les nombres, ne serait-ce que BigInteger et BigDecimal donnent des erreurs si vous obtenez des nombres décimaux périodiques. Donc, il y a une approximation aussi ici.

Par exemple, COBOL, utilisé historiquement pour les calculs financiers, a une précision maximale de 18 chiffres. Il y a donc souvent un arrondi implicite.

En conclusion, à mon avis, le double ne convient pas principalement pour sa précision à 16 chiffres, ce qui peut être insuffisant, non pas parce qu’il est approximatif.

Considérez le résultat suivant du programme suivant. Il montre qu'après avoir arrondi double, donner le même résultat que BigDecimal jusqu'à la précision 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;
    }
}
18
user1593165

Le résultat du nombre en virgule flottante n'est pas exact, ce qui le rend impropre à tout calcul financier nécessitant un résultat exact et non approximatif. float et double sont conçus pour des calculs techniques et scientifiques et ne donnent souvent pas le résultat exact. Le résultat du calcul en virgule flottante peut également varier d'une machine virtuelle à l'autre. Regardez ci-dessous l'exemple de BigDecimal et de la primitive double qui est utilisé pour représenter la valeur monétaire. Il est clair que le calcul en virgule flottante peut ne pas être exact et qu'il convient d'utiliser BigDecimal pour les calculs financiers.

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

Sortie:

difference between 2.0 and 1.1 using double is: 0.8999999999999999
difference between 2.0 and 1.1 using BigDecimal is: 0.9
15
Dheeraj Arora

Comme indiqué précédemment, "Représenter de l'argent sous forme de double ou de float sera probablement intéressant au premier abord, car le logiciel élimine les petites erreurs, mais à mesure que vous effectuez des additions, des soustractions, des multiplications et des divisions sur des nombres inexacts, vous perdrez de plus en plus de précision. au fur et à mesure que les erreurs s’additionnent. Cela rend les flottants et les doubles inadéquats pour traiter de l’argent, où une précision parfaite est requise pour des multiples de puissances de base 10. "

Enfin Java dispose d'un moyen standard de travailler avec Currency And Money!

JSR 354: API Money and Currency

JSR 354 fournit une API permettant de représenter, de transporter et d'effectuer des calculs complets avec Money et Currency. Vous pouvez le télécharger à partir de ce lien:

JSR 354: Téléchargement de l’API monétaire et monétaire

La spécification comprend les éléments suivants:

  1. Une API pour gérer e. g. montants et devises
  2. API pour prendre en charge des implémentations interchangeables
  3. Fabriques pour créer des instances des classes d'implémentation
  4. Fonctionnalité pour les calculs, la conversion et la mise en forme des montants monétaires
  5. API Java permettant de travailler avec Money and Currency, qui devrait être incluse dans Java 9.
  6. Toutes les classes et interfaces de spécification sont situées dans le package javax.money. *.

Exemples de réalisation de JSR 354: API Money and Currency:

Voici un exemple de création d’un MonetaryAmount et de son impression sur la console:

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

Lorsque vous utilisez l'API d'implémentation de référence, le code nécessaire est beaucoup plus simple:

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

L'API prend également en charge les calculs avec MonetaryAmounts:

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

CurrencyUnit et MonetaryAmount

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

MonetaryAmount dispose de plusieurs méthodes permettant d’accéder à la devise affectée, au montant numérique, à sa précision, etc.:

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;

MonetaryAmounts peut être arrondi à l'aide d'un opérateur d'arrondi:

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

Lorsque vous travaillez avec des collections de MonetaryAmounts, certaines méthodes utilitaires de Nice pour le filtrage, le tri et le regroupement sont disponibles.

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

Opérations personnalisées 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

Ressources:

Traitement de l'argent et des devises dans Java avec JSR 354

En regardant dans la Java 9 API Money and Currency (JSR 354)

Voir aussi: JSR 354 - Monnaie et monnaie

7
Virtual

Si votre calcul implique différentes étapes, l'arithmétique en précision arbitraire ne vous couvrira pas à 100%.

Le seul moyen fiable d’utiliser une représentation parfaite des résultats (utilisez un type de données Fraction personnalisé qui effectuera des opérations de division par lots jusqu’à la dernière étape) et de ne convertir que la dernière étape en notation décimale.

La précision arbitraire n'aidera pas, car il peut toujours y avoir des nombres comportant autant de décimales, ou des résultats tels que 0.6666666 ... Aucune représentation arbitraire ne couvrira le dernier exemple. Donc, vous aurez de petites erreurs à chaque étape.

Ces erreurs s’additionneront et risquent de ne plus être faciles à ignorer. Ceci s'appelle propagation d'erreur .

4
Kemal Dağ

Je préfère utiliser Integer ou Long pour représenter la devise. BigDecimal réduit trop le code source.

Vous devez juste savoir que toutes vos valeurs sont en cents. Ou la valeur la plus basse de la devise que vous utilisez.

2
Tony Ennis

La plupart des réponses ont mis en évidence les raisons pour lesquelles on ne devrait pas utiliser les doubles pour les calculs en monnaie et en argent. Et je suis totalement d'accord avec eux.

Cela ne signifie pas pour autant que les doubles ne puissent jamais être utilisés à cette fin.

J'ai travaillé sur un certain nombre de projets avec des exigences très faibles en gc, et le fait d'avoir des objets BigDecimal a largement contribué à cette surcharge.

C'est le manque de compréhension de la double représentation et le manque d'expérience dans la gestion de l'exactitude et de la précision qui sont à l'origine de cette sage suggestion.

Vous pouvez le faire fonctionner si vous êtes capable de gérer les exigences de précision et d'exactitude de votre projet, ce qui doit être fait en fonction de la plage de valeurs double utilisée.

Vous pouvez vous référer à la méthode FuzzyCompare de guava pour avoir plus d'idées. Le paramètre de tolérance est la clé. Nous avons traité ce problème pour une application de négociation de titres et nous avons effectué une recherche exhaustive sur les tolérances à utiliser pour différentes valeurs numériques dans différentes plages.

En outre, il peut arriver que vous soyez tenté d'utiliser Double wrappers en tant que clé de mappe, avec une carte de hachage comme implémentation. C'est très risqué parce que Double.equals et le code de hachage, par exemple, avec les valeurs "0.5" et "0.6 - 0.1" vont causer de gros dégâts.

2
Dev Amitabh

De nombreuses réponses à cette question traitent de l'IEEE et des normes relatives à l'arithmétique en virgule flottante.

Venant d’une formation non informatique (physique et ingénierie), j’ai tendance à regarder les problèmes sous un angle différent. Pour moi, la raison pour laquelle je n’utiliserais pas un double ou un float dans un calcul mathématique est que je perdrais trop d’informations.

Quelles sont les alternatives? Il y en a beaucoup (et beaucoup d'autres dont je ne suis pas au courant!).

BigDecimal dans Java est natif du langage Java. Apfloat est une autre bibliothèque de précision arbitraire pour Java.

Le type de données décimal en C # est l'alternative .NET de Microsoft pour 28 chiffres significatifs.

SciPy (Scientific Python) peut probablement aussi gérer des calculs financiers (je n'ai pas essayé, mais je pense bien que oui).

La bibliothèque GMP (Multiple Precision Library) GNU et la bibliothèque GNU MFPR sont deux ressources libres et à code source ouvert pour C et C++.

Il existe également des bibliothèques de précision numérique pour JavaScript (!) Et je pense que PHP peut gérer des calculs financiers.

Il existe également des solutions propriétaires (en particulier, je pense, pour Fortran) et des solutions à source ouverte, ainsi que pour de nombreux langages informatiques.

Je ne suis pas un informaticien de formation. Cependant, j'ai tendance à pencher vers BigDecimal dans Java ou décimal dans C #. Je n'ai pas essayé les autres solutions que j'ai énumérées, mais elles sont probablement très bien aussi.

Pour moi, j'aime BigDecimal à cause des méthodes qu'il supporte. La décimale de C # est très belle, mais je n’ai pas eu la chance de la travailler aussi souvent que je le voudrais. Je fais des calculs scientifiques qui m'intéressent pendant mon temps libre, et BigDecimal semble très bien fonctionner, car je peux régler la précision de mes nombres en virgule flottante. L'inconvénient de BigDecimal? Cela peut parfois être lent, surtout si vous utilisez la méthode de division.

Pour plus de rapidité, vous pouvez consulter les bibliothèques libres et propriétaires en C, C++ et Fortran.

1
fishermanhat

Pour ajouter aux réponses précédentes, il est également possible d'implémenter Joda-Money en Java, en plus de BigDecimal, pour traiter le problème traité dans la question. Java nom du module est org.joda.money.

Il nécessite Java SE 8 ou une version ultérieure et n'a aucune dépendance.

Pour être plus précis, il existe une dépendance à la compilation, mais elle n’est pas obligatoire.

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

Exemples d'utilisation de 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();

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

Exemples d'implémentation: https://www.programcreek.com/Java-api-examples/?api=org.joda.money.Money

1
Tadija Malić

Quelques exemples ... cela fonctionne (en réalité, ne fonctionne pas comme prévu), sur presque tous les langages de programmation ... J'ai essayé avec Delphi, VBScript, Visual Basic, JavaScript et maintenant avec 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!!!");
    }

SORTIE:

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

0
WilliamK