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

939

On m'a toujours dit de ne jamais représenter l'argent avec doubleou les floattypes, 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.

Fran Fitzpatrick
la source
4
Voir cette question SO: Arrondir les erreurs?
Jeff Ogata
80
Juste pour être clair, ils ne devraient pas être utilisés pour tout ce qui nécessite de la précision - pas seulement de la monnaie.
Jeff
152
Ils ne doivent pas être utilisés pour tout ce qui nécessite de l' exactitude . Mais les 53 bits significatifs du double (~ 16 chiffres décimaux) sont généralement assez bons pour les choses qui nécessitent simplement une précision .
dan04
21
@jeff Votre commentaire déforme complètement à quoi servent les virgules flottantes binaires et à quoi elles ne servent pas. Lisez la réponse de zneak ci-dessous et veuillez supprimer votre commentaire trompeur.
Pascal Cuoq

Réponses:

985

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

Dans la base 10, vous pouvez écrire 10,25 sous la forme 1025 * 10 -2 (un entier multiplié par une puissance de 10). Les nombres à virgule flottante IEEE-754 sont différents, mais une façon très simple de penser à eux est de multiplier par une puissance de deux à la place. Par exemple, vous pourriez regarder 164 * 2 -4 (un entier multiplié par une puissance de deux), 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 la plupart des fractions 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 pas d'entier fini que vous pouvez multiplier par une puissance de 10 pour obtenir 1/3. Vous pouvez vous contenter d'une longue séquence de 3 et d'un petit exposant, comme 333333333 * 10 -10 , mais ce n'est pas exact: si vous multipliez cela par 3, vous n'obtiendrez pas 1.

Cependant, dans le but de compter l'argent, au moins pour les pays dont l'argent est évalué dans un ordre de grandeur du dollar américain, tout ce dont vous avez besoin est généralement de pouvoir stocker des multiples de 10 -2 , donc cela n'a pas vraiment d'importance que 1/3 ne peut pas être représenté.

Le problème avec les flottants et les doubles est que la grande majorité des nombres de type monétaire n'ont pas une représentation exacte sous forme d'entier multipliée par une puissance de 2. En fait, les seuls multiples de 0,01 entre 0 et 1 (qui sont significatifs lors de la négociation) avec de l'argent parce qu'ils sont des cents entiers) qui peuvent être représentés exactement comme un nombre binaire à virgule flottante IEEE-754 sont 0, 0,25, 0,5, 0,75 et 1. Tous les autres sont légèrement décalés. Par analogie avec l'exemple 0.333333, si vous prenez la valeur à virgule flottante pour 0,1 et que vous la multipliez par 10, vous n'obtiendrez pas 1.

Représenter l'argent comme un doubleou floatsemblera probablement bon au premier abord à mesure que le logiciel arrondit les petites erreurs, mais à mesure que vous effectuez plus d'additions, de soustractions, de multiplications et de divisions sur des nombres inexacts, les erreurs s'accumuleront et vous vous retrouverez avec des valeurs qui sont visiblement pas précis. Cela rend les flotteurs et les doubles inadéquats pour faire face à l'argent, où une précision parfaite pour les multiples des pouvoirs de base 10 est requise.

Une solution qui fonctionne dans à peu près n'importe quel langage consiste à utiliser des entiers à la place et à compter les cents. Par exemple, 1025 serait 10,25 $. Plusieurs langues ont également des types intégrés pour gérer l'argent. Entre autres, Java a la BigDecimalclasse et C # a le decimaltype.

