0

In Android Studio I had problems with calculating invoice totals because of the way java rounds. I know there are a lot of explanations, but many recommend methods that don't return reliable results. For example:
1. Math.round((double)4.715 * (double)100 ) / (double)100 = 4.72 (expected 4.72)
2. Math.round((double)4.725 * (double)100 ) / (double)100 = 4.72 (but expected 4.73)

You can't put this code in an app for a client who calculates invoices. Because , in my case for example, the same invoice is calculated in another system and the result is different, meaning 4.72 respectively 4.73
I know that a double can't be represented exactly and the decimals are different than what we see. But we need a method that returns results as we expect.

Another example would be:
1. java.math.BigDecimal.valueOf(4.715).setScale(2,java.math.BigDecimal.ROUND_HALF_UP).doubleValue() = 4.72
2. new java.math.BigDecimal(4.715).setScale(2,java.math.BigDecimal.ROUND_HALF_UP).doubleValue() = 4.71
3. new java.math.BigDecimal( String.valueOf(4.715) ).setScale(2,java.math.BigDecimal.ROUND_HALF_UP).doubleValue() = 4.72

I think all these aspects could be well explained in Java documentation, but they should indicate a certain method for calculating rounds, a reliable method which returns results as we expected. I only wanted to round to 2 decimales.

In conclusion, which I hope will help some of the beginners, I think that the following method would return stable and good results:
java.math.BigDecimal.valueOf(4.715).setScale(2,java.math.BigDecimal.ROUND_HALF_UP).doubleValue() = 4.72

Or, at least, this is my observation after 3+ years of intensive usage of an app (500+ users every working day).
All practical explanations for these above are very welcome, so we can better understand how to avoid unexpected results.

mihai71
  • 347
  • 4
  • 9
  • Your first 1 & 2 are wrong. – matt Jul 27 '17 at 14:04
  • If you use BigDecimals, then initialize them with strings, not with doubles or floats. So `x = new java.math.BigDecimal("4.715").setScale(2, BigDecimal.ROUND_HALF_UP);` Do not use double at all, so don't do a `.doubleValue()` on the BigDecimal either. – Rudy Velthuis Jul 27 '17 at 14:19
  • matt, 1 and 2 may be wrong, but it's exactly the way they are from debugger. and this was a problem i faced with in the field. i only wanted to reveal these aspects for a beginner because documentation is not applied to practical examples but only explained from mathematical point of view. – mihai71 Jul 31 '17 at 13:41
  • @ Rudy Velthuis. please provide an example for the case : 4.11547 - 3 thanks – mihai71 May 17 '19 at 06:44

2 Answers2

1

For the BigDecimal examples the javadoc explains the difference.

BigDecimal(double value) ... is the exact decimal representation of the double's binary floating-point value.

Which we can check, by just printing the value.

System.out.println(new BigDecimal(4.715));
#4.714999999999999857891452847979962825775146484375

Which is barely less than 4.715, but enough such that it gets rounded down.

BigDecimal.valueOf(double value) uses the string representation of the double value from Double.toString(double value) which has quite a few rules.

System.out.println(Double.toString(4.715));
#4.715

The safest best is to just use BigDecimal for your calculations. Especially when dealing with arithmetic operations. It isn't clear when the value will switch to needing more decimal places. For example:

double d = 4.11547;

BigDecimal bd = BigDecimal.valueOf(d);

I this case, the string representation of d is 4.11547, so BigDecimal.valueOf returns the value that is written.

BigDecimal s1 = BigDecimal.valueOf(d-3);
BigDecimal s2 = bd.subtract(new BigDecimal(3));

It might be surprising to find s1 and s2 are different since '3' doesn't get rounded.

System.out.println(s1 + ", " + s2);
#1.1154700000000002, 1.11547

So it is best to use the BigDecimal methods for arithmetic too.

