2017-01-13 23 views
11

Я переводил .NET-код в Java и сталкивался с проблемой точности, не соответствующей.Средняя точка округления от нуля .net C# Decimal к Java Double

код .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, используя ссылку здесь C# 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; 
} 

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

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

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

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

ответ

5

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

Давайте посмотрим, как они работают в вашем коде. Использование удвоений, с аргументами 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); 

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

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

+0

Благодарим за ответ. Однако я уже пробовал это. Попробуйте senarios 1 и 2 (я обновил вопрос). Они не дают ожидаемого результата в вашей реализации. –

+0

Ваше решение бросает 'java.lang.ArithmeticException: Неограничивающее десятичное расширение; нет точного представимого десятичного результата' на 'BigDecimal numberDecimalMultiplier = number.divide (roundPrecision) .setScale (0, RoundingMode.CEILING);' когда число равно 10.0, а точность 0,1 –

+0

Каковы ваши аргументы, когда он выдает исключение? – gunnerone

0

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

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