45 votes

Un exemple réaliste où l’utilisation de BigDecimal pour la monnaie est strictement préférable à l’utilisation de double

Nous savons que l'utilisation d' double de la monnaie est sujette à des erreurs et n'est pas recommandé. Cependant, je suis encore à voir un réaliste exemple, où l' BigDecimal travaille en double échoue et ne peut pas être simplement fixé par certaines arrondissement.


Notez que trivial problèmes

double total = 0.0;
for (int i = 0; i < 10; i++) total += 0.1;
for (int i = 0; i < 10; i++) total -= 0.1;
assertTrue(total == 0.0);

ne comptez pas qu'ils sont trivialement résolu par arrondissement (dans cet exemple quelque chose à partir de zéro à seize virgule de places en faire).


Calculs impliquant la sommation des valeurs peut avoir besoin de certaines intermédiaire rouding, mais étant donné que le total de la monnaie en circulation en cours de USD 1e12, Java double (c'est à dire, la norme IEEE double précision) avec ses 15 chiffres décimaux est encore suffisamment de cas pour cents.


Les calculs impliquant la division sont, en général, imprécis, même avec BigDecimal. Je peux construire un calcul qui ne peut pas être effectuée avec doubles, mais peut être réalisée avec BigDecimal à l'aide d'une échelle de 100, mais ce n'est pas quelque chose que vous pouvez rencontrer dans la réalité.


Je ne prétends pas qu'une telle réalistes exemple n'existe pas, c'est juste que je n'ai pas encore vu.

J'ai aussi certainement d'accord que l'utilisation d' double est plus enclin à l'erreur.

Exemple

Ce que je suis à la recherche d'une méthode comme suit (en fonction de la réponse de Roland Illig)

/** 
  * Given an input which has three decimal places,
  * round it to two decimal places using HALF_EVEN.
*/
BigDecimal roundToTwoPlaces(BigDecimal n) {
    // To make sure, that the input has three decimal places.
    checkArgument(n.scale() == 3);
    return n.round(new MathContext(2, RoundingMode.HALF_EVEN));
}

avec un test comme

public void testRoundToTwoPlaces() {
    final BigDecimal n = new BigDecimal("0.615");
    final BigDecimal expected = new BigDecimal("0.62");
    final BigDecimal actual = roundToTwoPlaces(n);
    Assert.assertEquals(expected, actual);
}

Quand cela se naïvement réécrite à l'aide d' double, le test peut échouer (il n'a pas, pour l'entrée, mais il le fait pour les autres). Cependant, il peut être fait correctement:

static double roundToTwoPlaces(double n) {
    final long m = Math.round(1000.0 * n);
    final double x = 0.1 * m;
    final long r = (long) Math.rint(x);
    return r / 100.0;
}

C'est moche et sujettes à l'erreur (et peut probablement être simplifiée), mais il peut facilement être encapsulé quelque part. C'est pourquoi je suis à la recherche pour plus de réponses.

30voto

BeeOnRope Points 3617

Je peux voir quatre méthodes de base que double pouvez-vis de vous lorsque vous traitez avec des calculs de devise.

Mantisse Trop Petit

Avec ~15 décimales de précision dans la mantisse, vous allez-vous pour obtenir un mauvais résultat chaque fois que vous infligez avec des montants plus que cela. Si vous êtes suivi des cents, des problèmes commencent à se produire avant 1013 (dix milliards de dollars) de dollars.

Alors que c'est un grand nombre, ce n'est pas que les grandes. Le PIB des états-unis d'environ 18 milliards, qui la dépasse, donc tout ce que traiter avec des pays ou de la même société de taille moyenne pourraient facilement obtenir une réponse incorrecte.

En outre, il ya beaucoup de façons que des quantités beaucoup plus petites pourraient dépasser ce seuil lors du calcul. Vous pourriez être en train de faire une projection de croissance ou un sur un certain nombre d'années, ce qui donne une grande valeur finale. Vous pourriez être en train de faire un "what if" des scénarios d'analyse où les différents paramètres possibles sont examinés et une combinaison des paramètres peut entraîner de très grandes valeurs. Vous travaillez peut-être dans les règles qui permettent à une fraction d'un cent qui pourrait hacher l'autre de deux ordres de grandeur ou plus hors de votre portée, vous mettre à peu près en ligne avec la richesse de simples individus en USD.

Enfin, ne prenons pas l'un de NOUS centrée sur de voir les choses. Quels sont les autres monnaies? La Roupie Indonésienne est vaut à peu près de 13 000 USD, ce qui est un autre de 2 ordres de grandeur vous avez besoin pour suivre les montants en devises dans la monnaie (en supposant qu'aucun "grain de sel"!). Vous êtes presque passer pour des montants qui sont d'intérêt pour le commun des mortels.

