4
votes

Calculs financiers: doubles ou décimaux?

Nous travaillons avec des calculs financiers. J'ai trouvé cet article sur le stockage des valeurs monétaires sous forme de décimales: decimal vs double! - Lequel dois-je utiliser et quand?

Donc, je stocke le montant sous forme de décimales.

J'ai le calcul suivant: 12.000 * (1/12) = 1.000 p >

Si j'utilise un type de données décimal pour stocker le montant et le montant du résultat, je n'obtiens pas le résultat attendu

// First approach:    
decimal ratio = 1m / 12m;
decimal amount = 12000;
decimal ratioAmount = amount * ratio;
ratioAmount = 999.9999999999999

// Second approach:
double ratio = 1d / 12d;
decimal amount = 12000;
decimal ratioAmount = (decimal)((double)amount * ratio);
ratioAmount = 1.000

// Third approach:
double ratio = 1d / 12d;
double amount = 12000;
double ratioAmount = amount * ratio;
ratioAmount = 1.000

Quelle est la meilleure façon? Tout le monde dit que les montants / l'argent doivent être stockés sous forme de décimales.


0 commentaires

4 Réponses :


10
votes

Ne stockez jamais, jamais, jamais, jamais des montants financiers dans un double. Voici un exemple de de mon blog qui montre pourquoi double ne doit pas être utilisé:

decimal decAvailable = (decimal)dblAvailable;
decimal decTotal = (decimal)dblTotal;

if (decAvailable < decTotal)
{
    Console.WriteLine("They still don't add up!");
}

Vous verrez que la Console.WriteLine sera frappée car les doubles s'additionnent à 31995.270000000004 . Comme vous pourrez peut-être le deviner à partir des noms des variables, cet exemple de code était basé sur un code réel dans un système financier - ce problème empêchait les utilisateurs d'allouer correctement les montants aux transactions.

Ajout des nombres au format décimal avec ce code supplémentaire:

var lineValues = new List<double> { 1675.89, 2600.21, 5879.79, 5367.51, 8090.30, 492.97, 7888.60 };
double dblAvailable = 31995.27d;
double dblTotal = 0d;

foreach (var lineValue in lineValues)
{
    dblTotal += lineValue;
}

if (dblAvailable < dblTotal)
{
    Console.WriteLine("They don't add up!");
}

N'approchera pas de la Console .WriteLine . La morale de l'histoire: utilisez décimal pour les calculs financiers!

La toute première partie de la référence de langue pour le mot-clé décimal indique:

Comparé aux autres types à virgule flottante, le type decimal a plus de précision et une plage plus petite, ce qui le rend approprié pour les calculs financiers et monétaires.

Il convient également de noter que pour qu'un littéral numérique soit traité comme un nombre décimal, le suffixe m (pour m oney) doit être utilisé, indiquant en outre la pertinence du type de données financières.


3 commentaires

il ne fait aucun doute que vous avez tout à fait raison, il serait peut-être bon d'indiquer explicitement quel type doit être utilisé. implicitement, c'est clair, mais pour les gens qui ne savent pas cela pourrait être utile;) (tout comme vous avez la morale de l'histoire sur votre blog)


@deezg, merci et bon point! Ajouté =)


Toutes les informations contenues dans cette réponse sont correctes, mais elles ne répondent pas aux préoccupations du PO - elles n'expliquent pas pourquoi ils n'ont pas obtenu les résultats escomptés.



3
votes

decimal stocke 28-29 chiffres significatifs tandis que le double stocke ~ 15-17 chiffres

lorsque vous divisez 1m à 12m (1m / 12m), son résultat est 0.0833333333333333333333333333 ..... 3 où 3s sont infinis. float et arrondit deux fois au 0.083333333333333329 le plus proche .

lorsque 0.0833333333333333333333333333 ..... 3 est multiplié par 12000, le résultat est 999.9999999999999999. ..999999996 mais comme Decimal a 28-29 chiffres significatifs, il n'évalue pas 0.0833333333333333333333333333 plus que cela. et lorsque 0.0833333333333333333333333333 est multiplié par 12000, le résultat global est 999.99999999999999999999996


