As Martin Fowler said in his article about Patterns of Enterprise Application Architecture (https://martinfowler.com/eaaCatalog/money.html):
"A large proportion of the computers in this world manipulate money, so it's always puzzled me that money isn't actually a first class data type in any mainstream programming language."
Well, I didn't read the book Patterns of Enterprise Application Architecture, but I do know that he suggests the implementation of a class Money, with the attributes currency and amount, as well as a method called allocate that receives a list of proportions and distributes that money without leaking values with rounding. In this article I'll show how we've implemented our Money to solve a real problem of losing cents in apportionments, and also solve the mystery of this article's title.
We already had a Money class, which made it really easier to write the Money Pattern. So, if I am to give you a first hint, it is: DO NOT use Double or Big Decimal to financial values. Build your "Value Object". After having financial values spread all over your system, it's gonna be harder - but not impossible - to refactor.
Our Money class doesn't have the attribute currency because we don't have the need for a internationalization, at least not yet. And we are not going to implement currency until we actually need it. Just like we didn't have the allocate method. Until we needed it. So, our Money was like this:
public class Money implements Serializable {
private final BigDecimal amount;
private Money(BigDecimal amount) {
if (amount == null) {
throw new AmountCantBeEmptyException();
}
this.amount = amount;
}
public static Money of(BigDecimal amount) {
return new Money(amount);
}
public Money plus(Money addition) {
return Money.of(this.amount.add(addition.amount));
}
public Money minus(Money discount) {
return Money.of(this.amount.subtract(discount.amount));
}
public Money times(BigDecimal factor) {
return Money.of(this.amount.multiply(factor));
}
public BigDecimal getAmount() {
return amount;
}
@Override
public boolean equals(Object obj) {
if (obj == null || !(obj instanceof Money)) {
return false;
}
return this.amount.compareTo(((Money) obj).amount) == 0;
}
@Override
public int hashCode() {
return this.amount.hashCode();
}
}
This used to solve most of our financial problems. Until the day we needed the allocate method. A debit worth of $ 1000.20 should be apportioned between several accounts with the following rules:
# | Proportion | Gross Value | Rounded Down | Rounded Up | Rounded Half Even |
---|---|---|---|---|---|
1 | 22,9% | 229.0458 | 229.04 | 229.05 | 229.05 |
2 | 27.7% | 277.0554 | 277.05 | 277.06 | 277.06 |
3 | 7.8% | 78.0156 | 78.01 | 78.02 | 78.02 |
4 | 22.6% | 226.0452 | 226.04 | 226.05 | 226.05 |
5 | 19% | 190.038 | 190.03 | 190.04 | 190.04 |
Total | 100% | 1000.20 | 1000.17 | 1000.22 | 1000.22 |
As you can see, applying the apportionment percentage on every item, we'll never get to 1000.20 - this happens because cents are an indivisible unit, so the financial value represented by Money can't be 229.0458, for example. If we round down, 3 cents are lost in the end. If we round up, we get 2 extra cents. Even using Round Half Even will have precision loss.
And guess who faced a problem like this? George Washington himself and one of the proposed solutions came from Alexander Hamilton, one of the Founding Fathers, and first United States Secretary of the Treasury, using his Largest Remainder Method.
The problem faced at that time, described in U.S. Census Bureau, was shortly:
A country with 4 states, each one with its own population, and 20 Seats at the House of Representatives. How many senators each state should have so they are represented proportionally? A senator, just like a penny, can't be divided in pieces. Right? Just double checking...
State | Population | Seats by State | Seats by State before Hamilton's method | Seats by State after Hamilton's method |
---|---|---|---|---|
1 | 2560 | 4.31 | 4 | 4 |
2 | 3315 | 5.58 | 5 | 6 |
3 | 995 | 1.67 | 1 | 2 |
4 | 5012 | 8.44 | 8 | 8 |
Total | 11882 | 20 | 18 | 20 |
Hamilton's method consists in calculating the proportion between the population of 11882 and the number of 20 seats, resulting in the quota of 594.1. Now, for each state, divide its population for the quota. For example in the state 1, 2560/594.1 getting a quotient of 4.31.
As we have agreed, we can't have 4 senators and 0.31 of a senator for a state. So the number of senators for the state 1 is 4, which is the whole part of the result. That happens for every state. In the end, because of the rounding, the number of senators allocated in the states is 18. Which states should the other 2 senators go to? That's when Hamilton comes with a possible solution. His idea is that we distribute these 2 seats to the states that had the largest decimal part before the rounding.
I'll explain. If we suppress the whole part of the Seats by State column and desc order by the decimal part, we get:
- 1,67
- 5,58
- 8,44
- 4,31
Since we have a total of 2 seats to redistribute, the states that will receive these senators are the first 2 of the list. Resulting in the column Seats by State after Hamilton's method.
There's a paradox to be noticed that was discovered by applying Hamilton's method in the population of Alabama, reason it is called the Alabama Paradox. That will not be covered here. But this is when Uncle Bob comes in the article.
Amongst his countless contributions to the software development community, Uncle Bob spread the SOLID principles. They were relevant in the 90's, they are relevant now. The principle we are focusing now is the Open-Closed Principle.
We need to change our Money class to implement the allocate method to distribute its values in ratios without losing any cents. So we created it like:
public class Money implements Serializable {
private static final BigDecimal ONE_HUNDRED = new BigDecimal(100);
public List<Money> allocate(List<BigDecimal> ratios, RemainderDistribution distribution) {
long amountInCents = toCents();
List<Quota> quotas = new ArrayList<>();
for (BigDecimal ratio: ratios) {
quotas.add(new Quota(amountInCents, ratio));
}
distribution.distribute(quotas, amountInCents);
return quotas.stream().map(Quota::toMoney).collect(Collectors.toList());
}
public long toCents() {
return this.amount.multiply(ONE_HUNDRED).longValue();
}
}
public class Quota {
private static final BigDecimal ONE_HUNDRED = new BigDecimal(100);
private long amount;
private long total;
private BigDecimal ratio;
public Quota(long total, BigDecimal ratio) {
this.total = total;
this.ratio = ratio;
this.amount = ratio.multiply(BigDecimal.valueOf(total)).longValue();
}
}
public interface RemainderDistribution {
void distribute(List<Quota> quotas, long total);
}
Remember the Alabama Paradox? Even having validated with our Product Manager that we would adopt Hamilton's method we still have some doubts about how this will behave in the user's hands. So we didn't want to have this hard-coded in Money class. We wanted to have it changeable whenever needed. We wanted to make our Value Object Opened for extension but Closed for modification. That's why RemainderDistribution is an interface. One of its possible implementations, the one we used, is HamiltonApportionmentDistribution:
public class HamiltonApportionmentDistribution implements RemainderDistribution {
@Override
public void distribute(List<Quota> quotas, long total) {
long remain = total;
for (Quota quota : quotas) {
remain = remain - quota.getAmount();
}
List<Quota> sortedQuotas = quotas.stream().sorted(Comparator.comparing(Quota::getFractionalPart).reversed()).collect(Collectors.toList());
Iterator<Quota> iterator = sortedQuotas.iterator();
while(remain > 0) {
remain = remain - 1;
iterator.next().addRemain(1);
}
}
}
This way we solved that initial problem. where rounding down we lost 3 cents (remember that rounding down is due to the cents, similar to when we round a senator down). The calculation must be done using the smallest unit, in this case, cents. And we got this result:
# | Proportion | Gross Value | Gross Value in Cents | Final Value |
---|---|---|---|---|
1 | 22.9% | 229.0458 | 22904.58 | 229.05 |
2 | 27.7% | 277.0554 | 27705.54 | 277.05 |
3 | 7.8% | 78.0156 | 7801.56 | 78.02 |
4 | 22.6% | 226.0452 | 22604.52 | 226.04 |
5 | 19% | 190.038 | 19003.80 | 190.04 |
Total | 100% | 1000.20 | 100020 | 1000.20 |
At this point I hope I have demystified the intersection between Alexander Hamilton, Martin Fowler and Uncle Bob with the Financial Systems. And, as bonus, you get some code to apply in your favorite language. Ah, all this code was covered with Unit Tests.
Bonus: if you don't know who Alexander Hamilton was, there is a musical movie available in Disney+, called Hamilton. It tells the story of this who was the American First Secretary of Treasury recorded with rap, hip hop and jazz, directly from the Broadway in 2016. Here's the trailer of this amazing film:
Top comments (0)