Price is not a double

Price

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 TypeDefault Value (for fields)
byte0
short0
int0
long0L
float0.0f
double0.0d
char‘\u0000’
String (or any object)null
booleanfalse

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:

numbers

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();
}

Test on ideone.

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 ^^  
}

Test on ideone.

Lesson to learn: price is not a double. Use precise numeric types when you’re working with it.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.