Округление двойных значений в C#

Мне нужен метод округления для двойных значений в С#. Он должен иметь возможность округлять двойное значение до любого значения точности округления. Мой код под рукой выглядит так:

public static double RoundI(double number, double roundingInterval) {

    if (roundingInterval == 0.0)
    {
        return;
    }

    double intv = Math.Abs(roundingInterval);
    double sign = Math.Sign(number);
    double val = Math.Abs(number);

    double valIntvRatio = val / intv;
    double k = Math.Floor(valIntvRatio);
    double m = valIntvRatio - k;

    bool mGreaterThanMidPoint = ((m - 0.5) >= 1e-14) ? true : false;
    bool mInMidpoint = (Math.Abs(m - 0.5) < 1e-14) ? true : false;
    return (mGreaterThanMidPoint || mInMidpoint) ? sign * ((k + 1) * intv) : sign * (k * intv);
}

Таким образом, RoundI (100, 3) должен дать 99, а RoundI (1,2345, 0,001) должен дать 1,235.

Проблема в том, что RoundI(1,275, 0,01) возвращает 1,27, а не 1,28. Это связано с тем, что при выполнении double valIntvRatio = val/intv, то есть double valIntvRatio = 1,275/0,01, получается 0,127499999999999. Я знаю, что это проблема с двойным представлением на любом языке программирования. Мой вопрос в том, есть ли стандартный код для таких вещей, без необходимости беспокоиться о точности в двойном размере? Здесь я установил допуск на 1e-14, но это слишком ограничено для этой проблемы, и я не знаю, какой правильный допуск нужно установить. Спасибо за любую помощь.


person Steve    schedule 25.01.2010    source источник
comment
Возможно, вам следует рассмотреть возможность использования типа данных Decimal.   -  person Kibbee    schedule 25.01.2010
comment
Почему round(100,3) дает 99? Если вы округлите до той же дробной позиции, что и 3 (0 знаков), вы получите 100, а не 99.   -  person paxdiablo    schedule 25.01.2010
comment
paxdiablo: извините, цель RoundI не в том, чтобы округлить первый параметр до той же дробной позиции, что и второй параметр. Второй параметр — это интервал округления, и округление округляет первый параметр до близкого значения, которое имеет режим 0 ко второму параметру.   -  person Steve    schedule 25.01.2010
comment
Ах, это имеет больше смысла (и мой ответ немного менее полезен). Извинения за это.   -  person paxdiablo    schedule 25.01.2010


Ответы (4)


Пример использования decimal, как указал Кибби

double d = 1.275;
Math.Round(d, 2);          // 1.27
Math.Round((decimal)d, 2); // 1.28 
person Community    schedule 25.01.2010
comment
Хотя это отличное решение обычно работает, когда значащие цифры d близки к пределу точности double, преобразование в decimal на самом деле удаляет слишком большую точность. В качестве примера возьмем d = 12345678901234.256; (вам нужно вывести это с d.ToString("R"), чтобы выявить скрытую точность). Если вы используете просто Math.Round(d, 2) в этом примере (не забудьте записать результат с помощью "R"), вы получите лучший результат, чем если вы сделаете Math.Round((decimal)d, 2). Это приведение к decimal убирает здесь слишком большую точность. В таких случаях используйте decimal с самого начала, без приведения. - person Jeppe Stig Nielsen; 02.01.2014
comment
+1 очень хорошее решение, но, к сожалению, оно не округляет 0,005 до 0,01 . Результат: 0 - person Saro Taşciyan; 02.01.2014
comment
@Zefnus, потому что он использует округление Банкира. Если вы хотите округлить до 0,01, используйте Math.Round((decimal)d, 2, MidpointRounding.AwayFromZero) - person Jimmy; 03.01.2014
comment
Почему результат 0,125 равен 0,12? - person Nguyễn Văn Quang; 12.12.2017

double d = 1.2345;

Math.Round(d, 2);

приведенный выше код должен помочь.

person AceMark    schedule 25.01.2010

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

Наверняка есть лучший способ реализовать «округление» (почти разновидность банковского округления), чем мое жонглирование строками ниже.

public static decimal RoundI(decimal number, decimal roundingInterval)
{
   if (roundingInterval == 0) { return 0;}

   decimal intv = Math.Abs(roundingInterval);
   decimal modulo = number % intv;
   if ((intv - modulo) == modulo) {
       var temp = (number - modulo).ToString("#.##################");
       if (temp.Length != 0 && temp[temp.Length - 1] % 2 == 0) modulo *= -1;
   }
    else if ((intv - modulo) < modulo)
        modulo = (intv - modulo);
    else
        modulo *= -1;

    return number + modulo;
}
person Jonas Elfström    schedule 26.01.2010
comment
возможно нашел баг. Если вы передадите: число = 0,5 интервал округления 1,0, что-то пойдет не так с битом массива var temp. - person Jon; 04.11.2011
comment
Это верно, но больший интервал, чем число, не имеет никакого смысла, не так ли? Я добавил дополнительную проверку в метод. - person Jonas Elfström; 05.11.2011
comment
в сценарии, который я смотрю на это может произойти. Попытка округлить валюту X с roundingInterval Y, оба неизвестны во время компиляции. Таким образом, небольшое значение может быть округлено большим интервалом. Спасибо, что вернулись ко мне. - person Jon; 05.11.2011
comment
Действительно, отсутствовал temp.Length != 0, теперь RoundI(50m,100m) возвращает 100 вместо исключения из индекса. - person Jonas Elfström; 06.11.2011

Примеры с использованием десятичного приведения, представленные в ответе Джимми, не отвечают на вопрос, поскольку они не показывают, как округлить двойное значение до любого требуемого значения точности округления. Я считаю, что правильный ответ с использованием десятичного литья следующий:

    public static double RoundI(double number, double roundingInterval)
    {
        return (double)((decimal)roundingInterval * Math.Round((decimal)number / (decimal)roundingInterval, MidpointRounding.AwayFromZero));
    }

Поскольку в нем используется десятичное приведение, это решение подвержено ошибкам приведения, упомянутым Jeppe Stig Nielsen в его комментарии к ответу Jimmy.

Также обратите внимание, что я указал MidpointRounding.AwayFromZero, поскольку это согласуется со спецификацией запрашивающей стороны, согласно которой RoundI(1,2345, 0,001) должен дать 1,235.

person R.T.    schedule 02.02.2016