views:

520

answers:

6

What is the best way to solve this problem in code?

The problem is that I have 2 dollar amounts (known as a pot), that need to be allocated to 3 people. Each person gets a specific amount that comes from both pots and the rates must be approximately the same. I keep coming across rounding issues where my allocations either add up to too much or too little.

Here is a specific example:

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

Person #1 gets Allocation Amount: 345,678.89
Person #2 gets Allocation Amount: 460,599.73
Person #3 gets Allocation Amount: 304,832.48

My logic is as follows (Code is in 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;
        }
    }
}

The results I get are as follows:

Pot #1, Person #1 = 307,270.13
Pot #1, Person #2 = 409,421.99
Pot #1, Person #3 = 270,962.21
Pot #1 Total = 987,654.33 (1 penny off)

Pot #2, Person #1 = 38,408.76
Pot #2, Person #2 = 51,177.74
Pot #2, Person #3 = 33,870.27
Pot #2 Total = 123,456.77 (1 penny off)

The Pot Totals should match the original totals.

I think I may be missing something or there may be an extra step that I need to take. I think I am on the right track.

Any help would be greatly appreciated.

+1  A: 

Definitely the Math.Round.

I would suggest not rounding the calculation result, but if you need to display, then round to nearest penny. Or you can use pennies as smallest denominator, thus when displaying, divide everything by 100.

Adrian Godong
+2  A: 

I think this is exactly the problem that Eric Evans addresses in his "Domain Driven Design" Chapter 8, pp. 198-203.

duffymo
What did Eric Evans said then?
Adrian Godong
Can you give an excerpt from the book?
Jon
I agree that Evans did a great job discussing this issue.
Doug McClean
+2  A: 

Have you tried conntrolling the rounding behavior with the MidpointRounding argument?

public static decimal Round( decimal d, MidpointRounding mode )
Brian Reiter
+8  A: 

This happens in financial calculations a lot when rounding to the nearest penny. No amount of tweaking the individual operations rounding algorithm will work for every case.

You have to have an accumulator that tracks the amount allocated after the rounding and distribution operation. At the end of the allocations, you check the accumulator against the actual results (summed together) and distribute the leftover penny.

In the math example below, if you take 0.133 and round it to 0.13 and add 3 times you get a penny less than if you add 0.133 3 times first and then round.

 0.13    0.133
 0.13    0.133
+0.13   +0.133
_____   ______
 0.39    0.399 -> 0.40
Matt Spradley
+1  A: 

+1 for Matt Spradley's solution.

As an additional comment to Matt's solution, you of course also need to account for the case where you end up allocating penny (or more) less than the target amount -- in that case, you need to subtract money from one or more of the allocated amounts.

You also need to ensure that you don't end up subtracting a penny from an allocated amount of $0.00 (in the event that you are allocating a very small amount among a large number of recipients).

Jon Schneider
+1  A: 

What to do when dividing money is a perennial problem. Martin Fowler offers some commentary here (I think there is more detail in his actual PoEAA book):

But division is not [straightforward], as we have to take care of errant pennies. We'll do that by returning an array of monies, such that the sum of the array is equal to the original amount, and the original amount is distributed fairly between the elements of the array. Fairly in this sense means those at the beginning get the extra pennies.

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;
    }
AakashM