Mathématiquement

1d/12d = 0.083333333333333329 // looses precision
(1d/12d) * 12000 = 1000 // rounded

Evalue mathématiquement décimale

1m/12m = 0.0833333333333333333333333333
(1m/12m) * 12000 = 999.9999999999999999999999996

Double évalue mathématiquement

1/12 = 0.0833333333333333333333333333.....3
(1/12) x 12000 = 999.9999999999999999...999999996


1 commentaires

Outre ces arrondis, les parenthèses doivent également être prises en compte en tant que montant * (1m / 12m)! = Montant * 1m / 12m .



1
votes

Tous ceux qui vous disent d'utiliser décimal ont raison. Même les documents officiels disent que decimal est la chose à utiliser:

Comparé aux autres types à virgule flottante, le type décimal a plus de précision et une plage plus petite, ce qui le rend approprié pour les calculs financiers et monétaires.

Le comportement apparemment incorrect que vous avez observé vient du fait que 1/12 ne peut pas être parfaitement exprimé en décimal.

J'ai légèrement modifié vos exemples, et les a présentés comme des tests xUnit . Toutes les assertions des exemples passent.

Voici l'exemple qui vous pose des problèmes ...

[Fact]
public void OneTwelfthAsDecimal()
{
    Assert.Equal(0.0833333333333333333333333333m, 1m / 12m);
}

Clairement, 12 * ( 1/12) devrait être 1 , donc cela semble faux.

Avec une légère modification, nous pouvons obtenir le "correct" réponse ...

[Fact]
public void AnotherModifiedFirstApproach()
{
    // Values from first approach,
    // but with intermediate variables removed
    decimal ratioAmount = 12.000m * (1m / 12m);

    Assert.Equal(0.9999999999999999999999999996m, ratioAmount);
}

Le problème semble alors être la variable intermédiaire ratio , bien qu'il soit plus juste de la considérer comme une commande- problème de fonctionnement. L'ajout de parenthèses réintroduit l'erreur du code d'origine ...

[Fact]
public void ModifiedFirstApproach()
{
    // Values from first approach,
    // but with intermediate variables removed
    decimal ratioAmount = 12.000m * 1m / 12m;

    Assert.Equal(1.000m, ratioAmount);
}

Le problème central peut être illustré sur une seule ligne ...

[Fact]
public void FirstApproach()
{
    // First approach:    
    decimal ratio = 1m / 12m;
    decimal amount = 12.000m;

    decimal ratioAmount = amount * ratio;

    Assert.Equal(0.9999999999999999999999999996m, ratioAmount);
}

La fraction 1/12 ne peut être exprimée que sous la forme d'une décimale répétitive, ce qui la rend imprécise. Ce n'est pas la faute de C # - c'est juste un fait de travailler dans un système numérique décimal (base 10).


5 commentaires

Comment stocker 1/12 précisément sous forme de fraction binaire? Je pourrais me tromper, mais il me semble que ce devrait être un nombre infiniment répété.


En fait, je suis certain que vous ne pouvez pas. Pensez-y. Si vous pouviez, cela signifierait qu'il existe de tels entiers x et y que x / (2 ^ y) = 1/12 . Cela signifie que 12x = 2 ^ y . Mais c'est impossible puisque 12 est divisable par 3 , mais 2 ^ y ne l'est pas.


J'ai peut-être pris une longueur d'avance sur cette affirmation. Je vais le supprimer de ma réponse pendant que je raisonne moi-même.


@ Vilx- Vous avez raison, et (maintenant que je l'ai raisonné) c'est une explication très claire. Merci.


Vous êtes les bienvenus. :)



7
votes

Il semble que tous ces articles se rapprochent, mais n'expliquent pas tout à fait le nœud du problème. Ce n'est pas que decimal stocke les valeurs plus précisément ou que double a plus de chiffres ou quelque chose comme ça. Ils enregistrent chacun des valeurs différemment .

