web-dev-qa-db-fra.com

Meilleure façon de stocker des valeurs monétaires en C++

Je sais qu’un float n’est pas approprié pour stocker des valeurs monétaires à cause d’erreurs d’arrondi. Existe-t-il un moyen standard de représenter l'argent en C++? 

J'ai regardé dans la bibliothèque de boost et rien trouvé à ce sujet. En Java, il semble que BigInteger soit la solution, mais je n’ai pas trouvé d’équivalent en C++. Je pourrais écrire ma propre classe monétaire, mais préférer ne pas le faire si quelque chose est testé.

53
Javier Ramos

Ne le stockez pas en tant que centimes, car vous accumulerez des erreurs en multipliant les taxes et les intérêts assez rapidement. Au minimum, gardez deux chiffres significatifs supplémentaires: 12,45 USD seraient stockés sous 124 500. Si vous le conservez dans un entier signé de 32 bits, vous disposerez de 200 000 dollars (positif ou négatif). Si vous avez besoin de plus grands nombres ou de plus de précision, un entier signé de 64 bits vous laissera probablement tout l’espace dont vous aurez besoin pendant longtemps.

Il peut être utile d’envelopper cette valeur dans une classe, de vous donner un emplacement pour la création de ces valeurs, leur calcul arithmétique et leur mise en forme pour affichage. Cela vous donnerait également un emplacement central pour choisir la devise de stockage (USD, CAD, EURO, etc.).

30
Eclipse

Ayant traité cela dans les systèmes financiers actuels, je peux vous dire que vous souhaiterez probablement utiliser un nombre avec au moins 6 décimales de précision (en USD). Espérons que puisque vous parlez de valeurs monétaires, vous ne vous échapperez pas ici. Il y a des propositions pour ajouter des types décimaux au C++, mais je n'en connais pas encore.

Le meilleur type de C++ natif à utiliser ici serait long double.

Le problème avec les autres approches qui utilisent simplement un int est que vous devez stocker plus que vos centimes. Les transactions financières sont souvent multipliées par des valeurs non entières, ce qui risque de vous poser problème, car 100,25 USD convertis en 10025 * 0.000123523 (par exemple, APR) posent des problèmes. Vous finirez par vous retrouver en terre flottante et les conversions vont vous coûter cher.

Maintenant, le problème ne se produit pas dans la plupart des situations simples. Je vais vous donner un exemple précis:

Pour plusieurs milliers de valeurs monétaires, si vous multipliez chacune par un pourcentage puis que vous les additionnez, vous obtiendrez un nombre différent de celui obtenu si vous aviez multiplié le total par ce pourcentage si vous ne gardiez pas suffisamment de décimales. Cela peut fonctionner dans certaines situations, mais vous gagnerez souvent plusieurs centimes assez rapidement. Dans mon expérience générale, je veille à conserver une précision allant jusqu'à 6 décimales (en veillant à ce que la précision restante soit disponible pour la partie entière du nombre). 

Comprenez également que le type de fichier que vous stockez importe peu si vous faites les mathématiques de façon moins précise. Si vos calculs sont effectués en mode simple précision, peu importe si vous les stockez en double précision. Votre précision sera correcte au calcul le moins précis.


Cela dit, si vous ne faites pas de calcul autre qu'une simple addition ou soustraction et que vous enregistrez le nombre, tout ira bien, mais dès que quelque chose de plus complexe apparaîtra, vous aurez des problèmes.

21
Orion Adrian
20
jblocksom

Le plus gros problème est d'arrondir lui-même!

19% de 42,50 € = 8.075 €. En raison des règles allemandes en matière d'arrondissement, ce montant est de 8,08 €. Le problème est que (au moins sur ma machine) 8 075 ne peuvent pas être représentés par un double. Même si je modifie la variable dans le débogueur à cette valeur, je me retrouve avec 8,0749999 ....

