Округление средней точки от нуля .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));