matt
  • 10,892
  • 3
  • 22
  • 34
  • 1
    @mihai71 Your example isn't using BigDecimal to do your arithmatic. eg. `BigDecimal d = new BigDecimal("4.11547")` Then you can use `d.subtract(new BigDecimal(3));` – matt May 17 '19 at 06:47
  • my answer was (but i deleted it before you posted yours ...) it's not safest. this is an example that proves what i'm saying: 1. java.math.BigDecimal.valueOf(4.11547 - 3) = 1.1154700000000002 2. java.math.BigDecimal.valueOf(4.11547 - 3).setScale(8, java.math.BigDecimal.ROUND_HALF_UP).doubleValue() = 1.11547 3.java.math.BigDecimal.valueOf(4.11547 - 3).setScale(20, java.math.BigDecimal.ROUND_HALF_UP).doubleValue() = 1.1154700000000002 Your answer is good for my question. Thanks ! – mihai71 May 17 '19 at 06:55
  • matt, please write an answer and explain that for arithmetical operations we should use the functions of BigDecimal : subtract, divide, multiply, add . That should be marked as the correct answer. Thanks – mihai71 May 17 '19 at 07:52
  • @mihai71 I updated the answer to include an arithmetic example. – matt May 17 '19 at 08:30
0

It's in the nature of binary floating point data types, like float and double in Java. double actually states this in his name. It has double precision compared to float - but it is not an exact representation of a decimal number.

Just adding some simplified math detail to the existing answer. This might help understand the seemingly strange behavior of Java floating point numbers.

The root cause of the problem is binary vs. decimal representation of numbers. You use decimal representation when you use a floating point literal in your code, e.g. double d = 1.5; or a String value, e.g. String s = "1.5";.

But the JVM uses a binary representation of the number. The mapping for integer numbers is easy (d for decimal, b for binary): 1 = 1b, 2d = 10b, 3d = 11b .... There is no issue with integer numbers. int and long work just the way you would expect. Except for the overflow...

But for floating point numbers things are different: 0.5d = 0.1b, 0.25d = 0.01b, 0.125d = 0.001b.... You are only able to add values for the series 1/2, 1/4, 1/8, 1/16... Now imagine, you want to show 0.1d in binary representation.

You start with 0.0001b = 0.0625d, which is the first binary value that is still less than 0.1d. 0.0375d remaining. You continue, and the next close value is 0.03125d, and so on. You'll acutally never get to exactly 0.1d. All you get is an approximation. You'll get closer and closer.

Consider the following piece of code. It does the approximation with the help of BigDecimal values:

public void approximate0dot1() {
    BigDecimal destVal = new BigDecimal("0.1");
    BigDecimal curVal = new BigDecimal("0");
    BigDecimal inc = new BigDecimal("1");
    BigDecimal div = new BigDecimal("2");
    for (int step = 0; step < 20; step++) {
        BigDecimal probeVal = curVal.add(inc);
        int cmp = probeVal.compareTo(destVal);
        if (cmp == 0) {
            break;
        } else if (cmp < 0) {
            curVal = probeVal;
            System.out.format("Added: %s, current value: %s, remaining: %s\n", inc, curVal, destVal.subtract(curVal));
        }
        inc = inc.divide(div);
    }
    System.out.format("Final value: %s\n", curVal);
}

And the output is:

Added: 0.0625, current value: 0.0625, remaining: 0.0375
Added: 0.03125, current value: 0.09375, remaining: 0.00625
Added: 0.00390625, current value: 0.09765625, remaining: 0.00234375
Added: 0.001953125, current value: 0.099609375, remaining: 0.000390625
Added: 0.000244140625, current value: 0.099853515625, remaining: 0.000146484375
Added: 0.0001220703125, current value: 0.0999755859375, remaining: 0.0000244140625
Added: 0.0000152587890625, current value: 0.0999908447265625, remaining: 0.0000091552734375
Added: 0.00000762939453125, current value: 0.09999847412109375, remaining: 0.00000152587890625
Final value: 0.09999847412109375

This is just a basic example to show the underlying issue. Internally, the JVM obviously does some optimization to get the best possible approximation for the available 64-bit precision, e.g.

System.out.println(new BigDecimal(0.1));
// prints 0.1000000000000000055511151231257827021181583404541015625

But this example shows, that there is already a rounding issue with decimal numbers a simple as a constant with the decimal value 0.1.

Some basic tips:

  • Do not use BigDecimal(double) constructor if you need exact decimal math, use BigDecimal(String) instead. Bad: new BigDecimal(0.1), Good: new BigDecimal("0.1")
  • Do not mix BigDecimal and floating point arithmetic, e.g. do not extract double value for further calculations like new BigDecimal("0.1").doubleValue();
Jochen Reinhardt
  • 833
  • 5
  • 14