Et c’est là que ma fonction d’arrondi (et toute autre logique sur la logique à virgule flottante à laquelle je peux penser) échoue, car elle produit 8,07 €. Le chiffre significatif est 4 et la valeur est arrondie. Et cela est tout à fait faux et vous ne pouvez rien y faire à moins d’éviter autant que possible d’utiliser des valeurs en virgule flottante.

Cela fonctionne très bien si vous représentez 42,50 € en Integer 42500000.

42500000 * 19/100 = 8075000. Vous pouvez maintenant appliquer la règle d'arrondi au-dessus de 8080000. Cela peut facilement être converti en une valeur monétaire pour des raisons d'affichage. 8,08 €.

Mais je mettrais toujours ça dans une classe.

10
user331471

Je suggérerais que vous gardiez une variable pour le nombre de cents au lieu de dollars. Cela devrait éliminer les erreurs d'arrondi. L'affichage au format dollars/cents des normes devrait être un sujet de préoccupation.

8
Rontologist

Quel que soit le type que vous choisissiez, je vous conseillerais de le placer dans un "typedef" afin que vous puissiez le changer à un moment différent.

5
Jordan Parmer

Vous pouvez essayer le type de données décimal:

https://github.com/vpiotr/decimal_for_cpp

Conçu pour stocker des valeurs axées sur l'argent (solde monétaire, taux de change, taux d'intérêt), précision définie par l'utilisateur Jusqu'à 19 chiffres.

C'est une solution en-tête uniquement pour C++.

5
Piotr

Connaissez VOTRE plage de données.

Un float n’est bon que pour 6 à 7 chiffres de précision, ce qui signifie un maximum d’environ + -9999.99 sans arrondir. C'est inutile pour la plupart des applications financières. 

