Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why this weird output with truncate and BigDecimal?

I am calling a truncate method to truncate the double value so that there should be a single digit after decimal (without rounding off),
For Ex. truncate(123.48574) = 123.4.

My truncate method is something like this

public double truncate (double x) {
    long y = (long) (x * 10);
    double z = (double) (y / 10);
    return z;
}

Its working just fine for almost all the values except for this weird output.

double d = 0.787456;
d = truncate(d + 0.1); //gives 0.8 as expected. Okay.

But,

double d = 0.7;
d = truncate(d + 0.1); //should also give 0.8. But its giving 0.7 only. 
                       //Strange I don't know why?

Infact it works fine for all other 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, - , 0.8, 0.9
I mean for example,

double d = 0.8;
d = truncate(d + 0.1); //gives 0.9 as expected

I tried it with BigDecimal too. But the same. No change. Here is the code for that.

double d = 0.7;
BigDecimal a = new BigDecimal(d + 0.1);
BigDecimal floored = a.setScale(1, BigDecimal.ROUND_DOWN);
double d1 = floored.doubleValue();
System.out.println(d1); //Still gives 0.7

And again the real fact is that it works fine with Math.round.

public double roundUp1d (double d) {
    return Math.round(d * 10.0) / 10.0;
}

So if I call roundUp1d(0.7 + 0.1), it gives 0.8 as expected. But I don't want the values to be rounded off so I can't use this.

Whats the problem with 0.7 ?

like image 800
user1612078 Avatar asked Oct 11 '14 09:10

user1612078


Video Answer


1 Answers

(If you are not interested in theory, scroll to end, theres the fix for your code)

The reason is quite simple: As you know the binary system only supports 0s and 1s

So, let's look at your values, and what they are in binary representation:

0.1 - 0.0001100110011001100110011001100110011001100110011001101
0.2 - 0.001100110011001100110011001100110011001100110011001101
0.3 - 0.010011001100110011001100110011001100110011001100110011
0.4 - 0.01100110011001100110011001100110011001100110011001101
0.5 - 0.1
0.6 - 0.10011001100110011001100110011001100110011001100110011
0.7 - 0.1011001100110011001100110011001100110011001100110011
0.8 - 0.1100110011001100110011001100110011001100110011001101
0.9 - 0.11100110011001100110011001100110011001100110011001101

What does that mean? 0.1 is a 10th of 1. No big deal in the decimal system, simply shift the separator one position. But in binary you cannot express 0.1 - cause every shift of the decimal sign equals *2 or /2 - depending on the direction. (And 10 cannot be divided into X shifts of 2)

For values you want to divide by multiples of 2 - you get an EXACT result:

1/2 - 0.1
1/4 - 0.01
1/8 - 0.001
1/16- 0.0001
and so on.

Therefore trying to calculate a /10 is an infinite long result, which is truncated when the value runs out of bits.

This said, it is a limitation of the way computers work, that such a value can never be stored with full precision.

Site Note: This "fact" has been ignored with the Patriot System causing it to become unusable after some hours of operating time, see here: http://sydney.edu.au/engineering/it/~alum/patriot_bug.html


But why does it work for every thing but 0.7 + 0.1 - you might ask

If you test your code with 0.8 - it works - but not with 0.7 + 0.1.

Again, in binary both values are already imprecise. If you sum up both values, the result is even more imprecise, leading to a wrong result:

If you sum up 0.7 and 0.1 (after the decimal separator) you get this:

  0.101100110011001100110011001100110011001100110011001 1000
+ 0.000110011001100110011001100110011001100110011001100 1101
  ---------------------------------------------------------
  0.110011001100110011001100110011001100110011001100110 0101     

But 0.8 would be

  0.110011001100110011001100110011001100110011001100110 1000

Compare the last 4 bits and note, that the resulting "0.8" of the ADDITION is smaller than if you would convert 0.8 to binary directly.

Guess what:

System.out.println(0.7 + 0.1 == 0.8); //returns false

When working with numbers you should set yourself a limit of precision - and ALWAYS round numbers accordingly to avoid such errors (not truncate!):

 //compare doubles with 3 decimals
 System.out.println((lim(0.7, 3) + lim(0.1, 3)) == lim(0.8, 3)); //true

 public static long lim(double d, int t){
     return Math.round(d*10*t);
 }

To have your code fixed: round it to 4 digits, before truncating after the first digit:

public static double truncate(double x){
   long y = (long)((Math.round(x*10000)/10000.0)*10);
   double z = (double)y/10;
   return z;
}

System.out.println(truncate(0.7+0.1)); //0.8
System.out.println(truncate(0.8)); //0.8

This will still truncate as desired but ensures, that a 0.69999 will be rounded to 0.7 before truncating it. You can set the precision as required for your application. 10, 20 ,30, 40 digits?

Other values will still remain correct, because something like 0.58999 will just be rounded to 0.59 - so still truncated as 0.5 and not rounded to 0.6

like image 60
dognose Avatar answered Nov 10 '22 10:11

dognose