This answer comes in three parts:
- Explanation of why doubles are inaccurate and why that isn't easily fixable.
- The strategy that usually results in the 'right' answer with double math, and would in your case, but which has no particular guarantees
- An explanation of how to use BigDecimal which does get you that guarantee, at the cost of other issues
Doubles are inaccurate
Take a piece of paper.
Write down the result of 1 divided by 5. Write it down in decimal form. You'll write "0.2" and hand it back to me.
So far, so good.
Now do the same thing, but this time, write down 1 divided by 3.
You'll find that this is not possible. You write 0.333333... and eventually you run out of space on the paper to write more threes. You hand it back to me and the number is very slightly inaccurate.
double
is the same thing. Except, computers are decimal. The reason 1/5 fits and 1/3 does not fit, is because we are writing in decimal (base 10), and 5 'fits nicely' in 10, but 3 does not. For the same reason, you can write down 1/2 (0.5 - because 2 fits in 10). Explaining why 1/4 fits is slightly more problematic (4 does not fit nicely into 10, but once you write down the part that does fit, which is 1/5, 1/20th is left, and 20 does fit nicely into 10) - but the same principle applies.
Computers count in base 2. That's quite annoying - whereas the things that divide nicely into 10 is 1, 2, 5, 10, and anything that can be broken down into factors of those numbers, with base 2 it's just 1 and 2. So 1/2, 1/4, 1/8, 5/16 - those all fit 'perfectly', but even something as simple as 0.1 (1/10th) does not fit.
Witness:
double v = 0.0;
for (int i = 0; i < 10; i++) v += 0.1;
System.out.println(v);
System.out.println(1.0 == v);
The above prints surprising results if you aren't aware of this rounding business. It does not print 1.0 (instead, 0.9999999), and it also does not print 'true' - instead, it prints false. Which is weird.. 10 * 0.1 is obviously 1.0.
The rounding killed you here. It's identical to me asking you to write '1/3' on a note, do it two more times, and then asking you to add up the numbers on the 3 notes - which is no longer 1.0, but instead 0.9999999999 (not infinitely, just a lot, but a finite amount, of nines), and not equal to 1.0.
A completely different way to think about it: double
is represented by 64 bits.
Imagine I gave you 3 switches and the only way you can store information is by leaving the switching in an up or down state. You can 'remember' 8 unique events: 000, 001, 010, 011, 100, 101, 110, and 111. That's it. Same with doubles, but instead of 8 (which is 2 to the 3rd power), it's a humongous number: 2 to the 64th power.
But that's still a finite amount of unique things you can remember, and there are an infinite amount of numbers between 0 and 1, let alone between - and + infinity which is what double more or less oversimplified says it can represent.
Doubles can actually only represent slightly fewer than 2^64 unique numbers. Let's call those the 'blessed' numbers. For any number that isn't blessed, double math silently rounds to the nearest blessed number. The blessed numbers aren't uniformly divided: Near 1.0 there are very many, as you move away from 1.0 there are fewer and fewer.
So how do we use doubles?
By keeping in mind they have slight errors. This shows up in three ways:
- When printing, always use
String.format
or similar.
If I slightly change our code above to:
double v = 0.0;
for (int i = 0; i < 10; i++) v += 0.1;
System.out.println(String.format("%.3f", v));
It prints 1.000, because the double we Have (0.99999999999999764 or so) indeed rounds to 1.000 if we explicitly say: Please print with at most 3 decimal places.
It is always incorrect to just print a double, anywhere, unless you explicitly want the errors rendered. Instead, always, always use some rounding as you render.
When comparing, keep in mind the error to, and use 'delta comparison'. Instead of asking 'are a
and b
equal', ask 'is the absolute difference between a
and b
tiny?'. This is how:
double EPSILON = 0.0000001;
double v = 0.0;
for (int i = 0; i < 10; i++) v += 0.1;
System.out.println(Math.abs(v - 1.0) < EPSILON);
That would print the desired true
.
But, you're still playing with the fact that there are inaccuracies.
perfection and BigDecimal
BigDecimal represents numbers perfectly and in decimal. The downside is, they are incredibly slow. Fortunately, computers are incredibly fast, so, that usually does not matter.
They also have all the downsides of the math itself: If you try to divide 1 by 3 in BigDecimal math, it throws an exception unless you tell it to round, which.. just gets you that rounding again. At least its controlled, but still.
BigDecimal a = new BigDecimal("12.69");
BigDecimal b = new BigDecimal("15.99");
BigDecimal c = a.add(b);
System.out.println(c);
Often if you're trying to model something that has a distinct atomic unit, it's better to just store those atoms, in a long
. For example, for most circumstances, the best way to store money is in cents. To store the bill total for 2 articles, one of which costs €15.99, the other costs €12.69:
long priceA = 1599;
long priceB = 1269;
System.out.println("Bill total: €" + formatCents(priceA + priceB));
works great, where formatCents is from a library or if need be handwritten:
public String formatCents(long v) {
boolean sign = v >= 0;
if (!sign) v = -v;
int wholes = v / 100;
int parts = v % 100;
return String.format("%s%d.%02d", sign ? " " : "-", wholes, parts);
}
Every currency has atomic units. Yen is just yen, dollars and euros have cents, even bitcoin has satoshis.