Voici un exemple où une prévision de croissance de calcul à partir d'1e9 à 5% ne va pas:

method   year                         amount           delta
double      0             $ 1,000,000,000.00
Decimal     0             $ 1,000,000,000.00  (0.0000000000)
double     10             $ 1,628,894,626.78
Decimal    10             $ 1,628,894,626.78  (0.0000004768)
double     20             $ 2,653,297,705.14
Decimal    20             $ 2,653,297,705.14  (0.0000023842)
double     30             $ 4,321,942,375.15
Decimal    30             $ 4,321,942,375.15  (0.0000057220)
double     40             $ 7,039,988,712.12
Decimal    40             $ 7,039,988,712.12  (0.0000123978)
double     50            $ 11,467,399,785.75
Decimal    50            $ 11,467,399,785.75  (0.0000247955)
double     60            $ 18,679,185,894.12
Decimal    60            $ 18,679,185,894.12  (0.0000534058)
double     70            $ 30,426,425,535.51
Decimal    70            $ 30,426,425,535.51  (0.0000915527)
double     80            $ 49,561,441,066.84
Decimal    80            $ 49,561,441,066.84  (0.0001678467)
double     90            $ 80,730,365,049.13
Decimal    90            $ 80,730,365,049.13  (0.0003051758)
double    100           $ 131,501,257,846.30
Decimal   100           $ 131,501,257,846.30  (0.0005645752)
double    110           $ 214,201,692,320.32
Decimal   110           $ 214,201,692,320.32  (0.0010375977)
double    120           $ 348,911,985,667.20
Decimal   120           $ 348,911,985,667.20  (0.0017700195)
double    130           $ 568,340,858,671.56
Decimal   130           $ 568,340,858,671.55  (0.0030517578)
double    140           $ 925,767,370,868.17
Decimal   140           $ 925,767,370,868.17  (0.0053710938)
double    150         $ 1,507,977,496,053.05
Decimal   150         $ 1,507,977,496,053.04  (0.0097656250)
double    160         $ 2,456,336,440,622.11
Decimal   160         $ 2,456,336,440,622.10  (0.0166015625)
double    170         $ 4,001,113,229,686.99
Decimal   170         $ 4,001,113,229,686.96  (0.0288085938)
double    180         $ 6,517,391,840,965.27
Decimal   180         $ 6,517,391,840,965.22  (0.0498046875)
double    190        $ 10,616,144,550,351.47
Decimal   190        $ 10,616,144,550,351.38  (0.0859375000)

