floating-point - nombre - valeur max float




Pourquoi ne pas utiliser Double ou Float pour représenter la monnaie? (10)

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.

On m'a toujours dit de ne jamais représenter l'argent avec double types double ou float , 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.


Comme nous l'avons dit plus haut, "Représenter l'argent comme un double ou un flotteur sera probablement bon au premier abord, car le logiciel arrondit les petites erreurs, mais comme vous effectuez plus d'additions, soustractions, multiplications et divisions sur des nombres inexacts, vous perdrez de plus en plus de précision. Les erreurs s'accumulent, ce qui rend les flotteurs et les doubles inadéquats pour faire face à l'argent, ce qui nécessite une précision parfaite pour les multiples de 10 puissances de base. "

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 depuis ce lien:

JSR 354: Money and Currency API Télécharger

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

  1. API pour la gestion des montants monétaires et des devises
  2. API pour prendre en charge des implémentations interchangeables
  3. Les 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. Java API pour travailler avec Money et Currencies, qui est prévu pour être inclus dans Java 9.
  6. Toutes les classes de spécifications et interfaces se trouvent dans le package javax.money. *.

Exemples d'exemples d'API JSR 354: 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 a diverses méthodes qui permettent d'accéder à la devise affectée, la quantité numérique, sa précision et plus:

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 utiles pour le filtrage, le tri et le groupement 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:

Gérer l'argent et les devises en Java avec JSR 354

À la recherche de l'API Java 9 Money and Currency (JSR 354)

Voir aussi: JSR 354 - Monnaie et argent


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

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 simple boutique en ligne ne rencontrerait probablement aucun problème avec les flotteurs double précision, mais si vous faites par exemple la comptabilité ou toute autre chose qui nécessite une grande quantité (sans restriction) de nombres, vous ne voudriez pas toucher les nombres flottants avec un dix pieds pôle.


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

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 tournez : voir cet article de 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.


Quelques exemples ... cela fonctionne (en fait ne fonctionne pas comme prévu), sur presque n'importe quel langage 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!!!


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

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

Les types float et double sont 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 float ou un double exactement.

Par exemple, supposons que vous avez 1,03 $ et que vous dépensez 42c. Combien d'argent avez-vous encore?

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

0.6100000000000001 .

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


Most answers have highlighted the reasons why one should not use doubles for money and currency calculations. And I totally agree with them.

It doesn't mean though that doubles can never be used for that purpose.

I have worked on a number of projects with very low gc requirements, and having BigDecimal objects was a big contributor to that overhead.

It's the lack of understanding about double representation and lack of experience in handling the accuracy and precision that brings about this wise suggestion.

You can make it work if you are able to handle the precision and accuracy requirements of your project, which has to be done based on what range of double values is one dealing with.

You can refer to guava's FuzzyCompare method to get more idea. The parameter tolerance is the key. We dealt with this problem for a securities trading application and we did an exhaustive research on what tolerances to use for different numerical values in different ranges.

En outre, il peut y avoir des situations où vous êtes tenté d'utiliser Double wrappers comme clé de carte avec la carte de hachage étant l'implémentation. C'est très risqué parce que Double.equals et le code de hachage par exemple les valeurs "0.5" et "0.6 - 0.1" causeront un gros désordre.





currency