On m'a toujours dit de ne jamais représenter l'argent avec double
ou les 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.
On m'a toujours dit de ne jamais représenter l'argent avec double
ou les 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.
Réponses:
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 double
ou float
semblera 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 BigDecimal
classe et C # a le decimal
type.
1.0 / 10 * 10
n'est peut-être pas la même chose que 1.0.
De Bloch, J., Effective Java, 2e éd., Article 48:
Les types
float
etdouble
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 unfloat
oudouble
exactement.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
,int
oulong
pour des calculs monétaires.
Bien qu'il BigDecimal
ait quelques mises en garde (veuillez consulter la réponse actuellement acceptée).
long a = 104
et comptez en cents au lieu de dollars.
BigDecimal
.
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.
Math.round(0.49999999999999994)
retourne 1?
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
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.
float
, double
Et BigDecimal
sont 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.
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.
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;
}
}
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
double
FP 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.
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:
- Une API pour gérer par exemple les montants monétaires et les devises
- API pour prendre en charge les implémentations interchangeables
- Usines de création d'instances des classes d'implémentation
- Fonctionnalité pour les calculs, la conversion et le formatage des montants monétaires
- API Java pour travailler avec Money and Currencies, qui devrait être incluse dans Java 9.
- 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
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 .
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.
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.
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
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!!!
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:
Utilisez plutôt un entier long et comptez en cents.
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.
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.