2009-05-29 12 views
7

Quelle est la meilleure façon de résoudre ce problème dans le code?Arrondir les problèmes avec l'allocation des montants en dollars entre plusieurs personnes

Le problème est que j'ai 2 montants en dollars (appelés pot), qui doivent être alloués à 3 personnes. Chaque personne reçoit un montant spécifique qui provient des deux pots et les taux doivent être à peu près les mêmes. Je rencontre toujours des problèmes d'arrondis où mes allocations sont trop élevées ou insuffisantes.

Voici un exemple précis:

Pot # 1 987,654.32
Pot # 2 123,456.78

Personne # 1 obtient Allocation Montant: 345,678.89
Personne # 2 obtient Allocation Montant: 460,599.73
Personne # 3 obtient Allocation Montant: 304,832.48

Ma logique est la suivante (le code est en C#):

foreach (Person person in People) 
{ 
    decimal percentage = person.AllocationAmount/totalOfAllPots; 

    decimal personAmountRunningTotal = person.AllocationAmount; 

    foreach (Pot pot in pots) 
    { 
     decimal potAllocationAmount = Math.Round(percentage * pot.Amount, 2); 
     personAmountRunningTotal -= potAllocationAmount; 

     PersonPotAssignment ppa = new PersonPotAssignment(); 
     ppa.Amount = potAllocationAmount; 

     person.PendingPotAssignments.Add(ppa); 
    } 

    foreach (PersonPotAssignment ppa in person.PendingPotAssignments) 
    { 
     if (personAmountRunningTotal > 0) //Under Allocated 
     { 
      ppa.Amount += .01M; 
      personAmountRunningTotal += .01M; 
     } 
     else if (personAmountRunningTotal < 0) //Over Allocated 
     { 
      ppa.Amount -= .01M; 
      personAmountRunningTotal -= .01M; 
     } 
    } 
} 

Les résultats que je reçois sont les suivants:

Pot # 1, personne # 1 = 307,270.13
Pot # 1, personne # 2 = 409,421.99
Pot # 1, personne # 3 = 270,962.21
pot # 1 total = 987,654.33 (1 penny off)

pot # 2, personne # 1 = 38,408.76
pot # 2, personne # 2 = 51,177.74
pot # 2, personne # 3 = 33,870.27
pot # 2 Total = 123 456,77 (1 penny off)

Les totaux de pot doivent correspondre aux totaux d'origine.

Je pense que je peux manquer quelque chose ou il y a peut-être une étape supplémentaire que je dois prendre. Je pense que je suis sur la bonne voie.

Toute aide serait grandement apprécié.

+0

Vous pouvez voir cet article que je l'ai écrit sur la façon de gérer cela dans SQL: [Financial Arrondi d'attribution] (http://www.sqlservercentral.com/articles/Financial+Rounding/88067 /) –

Répondre

12

Cela se produit beaucoup dans les calculs financiers en arrondissant au centime le plus proche. Aucune modification de l'algorithme d'arrondi des opérations individuelles ne fonctionnera dans tous les cas.

Vous devez avoir un accumulateur qui suit la quantité allouée après l'opération d'arrondi et de distribution. À la fin des allocations, vous vérifiez l'accumulateur par rapport aux résultats réels (additionnés ensemble) et distribuez le reste de la pièce.Dans l'exemple mathématique ci-dessous, si vous prenez 0,133 et arrondissez-le à 0,13 et ajoutez 3 fois, vous obtenez un penny de moins que si vous ajoutez 0,133 3 fois d'abord puis arrondissez.

0.13 0.133 
0.13 0.133 
+0.13 +0.133 
_____ ______ 
0.39 0.399 -> 0.40 
+1

Belle illustration. C'est pourquoi, dans la plupart des cas, vous devriez retarder l'arrondissement le plus longtemps possible. – pseudocoder

1

Certainement le Math.Round.

Je suggère de ne pas arrondir le résultat du calcul, mais si vous avez besoin d'afficher, arrondissez au centime près. Ou vous pouvez utiliser des centimes comme le plus petit dénominateur, ainsi lors de l'affichage, divisez tout par 100.

1

Je pense que c'est exactement le problème que Eric Evans adresse dans son "Domain Driven Design" chapitre 8, pp. 198-203.

+2

Qu'a dit alors Eric Evans? –

+1

Pouvez-vous donner un extrait du livre? – Jon

+0

Je suis d'accord que Evans a fait un excellent travail pour discuter de ce problème. –

2

Avez-vous essayé de connecter le comportement d'arrondi avec l'argument MidpointRounding?

public static decimal Round(decimal d, MidpointRounding mode) 
2

+1 pour la solution de Matt Spradley.

En tant que commentaire supplémentaire à la solution de Matt, vous devez bien sûr aussi de tenir compte du cas où vous finissez par l'allocation penny (ou plus) moins que le montant cible - dans ce cas, vous devez soustraire de l'argent d'un ou de plusieurs des montants alloués.

Vous devez également vous assurer de ne pas soustraire un centime d'un montant alloué de 0,00 $ (dans le cas où vous allouez une très petite quantité parmi un grand nombre de destinataires). Ce qu'il faut faire lorsque l'on divise de l'argent est un problème perpétuel.

1

Martin Fowler offre quelques commentaires here (je pense qu'il est plus en détail dans son livre PoEAA réel):

Mais la division n'est pas [simple], nous devons prendre soin de quelques centimes errantes. Nous le ferons en retournant un tableau de sommes, de sorte que la somme du tableau soit égale au montant d'origine et que la quantité d'origine soit répartie équitablement entre les éléments du tableau. Assez dans ce sens signifie que ceux au début obtiennent les centimes supplémentaires.

class Money... 
    public Money[] divide(int denominator) { 
     BigInteger bigDenominator = BigInteger.valueOf(denominator); 
     Money[] result = new Money[denominator]; 
     BigInteger simpleResult = amount.divide(bigDenominator); 
     for (int i = 0; i < denominator ; i++) { 
      result[i] = new Money(simpleResult, currency, true); 
     } 
     int remainder = amount.subtract(simpleResult.multiply(bigDenominator)).intValue(); 
     for (int i=0; i < remainder; i++) { 
      result[i] = result[i].add(new Money(BigInteger.valueOf(1), currency, true)); 
     } 
     return result; 
    }