2

-- Short Solution --

Thank you for all the informative replies... @PetrJaneček @Matt pointed me in the right direction. I agree I had to brush up on my understanding of floating point arithmetic; but the difference in calling the constructor vs double's canonical string representation by using valueOf solved my problem.

private static Double sampleRound(Double value, int places) {
    BigDecimal bd1 = BigDecimal.valueOf(value);
    bd1 = bd1.setScale(places, RoundingMode.HALF_UP);

    return bd1.doubleValue();
}

As provided by @PetrJaneček a useful video:

youtu.be/wbp-3BJWsU8?t=246

-- Original Question --

I've come across a really odd one regarding rounding and just want to understand where I'm going wrong.

The function is:

private static Double sampleRound(Double value, int places) {
    BigDecimal bd1 = new BigDecimal(value);
    bd1 = bd1.setScale(places, RoundingMode.HALF_UP);
    
    return bd1.doubleValue();
}

If I pass the value "1942.945", I would expect it to output "1942.95", but instead I get "1942.94". However if I pass "1942.9451" I get "1942.95". So ok fair then let's assume I carry over this logic to 3 decimal places instead. So if I pass "1942.9445" I expect "1942.944", but instead I get "1942.945", how and why? The logic seems broken to me?

sampleRound(1942.945, 2) -> 1942.94

[Does NOT make sense, should be 1942.95]

sampleRound(1942.9450000000001, 2)-> 1942.95

*Edit: I've also passed this (1 additional zero), again I understand data types and it's constraints, I guess the point I'm making is the fraction .005 isn't being seen as a half up >= 5: in accordance with the documentation of java "discarded fraction is ≥ 0.5;" it's essentially seeing it as > 0.5 not >= 0.5 *

sampleRound(1942.94500000000001, 2) -> 1942.94

[Then this should be equivalent to the top]

Ok but then fine I can deal with the above logic, but as soon as I do 3 decimal places:

sampleRound(1942.9445, 3) -> 1942.945

[Does NOT make sense, in accordance with top logic should be 19423.944]. Then just like I mentioned in the above logic where franaction .005 isn't being seen as a >= 0.5 then .0005 should be treated the same way?

I hope by description makes sense, but I'm stumped?

Regards,

Joe
  • 23
  • 3
  • Does this answer your question? [DecimalFormat with RoundingMode.HALF\_UP](https://stackoverflow.com/questions/53740617/decimalformat-with-roundingmode-half-up) – ollitietavainen Jun 15 '22 at 09:16
  • 1
    I'm sorry I cannot post a complete answer as I'm on my phone, the problem is in `new BigDecimal(value)`. You should use `BigDecimal.valueOf(value)` instead. For more information see the javadocs of both methods, they try to explain it. Or see the first puzzler of this video which explains it, too: https://youtu.be/wbp-3BJWsU8?t=246 – Petr Janeček Jun 15 '22 at 09:17
  • 2
    The real point here: you seem to not understand the limitations of floating point numbers. There is a HUGE difference whether you have a STRING "1942.945" or whether you do a `new Double(1942.945)`. So, before you even think about ROUNDING, you have to understand how to properly go about floating point literals/numbers in general. When you round something that **is not what you think it is**, then wondering about the results of rounding is meaningless. – GhostCat Jun 15 '22 at 09:21
  • 2
    Check it. `System.out.println( new BigDecimal( 1942.945 ) )` results in "1942.944999999999936335370875895023345947265625" So you're trying to solve two problems here. "What is the rounding behavior of BigDecimal" and "How are double literals converted to actual doubles" – matt Jun 15 '22 at 09:24
  • 1
    (For the record I do not fully agree with this question being closed as a simple duplicate. Yes, it's floating point numbers accuracy, but even after understanding that, the trick here is to use a different `BigDecimal` factory method, and that fixes the problem. It's also a non-trivial fix as the API is borked forever, so I think it warrants its own answer.) – Petr Janeček Jun 15 '22 at 09:30
  • @PetrJaneček: The question has been reopened. – Gilbert Le Blanc Jun 15 '22 at 09:32

2 Answers2

2

As suggested in a comment by @PetrJaneček You can get around this issue by using BigDecimal.valueOf that will create a BigDecimal "using the double's canonical string representation provided by the Double.toString(double) method.".

The BigDecimal constructor, new BigDecimal(double) creates a BigDecimal based on the exact value of the the double provided.

double v =  1942.945;
System.out.println( Double.toString(v) );
System.out.println( new BigDecimal(v) );
System.out.println( BigDecimal.valueOf(v) );

1942.945
1942.944999999999936335370875895023345947265625
1942.945

What we see, is first the "canonical string" of v which is what you expect, but the exact value is not "1942.945" it is slightly less. We can see that when we use the BigDecimal constructor and get the exact value back.

For the rest of your examples, we just have to look at the exact representation.

System.out.println( new BigDecimal( 1942.9450000000001 ) );

1942.94500000000016370904631912708282470703125

System.out.println( new BigDecimal( 1942.94500000000001 ) );

1942.944999999999936335370875895023345947265625

System.out.println( new BigDecimal( 1942.9445 ) );

1942.94450000000006184563972055912017822265625

From that, I think it is clear why your rounding behavior is the way it is.

matt
  • 10,892
  • 3
  • 22
  • 34
2
String v = "1942.945";
System.out.println(new BigDecimal(v).toPlainString());

As double has no precision, scale, 1942.945 is just an approximation and might be nearer to 1942.9449999983; in fact those two doubles might be identical. Then setting the scale starts off with an imprecise approximation.

So use a String from which BigDecimal immediately might deduce its precision/scale.

Joop Eggen
  • 107,315
  • 7
  • 83
  • 138