Le type decimal stocke les valeurs sous forme décimale. Comme 1234.567 . Le double (et le float ) stocke les valeurs sous forme binaire, comme 1101010.0011001 . (Ils ont également des limites quant au nombre de chiffres qu'ils peuvent stocker, mais ce n'est pas pertinent ici - ou jamais. Si vous sentez que vous manquez de chiffres pour la précision, vous faites probablement quelque chose de mal)

Notez qu'il existe certaines valeurs qui ne peuvent pas être stockées précisément dans l'une ou l'autre notation, car elles nécessiteraient une quantité infinie de chiffres après la virgule décimale. Comme 1/3 ou 1/12 . Ces valeurs sont un peu arrondies lorsqu'elles sont stockées, ce que vous voyez ici.

L'avantage de decimal dans les calculs financiers est qu'il peut stocker des fractions décimales avec précision alors que double ne le peut pas. Par exemple, 0.1 peut être stocké précisément en décimal mais pas en double . Ce sont les types de valeurs que prennent habituellement les sommes d'argent. Vous n'avez jamais besoin de stocker 2/3 d'un dollar, vous avez besoin exactement de 0,66 dollar. Les devises humaines sont basées sur des décimales, donc le type decimal peut bien les stocker.

De plus, l'ajout et la soustraction de valeurs décimales fonctionnent parfaitement avec le type decimal également. Et c'est l'opération la plus courante dans les calculs financiers, c'est donc plus facile à programmer de cette façon.

La multiplication des valeurs décimales fonctionne également assez bien, même si elle peut augmenter le nombre de décimales utilisées pour garantir une valeur exacte.

Mais la division est très risquée car la plupart des valeurs que vous obtenez en divisant ne seront pas stockables avec précision et une erreur d'arrondi se produira.

À la fin de la journée, les valeurs double et décimal peuvent être utilisées pour stocker des valeurs monétaires, il vous suffit de faire très attention à leurs limites. Pour un type double , vous devez arrondir le résultat après chaque calcul, même addition et soustraction. Et chaque fois que vous affichez des valeurs à l'utilisateur, vous devez les formater explicitement pour avoir un certain nombre de chiffres décimaux. De plus, lorsque vous comparez des nombres, veillez à ne comparer que les X premiers chiffres décimaux (généralement 2 ou 4).

Pour un type décimal , certaines de ces restrictions peuvent être assouplies puisque vous savez que votre valeur monétaire est stockée avec précision. Vous pouvez généralement sauter les arrondis après l'addition et la soustraction. Si vous ne stockez que X chiffres décimaux en premier lieu, vous n'avez pas à vous soucier de la mise en forme et de la comparaison d'affichage explicites. Cela facilite considérablement les choses. Mais vous devez encore arrondir après multiplication et division.

Il y a une autre approche élégante qui n'est pas abordée ici. Changez vos unités monétaires. Au lieu de stocker les valeurs en dollars, stockez les valeurs en centimes. Ou si vous travaillez avec 4 chiffres décimaux, stockez 1 / 100e de cent.

Ensuite, vous pouvez utiliser int ou long pour tout!

Cela a la plupart des avantages d'un décimal (valeurs stockées avec précision, l'addition / soustraction fonctionne avec précision), mais les endroits dont vous avez besoin pour arrondir les choses deviendront encore plus évidents. Un léger inconvénient est cependant que le formatage de telles valeurs pour l'affichage devient un peu plus compliqué. D'un autre côté, si vous oubliez de le faire, cela aussi sera évident. C'est mon approche préférée jusqu'à présent.


4 commentaires

certaines valeurs .. ne peuvent être stockées précisément dans aucune notation


@TaW Umm ... Qu'est-ce que tu essaies de dire?


Juste en insistant sur le point principal de votre message; J'ai manqué de mettre les guillemets ;-)


@TaW - Ahh, d'accord. :)