zneak
la source
3
@Fran Vous obtiendrez des erreurs d'arrondi et, dans certains cas, lorsque de grandes quantités de devises sont utilisées, les calculs de taux d'intérêt peuvent être considérablement
réduits
5
... la plupart des fractions de base 10, c'est-à-dire. Par exemple, 0,1 n'a pas de représentation binaire flottante exacte. Donc, ce 1.0 / 10 * 10n'est peut-être pas la même chose que 1.0.
Chris Jester-Young,
6
@ linuxuser27 Je pense que Fran essayait d'être drôle. Quoi qu'il en soit, la réponse de zneak est la meilleure que j'ai vue, même meilleure que la version classique de Bloch.
Isaac Rabinovitch,
5
Bien sûr, si vous connaissez la précision, vous pouvez toujours arrondir le résultat et ainsi éviter tout le problème. C'est beaucoup plus rapide et plus simple que d'utiliser BigDecimal. Une autre alternative consiste à utiliser une précision fixe int ou longue.
Peter Lawrey
2
@zneak, vous savez que les nombres rationnels sont un sous - ensemble des nombres réels, non? Les nombres réels IEEE-754 sont des nombres réels. Il se trouve qu'ils sont également rationnels.
Tim Seguine
314

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

Les types floatet doublesont particulièrement mal adaptés aux calculs monétaires car il est impossible de représenter 0,1 (ou toute autre puissance négative de dix) comme un floatou doubleexactement.

Par exemple, supposons que vous avez 1,03 $ et que vous dépensez 42c. 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 est d'utiliser BigDecimal, intou long pour des calculs monétaires.

Bien qu'il BigDecimalait quelques mises en garde (veuillez consulter la réponse actuellement acceptée).

dogbane
la source
6
Je suis un peu confus par la recommandation d'utiliser int ou long pour les calculs monétaires. Comment représentez-vous 1.03 en tant qu'int ou long? J'ai essayé "long a = 1.04;" et "long a = 104/100;" en vain.
Peter
49
@Peter, vous utilisez long a = 104et comptez en cents au lieu de dollars.
zneak
@zneak Qu'en est-il quand un pourcentage doit être appliqué comme l'intérêt composé ou similaire?
trusktr
3
@trusktr, j'irais avec le type décimal de votre plate-forme. En Java, c'est BigDecimal.
zneak
13
@maaartinus ... et vous ne pensez pas que l'utilisation du double pour de telles choses est sujette à erreur? Je l' ai vu la question de l' arrondissement flottant a frappé les systèmes réels dur . Même dans le secteur bancaire. S'il vous plaît ne le recommande pas, ou si vous le faites, prévoir que , une réponse distincte (afin que nous puissions downvote il: P)
eis
75

Ce n'est pas une question d'exactitude, ni une question 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 produit pas de réponses «fausses» au sens mathématique, mais elle peut produire des réponses qui sont pas ce qui est attendu sur le plan 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 en interne des doubles, sur mon environnement de compilateur / système d'exploitation, il est stocké sous la forme d'un nombre binaire proche de 230,99999 ... donc si vous tronquez le nombre, vous obtenez 230 au lieu de 231. Vous pouvez penser que l'arrondi au lieu de la tronquer serait ont donné le résultat souhaité de 231. C'est vrai, mais l'arrondi implique toujours la troncature. Quelle que soit la technique d'arrondi que vous utilisez, il existe toujours des conditions aux limites comme celle-ci qui s'arrondiront lorsque vous vous attendez à ce qu'elle arrondisse. Ils sont suffisamment rares pour qu'ils ne soient souvent pas trouvés par des tests ou des observations occasionnels. Vous devrez peut-être écrire du code pour rechercher des exemples qui illustrent des résultats qui ne se comportent pas comme prévu.

Supposons que vous souhaitiez arrondir quelque chose au centime 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 centime le plus proche. Mais vos calculs de base 10 peuvent avoir indiqué que la réponse devrait être 3,465 exactement, ce qui devrait clairement arrondir à 3,47, et non à 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, donc ça passe souvent inaperçu comme problème, mais ça arrive.

Si vous utilisez la base 10 pour vos calculs internes au lieu de doublons, les réponses sont toujours exactement celles attendues par les humains, en supposant qu'il n'y a pas d'autres bogues dans votre code.

Randy D Oxentenko
la source
2
Connexes, intéressantes: dans ma console chrome js: Math.round (.4999999999999999): 0 Math.round (.4999999999999999999): 1
Curtis Yallop
16
Cette réponse est trompeuse. 1,40 * 165 = 231. Tout nombre autre qu'exactement 231 est faux dans un sens mathématique (et tous les autres sens).
Karu
2
@Karu Je pense que c'est pourquoi Randy dit que les flotteurs sont mauvais ... Ma console Chrome JS affiche 230.99999999999997 comme résultat. C'est faux, ce qui est le point fait dans la réponse.
trusktr
6
@Karu: À mon humble avis, la réponse n'est pas mathématiquement fausse. C'est juste qu'il y a 2 questions auxquelles on répond, ce qui n'est pas la question posée. La question à laquelle votre compilateur répond est 1.39999999 * 164.99999999 et ainsi de suite, ce qui est mathématiquement correct égal à 230.99999 .... Évidemment, ce n'est pas la question qui a été posée en premier lieu ...
Markus
2
@CurtisYallop parce que la valeur double ferme à 0.49999999999999999 est 0.5 Pourquoi Math.round(0.49999999999999994)retourne 1?
phuclv
53

Je suis troublé par certaines de ces réponses. Je pense que les doubles et les flottants ont une place dans les calculs financiers. Certes, lors de l'ajout et de la soustraction de montants monétaires non fractionnaires, il n'y aura aucune perte de précision lors de l'utilisation de classes entières ou de classes BigDecimal. Mais lorsque vous effectuez des opérations plus complexes, vous vous retrouvez souvent avec des résultats qui dépassent plusieurs décimales, quelle que soit la façon dont vous stockez les nombres. La question est de savoir comment vous présentez le résultat.

Si votre résultat est à la limite entre l'arrondi et l'arrondi, et que le dernier centime compte vraiment, vous devriez probablement dire au spectateur que la réponse est presque au milieu - en affichant plus de décimales.

Le problème avec les doubles, et plus encore avec les flotteurs, c'est quand ils sont utilisés pour combiner de grands nombres et de petits nombres. En java,

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

résulte en

1.1875
Rob Scala
la source
16
CETTE!!!! Je cherchais toutes les réponses pour trouver ce FAIT PERTINENT !!! Dans les calculs normaux, personne ne se soucie de quelque fraction de cent, mais ici, avec des chiffres élevés, des dollars se perdent facilement par transaction!
Falco
22
Et imaginez maintenant que quelqu'un gagne un revenu quotidien de 0,01% sur son 1 million de dollars - il n'obtiendrait rien chaque jour - et après un an, il n'a pas obtenu 1000 dollars, CECI COMPTERA
Falco
6
Le problème n'est pas la précision mais ce flotteur ne vous dit pas qu'il devient inexact. Un entier ne peut contenir que 10 chiffres, un flotteur peut en contenir jusqu'à 6 sans devenir inexact (lorsque vous le coupez en conséquence). Il permet cela alors qu'un entier obtient un débordement et un langage comme java vous avertira ou ne le permettra pas. Lorsque vous utilisez un double, vous pouvez aller jusqu'à 16 chiffres, ce qui est suffisant pour de nombreux cas d'utilisation.
sigi
39

Les flotteurs et les doubles sont approximatifs. Si vous créez un BigDecimal et passez un flottant dans le constructeur, vous voyez ce que le flottant équivaut réellement:

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

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

Le problème est que la spécification IEEE n'a pas de moyen de représenter exactement toutes les fractions, certaines finissent par se répéter, vous vous retrouvez donc avec des erreurs d'approximation. Étant donné que les comptables aiment que les choses sortent exactement au centime, et les clients seront ennuyés s'ils paient leur facture et après le paiement, ils doivent 0,01 et qu'ils sont facturés ou ne peuvent pas fermer leur compte, il est préférable d'utiliser types exacts comme décimal (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 . C'est juste plus facile de ne pas avoir à arrondir en premier lieu. La plupart des applications qui gèrent de l'argent n'appellent pas beaucoup de mathématiques, les opérations consistent à ajouter des choses ou à allouer des montants à différents compartiments. L'introduction de virgule flottante et d'arrondi ne fait que compliquer les choses.

Nathan Hughes
la source
5
float, doubleEt BigDecimalsont représentent exactement les valeurs. La conversion de code en objet est inexacte ainsi que d'autres opérations. Les types eux-mêmes ne sont pas inexacts.
chux
1
@chux: en relisant ceci, je pense que vous avez raison de dire que ma formulation pourrait être améliorée. Je vais modifier cela et reformuler.
Nathan Hughes
28

Je risquerai d'être rétrogradé, mais je pense que l'inadéquation des nombres à virgule flottante pour les calculs de devises est surfaite. Tant que vous vous assurez de faire correctement l'arrondi des centimes et d'avoir suffisamment de chiffres significatifs pour travailler afin de contrer le décalage de représentation décimale binaire expliqué par zneak, il n'y aura pas de problème.

Les personnes qui calculent avec des devises dans Excel ont toujours utilisé des flotteurs à double précision (il n'y a pas de type de devise dans Excel) et je n'ai encore vu personne se plaindre d'erreurs d'arrondi.

Bien sûr, vous devez rester raisonnable; Par exemple, une simple boutique en ligne ne connaîtrait probablement jamais de problème avec les flotteurs à double précision, mais si vous faites, par exemple, la comptabilité ou toute autre chose qui nécessite l'ajout d'un grand nombre (sans restriction) de nombres, vous ne voudriez pas toucher des nombres à virgule flottante avec dix pieds pôle.

escitalopram
la source
3
C'est en fait une réponse assez décente. Dans la plupart des cas, il est parfaitement possible de les utiliser.
Vahid Amiri
2
Il convient de noter que la plupart des banques d'investissement utilisent deux fois plus que la plupart des programmes C ++. Certains utilisent longtemps mais ont donc leur propre problème de suivi de l'échelle.
Peter Lawrey
20

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.

Généralement 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 l'arithmétique à virgule fixe, vous devez encore arrondir les nombres, sans le fait que BigInteger et BigDecimal donnent des erreurs si vous obtenez des nombres décimaux périodiques. Il y a donc aussi une approximation ici.

Par exemple, COBOL, historiquement utilisé 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 surtout pas pour sa précision à 16 chiffres, ce qui peut être insuffisant, non pas parce qu'il est approximatif.

Considérez la sortie suivante du programme suivant. Il montre qu'après l'arrondi double donne 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;
    }
}
user1593165
la source
15

Le résultat du nombre à virgule flottante n'est pas exact, ce qui les rend impropres à tout calcul financier qui nécessite un résultat exact et non une approximation. float et double sont conçus pour l'ingénierie et le calcul scientifique et plusieurs fois ne produisent pas de résultat exact. Le résultat du calcul en virgule flottante peut varier de JVM à JVM. Regardez l'exemple ci-dessous de BigDecimal et de la double primitive qui est utilisée pour représenter la valeur monétaire, il est tout à fait clair que le calcul en virgule flottante peut ne pas être exact et il faut 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)));

Production:

difference between 2.0 and 1.1 using double is: 0.8999999999999999
difference between 2.0 and 1.1 using BigDecimal is: 0.9
Dheeraj Arora
la source
3
Essayons autre chose que l'addition / soustraction triviale et la mutplicaiton entière.Si le code calculait le taux mensuel d'un prêt de 7%, les deux types n'auraient pas besoin de fournir une valeur exacte et devraient être arrondis au 0,01 le plus proche. L'arrondi à l'unité monétaire la plus basse fait partie des calculs monétaires.Utiliser des types décimaux évite ce besoin avec addition / soustraction - mais pas grand-chose d'autre.
chux
@ chux-ReinstateMonica: Si l'intérêt est censé être composé mensuellement, calculez l'intérêt chaque mois en additionnant le solde quotidien, multipliez celui-ci par 7 (le taux d'intérêt) et divisez, en arrondissant au centime le plus proche, par le nombre de jours l'année. Aucun arrondi, sauf une fois par mois à la toute dernière étape.
supercat
@supercat Mon commentaire met l'accent sur l'utilisation d'un FP binaire de la plus petite unité monétaire ou d'un FP décimal tous les deux entraînent des problèmes d'arrondi similaires - comme dans votre commentaire avec "et diviser, arrondir au centime le plus proche". L'utilisation d'un FP de base 2 ou de base 10 n'offre aucun avantage dans les deux cas dans votre scénario.
chux
@ chux-ReinstateMonica: Dans le scénario ci-dessus, si les calculs indiquent que l'intérêt doit être précisément égal à un certain nombre de demi-cents, un programme financier correct doit être arrondi de manière précisément spécifiée. Si les calculs en virgule flottante donnent une valeur d'intérêt de, par exemple, 1,23499941 $, mais que la valeur mathématiquement précise avant l'arrondi aurait dû être de 1,235 $ et que l'arrondi est spécifié comme "le plus proche pair", l'utilisation de ces calculs en virgule flottante ne fera pas apparaître le résultat être réduit de 0,000059 $, mais plutôt de 0,01 $, ce qui, aux fins de la comptabilité, est tout simplement faux.
supercat
@supercat L'utilisation de doubleFP binaire au cent n'aurait aucun problème à calculer au 0,5 cent, tout comme le FP décimal non plus. Si les calculs en virgule flottante donnent une valeur d'intérêt de, par exemple, 123,499941 ¢, que ce soit par FP binaire ou FP décimal, le problème de double arrondi est le même - aucun avantage de toute façon. Votre prémisse semble supposer que la valeur mathématiquement précise et le FP décimal sont les mêmes - ce que même FP décimal ne garantit pas. 0.5 / 7.0 * 7.0 est un problème pour les FP binaires et déicmaux. IAC, la plupart seront sans objet car je m'attends à ce que la prochaine version de C fournisse FP décimal.
chux
11

Comme dit précédemment "Représenter l'argent comme un double ou un flottant semblera probablement bon au début, car le logiciel arrondit les petites erreurs, mais à mesure que vous effectuez plus d'ajouts, de soustractions, de multiplications et de divisions sur des nombres inexacts, vous perdrez de plus en plus de précision au fur et à mesure que les erreurs s'accumulent. Cela rend les flotteurs et les doubles inadéquats pour faire face à l'argent, où une précision parfaite pour les multiples des pouvoirs de base 10 est requise. "

Enfin Java a une façon standard de travailler avec Currency And Money!

JSR 354: API Money and Currency

JSR 354 fournit une API pour représenter, transporter et 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 Money and Currency

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

  1. Une API pour gérer par exemple les montants monétaires et les devises
  2. API pour prendre en charge les implémentations interchangeables
  3. Usines de création d'instances des classes d'implémentation
  4. Fonctionnalité pour les calculs, la conversion et le formatage des montants monétaires
  5. API Java pour travailler avec Money and Currencies, qui devrait être incluse dans Java 9.
  6. Toutes les classes de spécification et interfaces se trouvent dans le package javax.money. *.

Exemples d'exemples de JSR 354: API Money and Currency:

Un exemple de création d'un MonetaryAmount et d'impression sur la console ressemble à ceci:

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 différentes méthodes qui permettent d'accéder à la devise attribuée, au montant numérique, à sa précision et plus encore:

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;

Les montants monétaires peuvent être arrondis à 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, de belles méthodes utilitaires 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 MonetaryAmount personnalisées

// 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:

Gestion de l'argent et des devises en Java avec JSR 354

Examen de l'API Java 9 Money and Currency (JSR 354)

Voir aussi: JSR 354 - Monnaie et argent

Affy
la source
5

Si votre calcul implique plusieurs étapes, l'arithmétique de 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 de fraction personnalisé qui effectuera des opérations de division par lots à la dernière étape) et de convertir uniquement en notation décimale à la dernière étape.

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

Ces erreurs s'ajouteront, et deviendront éventuellement difficiles à ignorer. C'est ce qu'on appelle la propagation des erreurs .

Kemal Dağ
la source
4

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

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

J'ai travaillé sur un certain nombre de projets avec des exigences gc très faibles, et avoir des objets BigDecimal a été un grand contributeur à 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 provoquent cette sage suggestion.

Vous pouvez le faire fonctionner si vous êtes en mesure 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 doubles dont il s'agit.

Vous pouvez vous référer à la méthode FuzzyCompare de la goyave pour avoir plus d'idée. La tolérance du paramètre 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 y avoir des situations où vous êtes tenté d'utiliser des wrappers doubles comme clé de carte avec une carte de hachage comme implémentation. C'est très risqué car Double.equals et le code de hachage, par exemple les valeurs "0.5" & "0.6 - 0.1" vont causer un gros gâchis.

Dev Amitabh
la source
2

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

Issu d'un milieu 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 de double ou de flottant 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 en 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 également gérer des calculs financiers (je n'ai pas essayé, mais je le soupçonne).

La bibliothèque GNU Multiple Precision (GMP) et la bibliothèque GNU MFPR sont deux ressources libres et open-source 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 les calculs financiers.

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

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

Pour moi, j'aime BigDecimal en raison des méthodes qu'il prend en charge. La décimale de C # est très agréable, mais je n'ai pas eu la chance de travailler avec elle autant 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 définir la précision de mes nombres à virgule flottante. L'inconvénient de BigDecimal? Cela peut parfois être lent, surtout si vous utilisez la méthode de division.

Vous pourriez, pour plus de rapidité, vous pencher sur les bibliothèques libres et propriétaires en C, C ++ et Fortran.

pêcheur
la source
1
Concernant SciPy / Numpy, la précision fixe (c'est-à-dire decimal.Decimal de Python) n'est pas prise en charge ( docs.scipy.org/doc/numpy-dev/user/basics.types.html ). Certaines fonctions ne fonctionneront pas correctement avec Decimal (isnan par exemple). Pandas est basé sur Numpy et a été lancé chez AQR, l'un des principaux hedge funds quantitatifs. Vous avez donc votre réponse concernant les calculs financiers (pas la comptabilité d'épicerie).
comte
2

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 abordé dans la question. Le nom du module Java est org.joda.money.

Il nécessite Java SE 8 ou version ultérieure et n'a pas de dépendances.

Pour être plus précis, il existe une dépendance au moment de la compilation mais elle n'est pas requise.

<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 de mise en œuvre: https://www.programcreek.com/java-api-examples/?api=org.joda.money.Money

Tadija Malić
la source
0

Un exemple ... cela fonctionne (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!!!");
    }

PRODUCTION:

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

WilliamK
la source
3
Le problème n'est pas qu'une erreur d'arrondi se produit, mais que vous ne la résolvez pas. Arrondissez le résultat à deux décimales (si vous voulez des cents) et vous avez terminé.
maaartinus
0

Float est une forme binaire de Decimal avec un design différent; ce sont deux choses différentes. Il y a peu d'erreurs entre deux types lors de leur conversion. En outre, float est conçu pour représenter un nombre infini de valeurs pour les scientifiques. Cela signifie qu'il est conçu pour perdre la précision à un nombre extrêmement petit et extrêmement grand avec ce nombre fixe d'octets. Le nombre décimal ne peut pas représenter un nombre infini de valeurs, il limite uniquement ce nombre de chiffres décimaux. Donc, Float et Decimal sont à des fins différentes.

Il existe plusieurs façons de gérer l'erreur pour la valeur monétaire:

  1. Utilisez plutôt un entier long et comptez en cents.

  2. Utilisez la double précision, gardez vos chiffres significatifs à 15 seulement pour que la décimale puisse être simulée avec précision. Arrondir avant de présenter les valeurs; Arrondissez souvent lors des calculs.

  3. Utilisez une bibliothèque décimale comme Java BigDecimal afin de ne pas avoir besoin d'utiliser le double pour simuler la décimale.

ps il est intéressant de savoir que la plupart des marques de calculatrices scientifiques portables fonctionnent en décimal au lieu de flotter. Donc, aucune erreur de conversion de flottement de plainte.

Chris Tsang
la source