When a software developer sees a requirement to store prices and do price calculations he has to decide which type he’s going to use.
Data Type | Default Value (for fields) |
---|---|
byte | 0 |
short | 0 |
int | 0 |
long | 0L |
float | 0.0f |
double | 0.0d |
char | ‘\u0000’ |
String (or any object) | null |
boolean | false |
Since price is usually explained in Euros (Dollars, Hryvnas, Bitcoins) it has an integer part and a decimal part. So the natural choice is to skip int
and take double
or float
.
This, however, has some consequences that are often learned the hard way. If you already forgot CS 101 here is a reminder what is wrong with doubles.
If you draw possible value spaces, they will look like this:
so the space of doubles is much bigger than the space of prices. That means when you do calculations with doubles you may get something that is not a price.
Example
First, an easy example that starts challenging your belief in math:
System.out.println(8.97); System.out.println(8.97 + 4.95 - 4.95);
The result is not as you have expected:
8.97 8.970000000000002
This one is pretty clear but what if your task is to make some more calculations. Suppose you have the following problem statement:
Calculate discount between old and new price and show as a percentage without decimal signs.
Additional requirement: always round percentage down.
Solution that is about almost correct
public int getPriceDifferenceInPercentage(Double oldPrice, Double price) { return new BigDecimal((oldPrice - price) * 100 / oldPrice) .setScale(0, BigDecimal.ROUND_DOWN).intValue(); }
The code looks innocent enough. We do discount calculation, convert to BigDecimal, round down. What could possibly go wrong?
Well, it turns out 14.95 – 8.97 in doubles is not 5.98 but 5.979999999999999 and it goes down from there because we round it down.
Correct solutions
The first solution would be to immediately convert the price from double
to BigDecimal
with scale 2 and do calcluation is BigDecimals. This makes the code a little bulky but solves the problem:
public int getPriceDifferenceInPercentage(Double oldPrice, Double price) { // Convert doubles to decimals with the scale of 2 to make a precise calculation. BigDecimal oldPriceDecimal = new BigDecimal(oldPrice) .setScale(2, BigDecimal.ROUND_HALF_UP); BigDecimal priceDecimal = new BigDecimal(price) .setScale(2, BigDecimal.ROUND_HALF_UP); BigDecimal percentage = oldPriceDecimal .subtract(priceDecimal) .divide(oldPriceDecimal, 2, BigDecimal.ROUND_DOWN); return percentage.multiply(new BigDecimal(100)).intValue(); }
The second approach is to do all the computations in doubles but do rounding with 1 decimal digit before converting it to the end result. This will often be enough to eliminate the imprecision you got during calculations.
public int getPriceDifferenceInPercentage(Double oldPrice, Double price) { return new BigDecimal((oldPrice - price) * 100 / oldPrice).setScale(1, BigDecimal.ROUND_HALF_UP).intValue(); // ^^ fix imprecision problem ^^ }
Lesson to learn: price is not a double. Use precise numeric types when you’re working with it.