11

I am translating .NET code to Java and ran into precision not matching issue.

.NET code:

private decimal roundToPrecision(decimal number, decimal roundPrecision)
{
    if (roundPrecision == 0)
        return number;
    decimal numberDecimalMultiplier = Math.Round(number / roundPrecision, MidpointRounding.AwayFromZero);
    return numberDecimalMultiplier * roundPrecision;
}

Calling roundToPrecision(8.7250, 0.05); function in the code above gives me 8.75 which is expected.

The conversion/translation of the function to Java is as follows. I din't find exact Math.Round option.

Java code:

public double roundToPrecision(double number, double roundPrecision) {
    if (roundPrecision == 0)
        return number;
    int len = Double.toString(roundPrecision).split("\\.")[1].length();
    double divisor = 0d;
    switch (len) {
        case 1:
            divisor = 10d;
            break;
        case 2:
            divisor = 100d;
            break;
        case 3:
            divisor = 1000d;
            break;
        case 4:
            divisor = 10000d;
            break;
    }
    double numberDecimalMultiplier = Math.round(number / roundPrecision);
    double res = numberDecimalMultiplier * roundPrecision;
    return Math.round(res * divisor) / divisor;
}

Calling roundToPrecision(8.7250, 0.05); in the Java code gives me 8.7and this is not correct.

I even tried modifying code with BigDecimal as follows in Java using the reference here C# Double Rounding but have no luck.

public double roundToPrecision(double number, double roundPrecision) {
    if (roundPrecision == 0)
        return number;
    int len = Double.toString(roundPrecision).split("\\.")[1].length();
    double divisor = 0d;
    switch (len) {
        case 1:
            divisor = 10d;
            break;
        case 2:
            divisor = 100d;
            break;
        case 3:
            divisor = 1000d;
            break;
        case 4:
            divisor = 10000d;
            break;
    }
    BigDecimal b = new BigDecimal(number / roundPrecision);
    b = b.setScale(len,BigDecimal.ROUND_UP);
    double numberDecimalMultiplier = Math.round(b.doubleValue());
    double res = numberDecimalMultiplier * roundPrecision;
    return Math.round(res * divisor) / divisor;
}

Please guide me for what I need to do to fix this.

Here are couple of scenarios to try out.

  • number = 10.05; precision = .1; expected = 10.1;
  • number = 10.12; precision = .01; expected = 10.12;
  • number = 8.7250; precision = 0.05; expected = 8.75;
  • number = 10.999; precision = 2; expected = 10;
  • number = 6.174999999999999; precision = 0.05; expected = 6.20;

Note: I have over 60 thousand numbers and precision can vary from 1 decimal to 4 decimal places. The output of .NET should match exactly to Java.

Dinesh Devkota
  • 1,417
  • 2
  • 18
  • 45

2 Answers2

7

The problem comes from how doubles vs decimals are stored and represented in memory. See these links for more specifics: Doubles Decimals

Let's take a look at how they each work in your code. Using doubles, with arguments of 8.725 and 0.05. number / roundPrecision gives 174.499..., since doubles aren't able to exactly represent 174.5. With decimals number / roundPrecision gives 174.5, decimals are able to represent this exactly. So then when 174.499... gets rounded, it gets rounded down to 174 instead of 175.

Using BigDecimal is a step in the right direction. There is an issue with how it's being used in your code however. The problem comes when you're creating the BigDecimal value.

BigDecimal b = new BigDecimal(number / roundPrecision);

The BigDecimal is being created from a double, so the imprecision is already there. If you're able to create the BigDecimal arguments from a string that would be much better.

public static BigDecimal roundToPrecision(BigDecimal number, BigDecimal roundPrecision) {
    if (roundPrecision.signum() == 0)
        return number;
    BigDecimal numberDecimalMultiplier = number.divide(roundPrecision, RoundingMode.HALF_DOWN).setScale(0, RoundingMode.HALF_UP);
    return numberDecimalMultiplier.multiply(roundPrecision);
}


BigDecimal n = new BigDecimal("-8.7250");
BigDecimal p = new BigDecimal("0.05");
BigDecimal r = roundToPrecision(n, p);

If the function must take in and return doubles:

public static double roundToPrecision(double number, double roundPrecision)
{
    BigDecimal numberBig = new BigDecimal(number).
            setScale(10, BigDecimal.ROUND_HALF_UP);
    BigDecimal roundPrecisionBig = BigDecimal.valueOf(roundPrecision);
    if (roundPrecisionBig.signum() == 0)
        return number;
    BigDecimal numberDecimalMultiplier = numberBig.divide(roundPrecisionBig, RoundingMode.HALF_DOWN).setScale(0, RoundingMode.HALF_UP);
    return numberDecimalMultiplier.multiply(roundPrecisionBig).doubleValue();
}

Keep in mind that doubles cannot exactly represent the same values which decimals can. So the function returning a double cannot have the exact output as the original C# function which returns decimals.

gunnerone
  • 3,566
  • 2
  • 14
  • 18
  • Thank for the answer. However, I have already tried this. Try senarios 1 and 2 (I have updated the question). They don't give expected result in your implementation. – Dinesh Devkota Jan 14 '17 at 02:57
  • Your solution throws `java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result` on `BigDecimal numberDecimalMultiplier = number.divide(roundPrecision).setScale(0, RoundingMode.CEILING);` when number is 10.0 and precision is 0.1 – Dinesh Devkota Jan 17 '17 at 17:41
  • What are your arguments when it throws the exception? – gunnerone Jan 17 '17 at 17:42
  • when number is 10.0 and precision is 0.1 – Dinesh Devkota Jan 17 '17 at 17:43
  • This case fails `number = 10.025d; precision = .05d; expected = 10.05d;` – Dinesh Devkota Jan 17 '17 at 19:01
  • The case works for me. How are you creating the function arguments? It looks like they're coming from doubles? – gunnerone Jan 17 '17 at 19:10
  • I cannot modify the argument but rather created double right after passing value to function. `roundToPrecision (10.05, .1)` this results to `10.100000000000001` while expectation is `10.1` – Dinesh Devkota Jan 17 '17 at 20:08
  • 10.1 cannot be represented exactly as a double. You could use a small epsilon value to compare for equality, http://stackoverflow.com/questions/25160375/comparing-double-values-for-equality-in-java Are your round precisions always a multiple of 0.0005? What are you doing with the values after you round? – gunnerone Jan 17 '17 at 20:23
  • precisions are determined by the logic in the application and cannot be profiled as multiple of anything. – Dinesh Devkota Jan 17 '17 at 20:37
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/133384/discussion-between-gunnerone-and-dinesh-devkota). – gunnerone Jan 17 '17 at 20:48
0

The real problem here is that the Math.round has two definitions. One returns a long, while the other returns an int! When you provide a double it runs the one for a long. To fix this simply cast your input to a float, to make it run the one to return the int.

double numberDecimalMultiplier = Math.round((float)(number / roundPrecision));
C L K Kissane
  • 34
  • 1
  • 5