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


On m'a toujours dit jamais 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.


763
2017-09-16 19:23


origine


Réponses:


Parce que les flotteurs 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 lié à Java, mais à 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 nombre entier fois une puissance de 10). Numéros à virgule flottante IEEE-754 sont différents, mais une façon très simple d'y penser est de multiplier par une puissance de deux à la place. Par exemple, vous pourriez regarder 164 * 2-4 (un nombre entier fois une puissance de deux), qui est également égal à 10.25. Ce n'est pas comme cela 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 est répétée (0.3333 ...), donc il n'y a pas d'entier fini que vous pouvez multiplier par une puissance de 10 pour obtenir 1/3. Vous pourriez vous installer sur 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.

Cependant, pour le calcul de l'argent, au moins pour les pays dont l'argent est évalué en fonction d'un ordre de grandeur du dollar américain, il suffit généralement de pouvoir stocker des multiples de 10-2, donc ça n'a pas vraiment d'importance que 1/3 ne puisse pas être représenté.

Le problème avec les flotteurs et les doubles est que le grande majorité des nombres semblables à de l'argent n'ont pas une représentation exacte comme un nombre entier fois une puissance de 2. En fait, les seuls multiples de 0.01 entre 0 et 1 (qui sont significatifs en traitant de l'argent parce qu'ils sont entiers cents) qui peuvent être représenté exactement comme un nombre à virgule flottante binaire IEEE-754 sont 0, 0.25, 0.5, 0.75 et 1. Tous les autres sont désactivés par une petite quantité. 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 en tant que double ou float sera probablement bien au début, car le logiciel complète les petites erreurs, mais comme vous effectuez plus d'additions, de soustractions, de multiplications et de divisions sur des nombres inexacts, les erreurs se compliqueront et vous obtiendrez des valeurs qui ne sont visiblement pas précises. Cela rend les flotteurs et les doubles inadéquats pour faire face à l'argent, où une précision parfaite pour les multiples des puissances de base 10 est requise.

Une solution qui fonctionne dans presque tous les langages consiste à utiliser des entiers à la place et à compter les cents. Par exemple, 1025 serait de 10,25 $. Plusieurs langues ont également des types intégrés pour faire face à l'argent. Entre autres, Java a le BigDecimal classe, et C # a le decimal type.


788
2017-09-16 19:26



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

le float et double les types sont   particulièrement mal adapté pour monétaire   calculs parce que c'est impossible   pour représenter 0,1 (ou tout autre   puissance négative de dix) en tant que float ou    double exactement.

Par exemple, supposons que vous avez 1,03 $   et vous dépensez 42c. Combien d'argent faire   tu as quitté?

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

imprime 0.6100000000000001.

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


270
2017-09-16 19:52



Ce n'est pas une question d'exactitude, 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 produit pas de réponses qui sont «fausses» dans un sens mathématique, mais peut produire des réponses pas ce qu'on attend d'un point de vue financier.

Même si vous arrondissez vos résultats à la dernière minute avant la sortie, vous pouvez toujours obtenir un résultat en utilisant des doubles 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 interne en utilisant des doubles, sur mon 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 pourriez raisonner plutôt que tronquer ont donné le résultat désiré de 231. C'est vrai, mais arrondir implique toujours la troncature. Quelle que soit la technique d'arrondi que vous utilisez, il y a toujours des conditions aux limites comme celle-ci qui s'arrêteront lorsque vous vous attendez à ce qu'elle soit arrondie. Ils sont suffisamment rares pour ne pas être détectés lors d'un test ou d'une observation 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 voulez 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 à pennies. Si le numéro interne que vous avez stocké é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 exactement 3.465, ce qui devrait clairement arrondir à 3.47, pas à 3.46. Ces types de choses se produisent parfois dans la vraie vie lorsque vous utilisez des doubles pour les calculs financiers. C'est rare, alors cela passe souvent inaperçu, mais cela arrive.

Si vous utilisez la base 10 pour vos calculs internes au lieu de doubles, les réponses sont toujours exactement ce qui est attendu par les humains, en supposant qu'aucun autre bogue dans votre code.


63
2017-09-12 15:11



Je suis troublé par certaines de ces réponses. Je pense que les doubles et les flotteurs ont leur place dans les calculs financiers. Certes, lors de l'addition et de la soustraction de montants monétaires non fractionnaires, il n'y aura pas de 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 obtenez souvent des résultats qui sortent plusieurs fois ou plusieurs fois, peu importe comment vous stockez les nombres. Le problème est de savoir comment vous présentez le résultat.

Si votre résultat est à la limite entre arrondir et arrondir à la baisse, 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 flottants, 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

40
2018-04-03 14:54



Les flotteurs et les doubles sont approximatifs. Si vous créez un BigDecimal et passez un float dans le 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 la façon dont vous voulez représenter 1,01 $.

Le problème est que la spécification IEEE n'a pas le moyen de représenter exactement toutes les fractions, certaines d'entre elles finissent par se répéter comme des fractions de sorte que vous vous retrouvez avec des erreurs d'approximation. Puisque les comptables aiment que les choses sortent exactement au sou, et les clients seront ennuyés s'ils paient leur facture et après que le paiement soit traité ils doivent .01 et ils obtiennent des frais ou ne peuvent pas fermer leur compte, il est préférable d'utiliser les types exacts comme décimal (en C #) ou java.math.BigDecimal en Java.

Ce n'est pas que l'erreur n'est pas contrôlable si vous arrondissez: voir cet article par Peter Lawrey. C'est juste plus facile de ne pas avoir à faire le tour en premier lieu. La plupart des applications qui traitent de l'argent ne nécessitent pas beaucoup de calculs, les opérations consistent à ajouter des choses ou à allouer des montants dans différents compartiments. L'introduction du virgule flottante et de l'arrondi ne fait que compliquer les choses.


35
2017-09-16 19:29



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 de nombreuses fois ne produit pas de résultat exact également le résultat du calcul en virgule flottante peut varier de JVM à JVM. Regardez ci-dessous l'exemple de BigDecimal et double primitive qui est utilisé pour représenter la valeur monétaire, il est assez clair que le calcul à virgule flottante peut ne pas être exact et on devrait 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
2017-08-11 20:18



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 l'arithmétique en virgule fixe, vous devez toujours arrondir les nombres, si BigInteger et BigDecimal ne donnent pas d'erreurs si vous obtenez des nombres décimaux périodiques. Donc, il y a 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 arrondissement implicite.

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

Considérez la sortie suivante du programme suivant. Il montre qu'après avoir arrondi le 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;
    }
}

14
2017-10-26 19:03



Je vais risquer d'être downvoted, mais je pense que l'inadéquation des nombres à virgule flottante pour les calculs de devise est surestimée. Tant que vous vous assurez que vous faites le cent-arrondi correctement et avez assez de chiffres significatifs pour travailler avec afin de contrer la discordance de représentation binaire-décimale expliquée par zneak, il n'y aura pas de problème.

Les personnes qui calculent avec des devises dans Excel ont toujours utilisé des flottants à 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 dans la raison; par exemple. une boutique en ligne simple ne rencontrera probablement aucun problème avec les flotteurs à double précision, mais si vous le faites par ex. comptabilité ou toute autre chose qui nécessite d'ajouter une grande quantité (sans restriction) de nombres, vous ne voudriez pas toucher les nombres à virgule flottante avec un poteau de dix pieds.


10
2018-01-20 11:56