views:

85

answers:

4

On the backend I'm storing money values in a Money class which wraps a BigDecimal and sets rounding to be always Half Even with scale 8. All basic operations work fine and behave as expected. But I need to show those values to the user with scale of 2, and that's bringing me rounding errors.

For example, I have these values in the backend:

a = 109.11432
b = 9015.57069
c = 9124.68501

Each one of them is formatted to the pt-BR locale:

NumberFormat nf = NumberFormat.getInstance();
nf.setCurrency(Currency.getInstance(new Locale("pt","BR")));
nf.setMinimumFractionDigits(2);
nf.setMaximumFractionDigits(2);
String n = nf.format(valor);
return n;

And then I have

a = 109,11
b = 9.015,57
c = 9.124,69

And that's ok, at first. But c should be a + b. With the real values, this is guaranteed, but the rounding gives me a 0.01 error.

What's the proper way to handle this situation?

A: 

If I were writing a money class, I would make it consist of two integer portions - dollars and cents (or reais and centavos, or whatever). That way you never end up with fractional cents. You just have to handle rolling over 100 cents in the addition and subtraction operations.

EDIT:

The comment to my original response has a good point. Another option would be to just store the number of cents, then divide by 100 when you need to display.

Jeff Barger
I don't think it's a good way to handle this problem, there will be too many problems implementing all the operations so they work properly. I think it will be too error prone.
Zenzen
A: 

In a class I'm using currently the "value" was represented by a Long in cents (or whatever you want), so we have 2 methods one that returns this value and one that returns the "amountValue" (which is value/100.0) so for example the user inputs 12,5$ and the class stores it as 12,5*100=1250. Then we do all the operations using that value and we round it so it's not a floating point unit.

BUT be aware that even this solution isn't flawed, but it's more because of java/how computers represent the values. For example inputing 8,7 is ok and gives us back a value 8,7 when needed (first it's converted to 870 and then divided by 100.0 when we need the "amountValue") but for example when the user inputs 8,2 and wants to retrieve that value he gets 8,19 (because according to java 8,2*100=819,(9)). Not sure there's an easy way to make a Money class that would work in every single case.

Zenzen
You could use Math.round(8.2 * 100) to get it to a long and then you get the expected 820.
taotree
Everything you talk about is a non-issue when using BigDecimal as Vito does - the *correct* way to store money. Storing cents as int or long is a workaround for when you don't have a proper decimal datatype. There is absolutely no justification to do that when you have BigDecimal.
Michael Borgwardt
I disagree. Unless one has business requirement that requires storage of fractional money units (in which case I would agree with using BigDecimal), then storing it as an integer forces it to be stored as an exact value. BigDecimal would allow storage of an invalid value. And since rounding questions are driven by the specific application, those generally have to be handled carefully and specifically and so BigDecimal does not give you any special advantage.
taotree
Also, note that the 8.19 issue mentioned is an artifact of converting the user input into a float before converting to an integer--which is not necessary, so the issue can and should be avoided.
taotree
I really can't fully agree with you Michael, there's plenty of applications using monetary values where you don't need values as precise as BigDecimal, which only then slows your program unnecessarily. Even when dealing with matters such as taxes, invoices etc. in most cases you will just want to round it to a proper integer or leave only 2 decimal places where taotree's will fix all the problems.
Zenzen
+1  A: 

It seems to be doing exactly what you want it to. The way you are storing it, you are storing far beyond 0.01. If that is not your intention, then stop doing it :)

If you want a + b to equal c with 2 decimal places, then you need to round before doing the addition. The solution to your issue depends on the requirements of your application. One common way of storing money is actually with an integer. That way, you cannot store fractions and you could never have the issue you are currently describing.

But it really depends on your requirements. Are you required to do money arithmetic based on 0.01, or on full accuracy and then round the end result? That's a business question, not a technical question.

taotree
sure you can have the same issue - you still need to round at some point.
Michael Borgwardt
No, you can't have the exact same issue because he is adding two stored money values. Adding two stored money values when using an integer can never have 1+7 = 9 which is the exact issue he is having.Using an integer forces you to make the appropriate decisions of rounding and arithmetic before you store the value.
taotree
I'm required to do money arithmetic based on full accuracy and then round the end result. That's the problem.
Vitor De Mario
Which thing is the end result? The problem here is now you're saying a+b=c at two decimals--why? If this is only for display purposes then displaying .12 when you're storing .125 is wrong--it's a lie. If you are storing fractional pennies, display that or you will get these weird things. If c is some final answer thing, detect a situation where .4 + .4 = .8 means 0 + 0 = 1 and instead round one up and one down. Anyway... again, you're asking a business question, not a technical one. What are the rules/constraints around these calculations. Once you have those defined, you can implement it.
taotree
I guess you're right, taotree. I need two decimals only for display purposes, so it ends up being a lie, indeed. We've been discussing our requirements around this issue, and hopefully will settle into a solution based on the business needs of the system.
Vitor De Mario
+1  A: 

Note that a NumberFormat also can have a rounding mode.

But ultimately, no rounding method can fulfill a business requirement like "these values have to add up to this one" without being designed specifically for that case. Round-half-even is designed to avoid a large-scale bias, not single last-decimal errors. So where do you originally get the data from? That's where you have to make sure that the rounding preserves the total.

Is storing the data with 8 fractional digits really a requirement, since you display only 2? I'd also question the assumption that "With the real values, this is guaranteed", since the same thing could happen there, when rounding to 8 digits after whatever calculation produced the values.

Michael Borgwardt