Un double est bon pour 13 chiffres, ainsi: + -99 999 999 999,99, soyez toujours prudent lorsque vous utilisez de grands nombres. Reconnaître que la soustraction de deux résultats similaires enlève une grande partie de la précision (voir un livre sur l'analyse numérique pour les problèmes potentiels). 

Le nombre entier 32 bits est bon à + -2 Milliards (une graduation en centimes laissera tomber 2 décimales)

Le nombre entier 64 bits manipulera tout l'argent, mais encore une fois, soyez prudent lors de la conversion et de la multiplication par divers taux de votre application pouvant être des flottants/doubles.

La clé est de comprendre votre domaine de problème. Quelles exigences juridiques avez-vous pour la précision? Comment allez-vous afficher les valeurs? À quelle fréquence la conversion aura-t-elle lieu? Avez-vous besoin d'internationalisation? Assurez-vous de pouvoir répondre à ces questions avant de prendre votre décision.

5
Dan Hewett

Cela dépend des besoins de votre entreprise en matière d'arrondissement. Le moyen le plus sûr consiste à stocker un nombre entier avec la précision requise et à savoir quand et comment appliquer l'arrondi.

4
Douglas Mayle

Vous dites que vous avez regardé dans la bibliothèque de boost sans rien trouver à ce sujet . Mais vous avez là multiprecision/cpp_dec_float qui dit:

La base de ce type est 10. En conséquence, il peut se comporter légèrement différemment des types en base 2.

Ainsi, si vous utilisez déjà Boost, les valeurs et les opérations en devise devraient être valables, avec un nombre de base de 10 et une précision de 50 ou 100 chiffres (beaucoup).

Voir:

#include <iostream>
#include <iomanip>
#include <boost/multiprecision/cpp_dec_float.hpp>

int main()
{
    float bogus = 1.0 / 3.0;
    boost::multiprecision::cpp_dec_float_50 correct = 1.0 / 3.0;

    std::cout << std::setprecision(16) << std::fixed 
              << "float: " << bogus << std::endl
              << "cpp_dec_float: " << correct << std::endl;

    return 0;
}

Sortie:

float: 0.3333333432674408

cpp_dec_float: 0.3333333333333333

* Je ne dis pas que float (base 2) est mauvais et décimal (base 10) est bon. Ils se comportent simplement différemment ...

** Je sais que ceci est un ancien post et boost :: multiprecision a été introduit en 2013, je voulais donc le remarquer ici.

3
Fernando

Entiers, toujours - stockez-le sous forme de centimes (ou quelle que soit la devise dans laquelle vous programmez.) il en virgule flottante. Arrondir à la dernière minute n’est pas la solution car les calculs en monnaie réelle sont arrondis au fur et à mesure.

Vous ne pouvez pas éviter le problème en modifiant l'ordre des opérations. Cela échoue si vous avez un pourcentage qui vous laisse sans représentation binaire appropriée. Les comptables vont paniquer si vous êtes par un seul centime.

2
Loren Pechtel

Je recommanderais d'utiliser un long int pour stocker la devise dans la plus petite valeur (par exemple, la monnaie américaine serait de centimes), si une devise basée sur une décimale est utilisée.

Très important: assurez-vous de nommer toutes vos valeurs monétaires en fonction de leur contenu. (Exemple: account_balance_cents) Ceci évitera beaucoup de problèmes en bout de ligne.

(Les pourcentages sont un autre exemple où cela apparaît. Ne nommez jamais une valeur "XXX_percent" quand elle contient en fait un rapport non multiplié par 100).

1
Jeffrey L Whitledge

La solution est simple: stockez à la précision requise, sous forme d’entier décalé. Mais lors de la lecture, convertissez-le en double flottant, de sorte que les calculs subissent moins d’erreurs d’arrondi. Puis, lorsque vous stockez dans la base de données, multipliez-le à l'exactitude requis, mais avant de le tronquer sous la forme d'un entier, ajoutez +/- 1/10 pour compenser les erreurs de troncature, ou +/- 51/100 pour arrondir .

1
Laurie

continuez et écrivez votre propre argent ( http://junit.sourceforge.net/doc/testinfected/testing.htm ) ou la classe currency () (selon vos besoins). et le tester.

0
Ray Tayek

Notre institution financière utilise "double". Puisque nous sommes un magasin "à revenu fixe", nous avons beaucoup d'algorithmes compliqués qui utilisent le double de toute façon. L'astuce consiste à vous assurer que votre présentation d'utilisateur final ne dépasse pas la précision de double. Par exemple, lorsque nous avons une liste de transactions totalisant des milliards de dollars, nous devons nous assurer de ne pas imprimer de déchets en raison de problèmes d'arrondissement.

0
Arkadiy

J'utilisais signé long pour 32 bits et signé long pour 64 bits. Cela vous donnera une capacité de stockage maximale pour la quantité sous-jacente elle-même. Je développerais ensuite deux manipulateurs personnalisés. Celle qui convertit cette quantité en fonction des taux de change et celle qui la formate dans la devise de votre choix. Vous pouvez développer davantage de manipulateurs pour diverses opérations/règles financières.

0
user2074102

La bibliothèque GMP a des implémentations "bignum" que vous pouvez utiliser pour les calculs de nombres entiers de taille arbitraire nécessaires au traitement de l'argent. Voir la documentation de mpz_class(attention: ceci est terriblement incomplet, une gamme complète d'opérateurs arithmétiques est fournie).

0
Greg Rogers

Une option consiste à stocker 10,01 $ sous la forme 1001 et à effectuer tous les calculs en cents, en divisant par 100D lorsque vous affichez les valeurs.

Ou bien, utilisez des flotteurs, et arrondissez seulement au dernier moment possible.

Les problèmes peuvent souvent être atténués en modifiant l’ordre des opérations.

Au lieu de la valeur * .10 pour un rabais de 10%, utilisez (valeur * 10)/100, ce qui aidera considérablement. (rappelez-vous .1 est un binaire répétitif)

0
Chris Cudmore

Stockez le montant en dollars et en cents sous forme de deux entiers distincts.

0
kmiklas