Округление средней точки от нуля .net С# Decimal для Java Double

Я переводил .NET-код на Java и сталкивался с проблемой точности, не соответствующей.

Код .NET:

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

Вызов функции roundToPrecision(8.7250, 0.05); в приведенном выше коде дает мне 8.75, который ожидается.

Преобразование/перевод функции в Java выглядит следующим образом. Я не нахожу точных Math.Round.

Код Java:

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;
}

Вызов roundToPrecision(8.7250, 0.05); в коде Java дает мне 8.7, и это неверно.

Я даже пробовал модифицировать код с помощью BigDecimal следующим образом в Java, используя ссылку здесь С# Double Rounding, но не повезло.

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;
}

Пожалуйста, помогите мне, что мне нужно сделать, чтобы исправить это.

Вот несколько сценариев, которые можно попробовать.

  • number = 10.05; точность = .1; Ожидаемый = 10.1;
  • number = 10.12; точность = .01; Ожидаемый = 10.12;
  • number = 8.7250; точность = 0.05; Ожидаемый = 8.75;
  • number = 10.999; точность = 2; Ожидаемый = 10;
  • number = 6.174999999999999; точность = 0.05; Ожидаемый = 6.20;

Примечание. У меня более 60 тысяч чисел, и точность может варьироваться от 1 десятичной до 4 знаков после запятой. Результат .NET должен точно соответствовать Java.

Ответ 1

Проблема возникает из того, как дубликаты vs десятичные числа хранятся и представлены в памяти. См. Эти ссылки для более подробной информации: Doubles Десятичные числа

Посмотрим, как они работают в вашем коде. Использование удвоений, с аргументами 8.725 и 0.05. number / roundPrecision дает 174.499..., так как двойники не могут точно представлять 174.5. С десятичными знаками number / roundPrecision дает 174.5, десятичные числа могут точно представлять это. Итак, когда 174.499... округляется, округляется до 174 вместо 175.

Использование BigDecimal - шаг в правильном направлении. Однако есть проблема с тем, как он используется в вашем коде. Проблема возникает, когда вы создаете значение BigDecimal.

BigDecimal b = new BigDecimal(number / roundPrecision);

BigDecimal создается из двойника, поэтому неточность уже существует. Если вы можете создать аргументы BigDecimal из строки, которая будет намного лучше.

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);

Если функция должна принимать и возвращать удвоения:

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();
}

Имейте в виду, что двойники не могут точно представлять те же значения, что и десятичные знаки. Таким образом, функция, возвращающая double, не может иметь точный результат как исходную функцию С#, которая возвращает десятичные числа.

Ответ 2

Реальная проблема здесь в том, что Math.round имеет два определения. Один возвращает длинный, в то время как другой возвращает int! Когда вы предоставляете двойной, он длится один. Чтобы исправить это, просто введите свой ввод в float, чтобы он запускал тот, который возвращает int.

double numberDecimalMultiplier = Math.round((float)(number / roundPrecision));