Le delta (différence entre double et BigDecimal frappe d'abord > 1 cent à l'an 160, autour de 2 milliards de dollars (ce qui pourrait ne pas être tout ce que beaucoup plus de 160 ans à partir de maintenant), et bien sûr ne cesse de s'aggraver.

Bien sûr, l'53 bits de Mantisse dire que la relative erreur de ce type de calcul est probablement très faible (j'espère que vous ne perdez pas votre emploi de plus de 1 cent à 2 milliards de dollars). En effet, l'erreur relative fondamentalement détient relativement stable dans la plupart de l'exemple. Vous pourriez certainement l'organiser si de sorte que vous (par exemple) soustraire deux différents avec une perte de précision dans la mantisse résultant en une arbitrairement grand d'erreur (exercice jusqu'au lecteur).

Évolution Sémantique

Si vous pensez que vous êtes assez intelligent, et réussi à venir avec un arrondi régime qui vous permet d'utiliser double et les ont testés de manière exhaustive vos méthodes sur votre JVM. Aller de l'avant et de le déployer. Demain ou la semaine prochaine ou à chaque fois qu'il y a de pire pour vous, les résultats de la modifier et de vos astuces pause.

Contrairement à presque tous les autres la langue de base de l'expression, et certainement à la différence de nombre entier ou BigDecimal de l'arithmétique, par défaut, les résultats de nombre à virgule flottante expressions n'ont pas une seule des normes définies valeur en raison de l' strictfp fonctionnalité. Les plates-formes sont libres d'utiliser, à leur discrétion, une plus grande précision des intermédiaires, ce qui peut entraîner des résultats différents sur un matériel différent, JVM versions, etc. Le résultat, pour les mêmes entrées, peut même varier au moment de l'exécution lorsque la méthode des substitutions à partir interprété de JIT-compilé!

Si vous aviez écrit votre code dans le pré-Java 1.2 jours, vous seriez assez énervé lorsque Java 1.2 soudainement introduit la variable par défaut FP comportement. Vous pourriez être tenté d'utiliser strictfp partout et j'espère que vous n'avez pas dans la multitude des bugs en rapport - mais sur certaines plates-formes, vous seriez de jeter une grande partie de la performance qui double acheté chez vous en premier lieu.

Il n'y a rien de dire que la JVM spec ne vais pas encore changer à l'avenir, pour accueillir de nouvelles modifications de la FP matériel, ou que la JVM des réalisateurs de ne pas utiliser la corde que le défaut de non-strictfp comportement donne à faire quelque chose de délicat.

Inexact Représentations

Comme Roland l'a souligné dans sa réponse, l'un des principaux problème avec double , c'est qu'il n'est pas exact des représentations pour certains la plupart des non-valeurs entières. Bien qu'un seul non-valeur exacte comme 0.1 souvent "aller-retour" OK, dans certains scénarios (par exemple, Double.toString(0.1).equals("0.1")), dès que vous faire des maths sur ces imprécis valeurs de l'erreur peut être composé, et cela peut être irrémédiable.

En particulier, si vous êtes "fermer" pour un arrondissement point, par exemple, ~1.005, vous pourriez obtenir une valeur de 1.00499999... lorsque la valeur réelle est 1.0050000001..., ou vice-versa. Parce que les erreurs vont dans les deux directions, il n'y a pas d'arrondi magie qui peut résoudre ce problème. Il n'y a aucun moyen de savoir si une valeur de 1.004999999... devrait être augmenté ou pas. Votre roundToTwoPlaces() méthode (un type de double arrondi) ne fonctionne que parce qu'il a traité un cas où 1.0049999 devrait être augmenté, mais il ne sera jamais en mesure de traverser la frontière, par exemple, si les erreurs cumulatives cause 1.0050000000001 pour être transformé en 1.00499999999999 il ne peut pas le réparer.

Vous n'avez pas besoin d'un grand ou un petit nombre de frapper cette. Vous avez seulement besoin de mathématiques, et pour que le résultat de l'automne à proximité de la frontière. Plus de maths que vous faites, plus les déviations possibles de la réalité du résultat, et le plus de chance de cheval sur une frontière.

Comme demandé ici d'une recherche de test qui fait un simple calcul: amount * tax et arrondi à 2 décimales (c'est à dire, en dollars et en cents). Il existe quelques méthodes d'arrondi dans les il y, celui qui est actuellement utilisé, roundToTwoPlacesB est un surgonflée version de la vôtre1 (par augmentation du multiplicateur n dans le premier arrondissement vous faites, il est beaucoup plus sensible à la version originale ne parvient pas tout de suite sur trivial entrées).

Le test crache les échecs qu'il trouve, et ils viennent dans des bouquets. Par exemple, les premiers échecs:

Failed for 1234.57 * 0.5000 = 617.28 vs 617.29
Raw result : 617.2850000000000000000000, Double.toString(): 617.29
Failed for 1234.61 * 0.5000 = 617.30 vs 617.31
Raw result : 617.3050000000000000000000, Double.toString(): 617.31
Failed for 1234.65 * 0.5000 = 617.32 vs 617.33
Raw result : 617.3250000000000000000000, Double.toString(): 617.33
Failed for 1234.69 * 0.5000 = 617.34 vs 617.35
Raw result : 617.3450000000000000000000, Double.toString(): 617.35

Notez que le "résultat brut" (c'est à dire, l'exact résultat non arrondi) est toujours à proximité d'un x.xx5000 limite. Votre méthode d'arrondi se trompe à la fois sur le haut et les bas côtés. Vous ne pouvez pas réparer de façon générique.

Imprécis Calculs

