0

I've read that BigDecimal is the way to go when representing money in Java.

But I don't understand why one of my unit tests is failing with the following message:

org.opentest4j.AssertionFailedError:   
Expected :0.40  
Actual   :0.4

The "Actual" value is the result of BigDecimal.valueOf(0.398).setScale(2, RoundingMode.HALF_UP).

So I guess my question has two parts:

  1. Why aren't these values treated as equal?
  2. How can I represent $0.40 as a BigDecimal in a way that won't trigger this sort of mismatch?
crenshaw-dev
  • 7,504
  • 3
  • 45
  • 81
  • 4
    as per javadocs *Compares this BigDecimal with the specified Object for equality. Unlike compareTo, this method considers two BigDecimal objects equal only if they are equal in value and scale (thus 2.0 is not equal to 2.00 when compared by this method).* – Scary Wombat Jun 28 '19 at 01:49
  • Maybe you can compare the result of `.doubleValue()` for both? – buræquete Jun 28 '19 at 01:49
  • @buræquete That might result in precis in errors/rounding, because floating point is not exact.o – Tim Biegeleisen Jun 28 '19 at 01:50
  • @TimBiegeleisen I said so since he is already rounding to a 2 decimal points, there wouldn't be that much trouble at that point I think. – buræquete Jun 28 '19 at 01:51
  • @TimBiegeleisen I'm puzzled why this is a duplicate. The linked question seems more about presentation than value. And I don't think the accepted answer addresses either why these values aren't considered equal or how to represent them in a way that causes them to be treated as equal (unless you suggest I convert to string for comparison). – crenshaw-dev Jun 28 '19 at 01:54
  • @MichaelCrenshaw Yes, I am suggesting that you convert to string for comparison. BigDecimal `0.40` and `0.4` are not the same thing, so you need a way to articulate to Java that they mean the same thing. – Tim Biegeleisen Jun 28 '19 at 01:56
  • 3
    `0.40` means that you have measured to the hundreths place, and you know for certain that the hundreths place is `0`. `0.4` means that you have _not_ measured to the hundreths place, and you _don't_ what the hundreths value is. – Tim Biegeleisen Jun 28 '19 at 01:57
  • Yup, makes total sense. (Still doesn't feel like a dupe, but you're the Java expert.) To avoid string comparison, could I just `setScale(2)` on both sides of the comparison? – crenshaw-dev Jun 28 '19 at 01:57
  • That seems to be what the javadocs are saying – Scary Wombat Jun 28 '19 at 01:58
  • @ScaryWombat can you drop a link for that? – crenshaw-dev Jun 28 '19 at 01:58
  • 1
    https://docs.oracle.com/javase/7/docs/api/java/math/BigDecimal.html#equals(java.lang.Object) – Scary Wombat Jun 28 '19 at 01:59
  • 1
    Also could try using `compareTo` – Scary Wombat Jun 28 '19 at 02:00
  • Unfortunately, I think junit enforces using `equals` (though I'm new to both Java and junit). But applying `setScale` to both sides satisfied the unit test! If someone wants to put that info in an answer, I'll gladly accept. (First one wins, all this info was helpful. :-)) – crenshaw-dev Jun 28 '19 at 02:01
  • 1
    No, you should use `assertEquals(0, actualBigDecimal.compareTo(expectedBigDecimal));` – Dawood ibn Kareem Jun 28 '19 at 02:17

1 Answers1

-1

BigDecimals are a combination of a number and a 'scale'. 2 BDs dont consider themselves equal unless both are equal. I suggest using .compareTo(other) == 0) to get the answer.

NB: I don't think using BD is a good way to do currency.

There are generally 2 ways to approach currency. The easy way and the hard way.

The easy way is to store cents in an int or long. So, store $0.40 as simply 40, and something like $12.50 would be stored as 1250. You now have 2 problems: You can't represent half-cents, and overflow can happen (you can't represent an amount higher than 2^31-1 cents, but that's.. a lot of cents. Make it a long and we're well beyond the GDP of the entire world).

But, half-cents are a problem in general. Take this issue:

I have 4 cents. I wish to divide these amongst 3 people.

So, what do we do? BigDecimal can't help you out here; you can't represent 4 divided by 3 with BD with perfection. YOu have to round SOMEWHERE (it's 1.33333333... BD can't represent infinite sequences). Even if it could or you decide to round at some egregious amount (lets say 200 digits), now what? You can't tell your bank to transfer a third of a cent. There are no easy answers: If this is what your app needs to do, then you need to decide how to deal with this. For example, 'the remaining cent is for the house' or 'the software picks a random recipient; they get 2 cents, the other 2 get a single cent).

In other words, if 'just use an int/long' doesn't do it, odds are BigDecimal is no good either.

rzwitserloot
  • 85,357
  • 5
  • 51
  • 72
  • 1
    There is no answer at all to dividing 4 cents among 3 people. `BigDecimal` is behaving exactly like the bank here. – user207421 Jun 28 '19 at 03:58
  • I guess using `long` could force the developer to confront rounding issues such as the four-cent problem, where using `BigDecimal` could allow them to happily divide away without considering the consequences. Well-phrased answer. – crenshaw-dev Jun 28 '19 at 13:23
  • @user207421 indeed, which is precisely my point: Using BD is not some sort of silver bullet that solves all issues. Using BD is a lot more convoluted than just a long representing cents, and I was trying to tell OP that this extra complication is generally not worth it. – rzwitserloot Jun 29 '19 at 07:46