12. nov. 2009

DecimalFormat and rounding off

Why does DecimalFormat round off wrong?


I have this nice method that can format a double.
You may also specify the number of decimals.

public static String formatNumber(double number, int decimals) {
String pattern = "###,##0";
if (decimals > 0) {
pattern += "." ;
// Add the wanted number of decimals to the pattern
for (int i = 0; i < decimals; i++) {
pattern += "0";
}
}

// Get the English version of the decimalformatter
DecimalFormat df = (DecimalFormat) NumberFormat.getInstance(
Locale.ENGLISH);
df.applyPattern(pattern);

return df.format(number);
}


Now I want to run it:

public static void main(String[] args) {
testNumber(1.5, 0, "2");
testNumber(2.5, 0, "3");
testNumber(2.15, 1, "2.2");
testNumber(2.25, 1, "2.3");
}
public static void testNumber(double number, int decimals,
String expected) {
String formatted = formatNumber(number, decimals);
System.out.print("Testing " + number + ", decimals " +
decimals + ", Actual = " + formatted);
if (formatted.equals(expected)) {
System.out.println(" Expected = " + expected);
} else {
System.out.println(" Expected = " + expected +
" <- Not the same!");
}
}


The outcome is:

Testing 1.5, decimals 0, Actual = 2 Expected = 2
Testing 2.5, decimals 0, Actual = 2 Expected = 3 <- Not the same!
Testing 2.15, decimals 1, Actual = 2.2 Expected = 2.2
Testing 2.25, decimals 1, Actual = 2.2 Expected = 2.3 <- Not the same!


What???
It looks like it rounds off completely randomly.
If you look closer to the documentation, you'll find that DecimalFormat uses ROUND_HALF_EVEN.
This means that "Rounding mode to round towards the 'nearest neighbor' unless both neighbors are equidistant, in which case, round towards the even neighbor. "

But I don't want that, I want it to round up where the fraction is >= 0.5, which is ROUND_HALF_UP, but it seems that you can't change the way DecimalFormat is rounding off, so I've found a way to do it.

If you move the fraction, so the rounding off allways is at the first decimal, you can use Math.round which uses ROUND_HALF_UP, and then set back the correct position for the fraction.

Something like:

double pow = Math.pow(10, decimals);
double numberPowed = number * pow;
number = Math.round(numberPowed) / pow;


Now we can format it using DecimalFormat

But we're not yet homefree, because Math.round is returning a Long, which can't hold big numbers like doubles, so we can only do this trick if the number don't get too big!

The complete method now looks like this:

public static String formatNumber(double number, int decimals) {
double pow = Math.pow(10, decimals);
double numberPowed = number * pow;
// Only do it if the number is not too big
// Math.round returns the neastest Long, so it must fit
// inside a long
if (numberPowed < Long.MAX_VALUE) {
number = Math.round(numberPowed) / pow;
}

String pattern = "###,##0";
if (decimals > 0) {
pattern += "." ;
// Add the wanted number of decimals to the pattern
for (int i = 0; i < decimals; i++) {
pattern += "0";
}
}

// Get the English version of the decimalformatter
DecimalFormat df = (DecimalFormat) NumberFormat.getInstance(
Locale.ENGLISH);
df.applyPattern(pattern);

return df.format(number);
}


And we'll get the right outcome:

Testing 1.5, decimals 0, Actual = 2 Expected = 2
Testing 2.5, decimals 0, Actual = 3 Expected = 3
Testing 2.15, decimals 1, Actual = 2.2 Expected = 2.2
Testing 2.25, decimals 1, Actual = 2.3 Expected = 2.3