Closed nezda closed 4 years ago
please try
final var decimalArithmetic = Scales.getScaleMetrics(6).getArithmetic(RoundingMode.HALF_UP);
The scale you used (5) meant that the 6th decimal place was truncated
final double input = 0.663125;
final double expected = Precision.round(input, 5, RoundingMode.HALF_UP.ordinal()); // 0.66313
final var decimalArithmetic = Scales.getScaleMetrics(6).getArithmetic(RoundingMode.HALF_UP);
final double actual = decimalArithmetic.toDouble(decimalArithmetic.fromDouble(input)); // 0.663125
assertThat(actual).isEqualTo(expected);
fails differently...
expected: 0.66313
but was : 0.663125
Hi
Short answer is: this is expected behaviour in decimal4j.
The longer answer is an explanation and some alternative ways to round a double with decimal4j.
Explanation
Firstly --- this is a very good question, thanks for stating it here. The problem that Is highlighted here is more broadly the problem of 'rounding a double value' or more generally 'rounding a binary floating point value to a decimal precision'.
To see this let’s look into the input value. If printed to its full precision the value is actually
0.66312499999999996447286321199499070644378662109375
The value can be obtained via
new BigDecimal(0.663125).toPlainString();
Now you need to know that decimal4j looks at the exact value representation of a double value when it is converted — so it produces the correct result when rounding above value down to 0.66312
.
The big question — and source of confusion — is that Java prints this value as
0.663125
and not the huge 50 fraction digit value above. It prints “as many, but only as many, […] digits as are needed to uniquely distinguish the [...] value from adjacent values […]”. This is quite ingenious but can lead to confusion as many values now look just like an exact decimal value when in fact they are not, and it is also not trivial to implement. The documentation for this can be found on the static Double.toString(double) method.
To reiterate, the decimal4j fromDouble(..) conversion methods operate with the binary exact value and not with the string representation of that value.
Alternatives
The most straight forward way to round the above value according to its string representation is to input it as a string:
DecimalArithmetic arith = Scales.getScaleMetrics(5).getArithmetic(RoundingMode.HALF_UP);
arith.toDouble(arith.parse(String.valueOf(0.663125)));
This is not always desirable as it creates a string object and therefore garbage. This can be prevented by using a StringBuilder that is reused:
StringBuilder stringBuilder = new StringBuilder(64);
stringBuilder.setLength(0);
stringBuilder.append(0.663125);
DecimalArithmetic arith = Scales.getScaleMetrics(5).getArithmetic(RoundingMode.HALF_UP);
arith.toDouble(arith.parse(stringBuilder, 0, stringBuilder.length()));
A big drawback of this is currently that decimal4j does not support scientific notation, so it will not work for all double values. Parsing scientifically printed double values has been requested here and may be supported in future decimal4j versions.
A last option to round a value more intuitively is to use a 2 step rounding approach:
For our example this would look for instance like this:
DecimalArithmetic arith8 = Scales.getScaleMetrics(8).getRoundingHalfEvenArithmetic();
DecimalArithmetic arith5 = Scales.getScaleMetrics(5).getArithmetic(RoundingMode.HALF_UP);
long unscaled8 = arith8.fromDouble(0.663125);//rounding off the noise
long unscaled5 = arith5.fromUnscaled(unscaled8, arith8.getScale());//actual rounding happens here!
double rounded = arith5.toDouble(unscaled5);
The scale for noise rounding should be at least 1 more than the actual precision, our favourite choice is 3 extra digits. Note however that the extra precision means a lower maximum value and one should be careful not to cause overflows!
Source Code
All code above is available as a unit test here:
This test comparing rounding of a value with decimal4j 1.0.3 vs commons-math3 3.6.1 fails
output: