BigDecimals
Since our business didn’t want us to use floating points any more, but something more precise, we decided to start using BigDecimals instead of doubles.
Our calculations are actually fairly straight forward. Something like
private double calc(double x, double y, double previous){ return previous * (x/y); }
So I converted the code to BigDecimal (remark x and y are coming from somewhere else so they are still double (for now))
private BigDecimal calc(double x, double y, BigDecimal previous){ return previous.multiply(new BigDecimal(x).divide(new BigDecimal(y)); }
“Ah quite easy” you would think, but when running this code I sometimes get a java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
This happens when, for example, we use x = 2 and y = 3. 2/3 is 0.666666666…. (endless). So BigDecimal doesn’t know what to do and gives a runTimeException.
This means we should actually use some rounding method.
private BigDecimal calc(double x, double y, BigDecimal previous){ return new BigDecimal(x).divide(new BigDecimal(y),RoundingMode.HALF_UP).multiply(previous); }
So now we don’t get the ArithmeticException any more but the result with 2,3 and 1 now gives 1, while we expect somthing like 0.6666666666…7
2/3 is 0.6… rounded this is 1. So this is still not what we wanted.
What you can do now is:
- define the scale you want to have after division (amount of decimals after comma)
- Use a MathContext (and basically defining percision and rounding mode with that)
Some examples:
| 2/3 | 2000/3 | 0.2/0.3 | |
| scale = 5 | 0.66667 | 666.66667 | 0.66667 |
| mathContext (presision=5) | 0.66667 | 666.67 | 0.66667 |
| only rounding | 1 | 667 | 0.666666666666666728345723590286476750563174908436298048 |
When using only the round we discover another problem: when converting double to BigDecimal the BigDecimal is not completely the number we meant to have. Because of this 0.2/0.3 seems quite exact but it is actually wrong. This is caused by the fact that 0.3 is actually 0.299999999999999988897769753748… this is caused by the nature of double values). So to be correct, we should/can actually also round the original starting numbers.
Since we also have a limited amount of numbers after the comma which are interesting for us, I ended up to convert every number before using it to a number with a certain scale. Then we don’t have to care any more about this scale or precision when multiplying and dividing (however still about the rounding method).
So finally I got this method:
private static BigDecimal calc(double x, double y, BigDecimal previous){ BigDecimal bigX = new BigDecimal(x).setScale(5, RoundingMode.HALF_UP); BigDecimal bigY = new BigDecimal(y).setScale(5, RoundingMode.HALF_UP); return bigX.divide(bigY,RoundingMode.HALF_UP).multiply(previous); }
One last thing:
new BigDecimal(1).equals(new BigDecimal(1).setScale(10)) returns false.
Basically we are comparing 1 and 1.0000000000 here. These two are not the same!