DEV Community

Serguey Asael Shinder
Serguey Asael Shinder

Posted on

Your Map Could Not Find 120.50 Because the Bank Wrote 120.5

The reconciliation job was simple.

Load the ledger.

Put every open payment in a map,
keyed by its amount.

Read the bank file,
look up each amount,
mark it matched.

You used BigDecimal,
because you know
money does not go in a double.

Most mornings,
about one payment in ten
stayed unmatched.

Finance cleared them by hand.

Then someone noticed
they all had something in common.

120.50.

99.90.

15.00.

Every amount
whose last digit was a zero.

Here is the mechanism.

A BigDecimal is two things.

A whole number of units,
and a scale
that says where the point goes.

The ledger column
had two decimal places,
so 120.50 came back
as 12050 with a scale of two.

The bank file wrote 120.5.

That parsed as 1205
with a scale of one.

Same amount.

Different object.

And equals in BigDecimal
compares both parts.

So 120.5 does not equal 120.50.

The hash code is built
from both parts too,
so the map looks in a different bucket
and finds nothing.

It is written down.

In the Javadoc,
in plain words,
and almost nobody has read it.

compareTo is the one that ignores scale.

It says the two are equal.

So a TreeMap
would have matched them,
and a HashMap
quietly never will.

Kotlin will not save you either.

Its double equals calls equals.

The fix is not to change the map.

It is to decide, at the edge,
what an amount looks like.

The moment a value comes in,
from a file, a column or a request,
set its scale to the currency's.

Use setScale with
RoundingMode.UNNECESSARY,
so a value with a third decimal
throws
instead of being rounded
into a match it does not deserve.

stripTrailingZeros
also makes them agree,
but 100.00 comes out as 1E+2,
and someone will print it.

Then write the test
with the bank's spelling
and the ledger's spelling
of the same number.

Expect one match.

Two values can be equal
and still not be the same.

Decide which one your code means
before the data decides for you.

– Serguey Asael Shinder

Top comments (0)