2

It looks like BigDecimal.setScale truncates to the scale+1 decimal position and then rounds based on that decimal only. Is this normal or there is a clean way to apply the rounding mode to every single decimal?

This outputs: 0.0697 (this is NOT the rounding mode they taught me at school)

double d = 0.06974999999;
BigDecimal bd = BigDecimal.valueOf(d);
bd = bd.setScale(4, RoundingMode.HALF_UP);
System.out.println(bd);


This outputs: 0.0698 (this is the rounding mode they taught me at school)

double d = 0.0697444445;
BigDecimal bd = BigDecimal.valueOf(d);
int scale = bd.scale();
while (4 < scale) {
    bd = bd.setScale(--scale, RoundingMode.HALF_UP);
}
System.out.println(bd);

EDITED After reading some answers, I realized I messed everything up. I was a bit frustrated when I wrote my question. So, I'm going to rewrite the question cause, even though the answers helped me a lot, I still need some advice.

The problem is: I need to round 0.06974999999 to 0.0698, that's because I know those many decimals in fact are meant to be 0.6975 (A rounding error in a place not under my control).
So i've been playing around with a kind of "double rounding" which performs the rounding in two steps: first round to some higher precision, then round to the precision needed.
(Here is where I messed up because I thought a loop for every decimal place would be safer). The thing is that I don't know which higher precision to round to in the first step (I'm using the number of decimals-1). Also I don't know if I could find some unexpected results for other cases.
Here is the first way I discarded in favour of the one with the loop, which now looks a lot better after reading your answers:

public static BigDecimal getBigDecimal(double value, int decimals) {
    BigDecimal bd = BigDecimal.valueOf(value);
    int scale = bd.scale();
    if (scale - decimals > 1) {
        bd = bd.setScale(scale - 1, RoundingMode.HALF_UP);
    }
    return bd.setScale(decimals, roundingMode.HALF_UP);
}

These prints the following results:
0.0697444445 = 0.0697
0.0697499994 = 0.0697
0.0697499995 = 0.0698
0.0697499999 = 0.0698
0.0697444445 = 0.069744445 // rounded to 9 decimals
0.0697499999 = 0.069750000 // rounded to 9 decimals
0.069749 = 0.0698

The questions now are if there is a better way to do this (maybe a different rounding mode)? and if this is safe to use as a general rounding method?
I need to round many values and having to choose at runtime between this and the standard aproach depending on the kind of numbers I receive seems to be really complex.

Thanks again for your time.

4 Answers4

0

If you need to bias your rounding you can add a small factor.

e.g. to round up to 6 decimal places.

double d = 
double rounded = (long) (d * 1000000 + 0.5) / 1e6;

to add a small factor you need to decide how much extra you want to give. e.g.

double d = 
double rounded = (long) (d * 1000000 + 0.50000001) / 1e6;

e.g.

public static void main(String... args) throws IOException {
    double d1 = 0.0697499994;
    double r1 = roundTo4places(d1);
    double d2 = 0.0697499995;
    double r2= roundTo4places(d2);
    System.out.println(d1 + " => " + r1);
    System.out.println(d2 + " => " + r2);

}

public static double roundTo4places(double d) {
    return (long) (d * 10000 + 0.500005) / 1e4;
}

prints

0.0697499994 => 0.0697
0.0697499995 => 0.0698

The first one is correct.

0.44444444 ... 44445 rounded as an integer is 0.0

only 0.500000000 ... 000 or more is rounded up to 1.0

There is no rounding mode which will round 0.4 down and 0.45 up.

If you think about it, you want an equal chance that a random number will be rounded up or down. If you sum a large enough number of random numbers, the error created by rounding cancels out.


The half up round is the same as

long n = (long) (d + 0.5);

Your suggested rounding is

long n = (long) (d + 5.0/9);

Random r = new Random(0);
int count = 10000000;

// round using half up.
long total = 0, total2 = 0;
for (int i = 0; i < count; i++) {
    double d = r.nextDouble();
    int rounded = (int) (d + 0.5);
    total += rounded;

    BigDecimal bd = BigDecimal.valueOf(d);
    int scale = bd.scale();
    while (0 < scale) {
        bd = bd.setScale(--scale, RoundingMode.HALF_UP);
    }

    int rounded2 = bd.intValue();
    total2 += rounded2;
}
System.out.printf("The expected total of %,d rounded random values is %,d,%n\tthe actual total was %,d, using the biased rounding %,d%n",
        count, count / 2, total, total2);

prints

The expected total of 10,000,000 rounded random values is 5,000,000, 
    the actual total was 4,999,646, using the biased rounding 5,555,106

http://en.wikipedia.org/wiki/Rounding#Round_half_up

Peter Lawrey
  • 525,659
  • 79
  • 751
  • 1,130
  • Thanks, in fact I need to round to a high precision to "fix" "wrong" values and then round to the needed precission. It will add a positive bias for every number with a four in the precision+1 place and a bunch of nines afterwards (I edited my question with this). – user1206525 Feb 13 '12 at 15:06
  • Thanks. That's what I need to do. I've been playing with Math.nextAfter to see if the scale is lowered. So far it's been working perfectly. I'll see if I can improve it with your advice. – user1206525 Feb 14 '12 at 15:16
0

When you are rounding you look at the value that comes after the last digit you are rounding to, in your first example you are rounding 0.06974999999 to 4 decimal places. So you have 0.0697 then 4999999 (or essentially 697.4999999). As the rounding mode is HALF_UP, 0.499999 is less than 0.5, therefore it is rounded down.

DaveJohnston
  • 10,031
  • 10
  • 54
  • 83
0

If the difference between 0.06974999999 and 0.06975 matters so much, you should have switched to BigDecimals a bit sooner. At the very least, if performance is so important, figure out some way to use longs and ints. Double's and floats are not for people who can tell the difference between 1.0 and 0.999999999999999. When you use them, information gets lost and there's no certain way to recover it.

(This information can seen insignificant, to put it mildly, but if travelling 1,000,000 yards puts you at the top of a cliff, travelling 1,000,001 yards will put you a yard past the top of a cliff. That one last yard matters. And if you loose 1 penny in a billion dollars, you'll be in even worse trouble when the accountants get after you.)

RalphChapin
  • 3,108
  • 16
  • 18
0

What about trying previous and next values to see if they reduce the scale?

public static BigDecimal getBigDecimal(double value) {
    BigDecimal bd = BigDecimal.valueOf(value);
    BigDecimal next = BigDecimal.valueOf(Math.nextAfter(value, Double.POSITIVE_INFINITY));
    if (next.scale() < bd.scale()) {
        return next;
    }
    next = BigDecimal.valueOf(Math.nextAfter(value, Double.NEGATIVE_INFINITY));
    if (next.scale() < bd.scale()) {
        return next;
    }
    return bd;
}

The resulting BigDecimal can then be rounded to the scale needed.
(I can't tell the performance impact of this for a large number of values)