Plusieurs de l' java.lang.Math méthodes ne nécessitent pas correctement arrondie résultats, mais plutôt de permettre aux erreurs de jusqu'à 2,5 ulp. Certes, vous n'allez probablement pas être en utilisant les fonctions hyperboliques beaucoup avec de la monnaie, mais des fonctions telles que exp() et pow() souvent à trouver leur chemin dans la monnaie de calculs et de ces seuls ont une précision de 1 ulp. Si le numéro est déjà "mal" lorsqu'il est retourné.

Cette interagit avec le "Inexact Représentation" de l'émission, puisque ce type d'erreur est beaucoup plus grave que celle de la normale mathématique des opérations qui sont à moins de choisir la meilleure valeur possible à partir du représentable domaine de l' double. Cela signifie que vous pouvez avoir beaucoup plus d'aller-boundary crossing événements lorsque vous utilisez ces méthodes.

21voto

Roland Illig Points 15357

Lorsque vous arrondissez double price = 0.615 à deux décimales, vous obtenez 0,61 (arrondi au bas), mais vous attendez probablement 0,62 (arrondi au plus, en raison des 5).

En effet, le double 0,615 correspond en réalité à 0,614999999999999999911112258029987476766109466552734375.

11voto

Henry Points 14061

Les principaux problèmes auxquels vous êtes confrontés dans la pratique, sont liées au fait que, round(a) + round(b) n'est pas nécessairement égal à round(a+b). En utilisant BigDecimal vous avez un contrôle plus fin de la l'arrondissement de processus et peut donc faire votre sommes en sortir correctement.

Lorsque vous calculez les impôts, dire de la TVA de 18%, il est facile d'obtenir des valeurs qui ont plus de deux décimales quand exactement. Donc, l'arrondissement devient un problème.

Supposons que vous achetez 2 articles pour un montant de 1,3 chaque

Article  Price  Price+VAT (exact)  Price+VAT (rounded)
A        1.3    1.534              1.53
B        1.3    1.534              1.53
sum      2.6    3.068              3.06
exact rounded   3.07

Donc, si vous faites les calculs avec lit double et seulement autour d'imprimer le résultat, vous obtenez un total de 3.07 tandis que le montant sur le projet de loi devrait en fait être 3.06.

10voto

GhostCat Points 83269

Donnons un "moins technique, plus philosophique" réponse ici: pourquoi pensez-vous que "Cobol" n'est pas en arithmétique à virgule flottante lorsque vous traitez avec de la monnaie?!

("Cobol" entre guillemets, comme dans: les approches traditionnelles pour résoudre de vrais problèmes d'affaires).

Sens: près de 50 ans, lorsque les gens ont commencé à utiliser des ordinateurs pour les affaires aka financière de travail, ils se sont rapidement rendu compte que la "virgule flottante" la représentation n'est pas d'aller travailler pour l'industrie financière (peut-être attendre quelques rares niche coins comme l'a souligné dans la question).

Et garder à l'esprit: a l'époque, les abstractions ont été vraiment cher!!! C'était assez cher pour avoir un peu ici et un registre; et encore il est rapidement devenu évident pour les géants sur les épaules desquels nous sommes ... que l'utilisation de "floating points" ne serait pas à résoudre leurs problèmes; et qu'ils ont dû compter sur quelque chose d'autre, plus abstraite, plus chers!

Notre industrie a+ de 50 ans à venir avec "virgule flottante qui fonctionne pour devise" - et la commune de la réponse est toujours: ne pas le faire. Au lieu de cela, vous vous tournez vers des solutions telles que BigDecimal.

7voto

EJP Points 113412

Vous n'avez pas besoin d'un exemple. Vous avez juste besoin de quatrième formulaire de mathématiques. Les Fractions de calcul en virgule flottante sont représentés en binaire radix et binaire radix est incommensurable avec décimale radix. En dixième année de trucs.

Donc il y aura toujours de l'arrondi et de rapprochement, et il n'est ni acceptable, en comptabilité, en aucune manière, la forme ou la forme. Les livres ont pour équilibrer au dernier cent, et ainsi de FYI ne une succursale de la banque à la fin de chaque jour, et l'ensemble de la banque à intervalles réguliers.

une expression de souffrance à partir des erreurs d'arrondi ne compte pas'

Ridicule. C' est le problème. À l'exclusion des erreurs d'arrondi exclut de l'ensemble du problème.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X