Почему Math.Round (2.5) возвращает 2 вместо 3?

416

В C # результат Math.Round(2.5)равен 2.

Это должно быть 3, не так ли? Почему вместо 2 в C #?

jeffu
источник
5
Это на самом деле особенность. См. <a href=" msdn.microsoft.com/en-us/library/… документацию MSDN</a> . Этот вид округления известен как банковское округление. Что касается обходного пути, существует <a href = " msdn. microsoft.com/en-us/library/… overload </a>, что позволяет вызывающей стороне указать, как выполнить округление.
Джо
1
Очевидно, что метод округления, когда его просят округлить число ровно между двумя целыми числами, возвращает четное целое число. Итак, Math.Round (3.5) возвращает 4. Смотрите эту статью
Мэтью Джонс
20
Math.Round(2.5, 0, MidpointRounding.AwayFromZero);
Роберт Дургин
SQL Server округляется таким образом; интересные результаты тестов, когда есть модульный тест C # для проверки округления в T-SQL.
Идстам
7
@Med это не ошибка. Это способ работы двоичных чисел с плавающей запятой. 1.005не может быть представлен точно в двойном Это наверное 1.00499.... Если вы используете Decimalэту проблему, исчезнет. Существование перегрузки Math.Round, которая принимает число десятичных цифр в двойном числе, является сомнительным вариантом выбора IMO, поскольку она редко будет работать значимым образом.
CodesInChaos

Ответы:

561

Во-первых, в любом случае это не ошибка C #, а ошибка .NET. C # - это язык - он не решает, как Math.Roundэто реализовано.

И, во-вторых, нет - если вы прочитаете документы , вы увидите, что по умолчанию используется округление «до четного» (округление банкира):

Возвращаемое значение
Тип: System.Double
Целое число, ближайшее к. Если дробный компонент а находится посередине между двумя целыми числами, одно из которых является четным, а другое нечетным, то возвращается четное число. Обратите внимание, что этот метод возвращает Doubleвместо целочисленного типа.

Заметки
Поведение этого метода соответствует стандарту IEEE 754, раздел 4. Этот вид округления иногда называют округлением до ближайшего или округлением банкира. Это сводит к минимуму ошибки округления, возникающие в результате последовательного округления значения средней точки в одном направлении.

Вы можете указать, как Math.Roundследует округлять средние точки, используя перегрузку, которая принимает MidpointRoundingзначение. Существует одна перегрузка, MidpointRoundingсоответствующая каждой из перегрузок, у которой ее нет:

Был ли этот стандарт выбран правильно или нет - это другой вопрос. ( MidpointRoundingБыл представлен только в .NET 2.0. До этого я не был уверен, что был какой-либо простой способ реализовать желаемое поведение, не делая его самостоятельно.) В частности, история показала, что это не ожидаемое поведение - и в большинстве случаев это кардинальный грех в дизайне API. Я понимаю, почему округление Банкира полезно ... но для многих это все еще сюрприз.

Возможно, вам будет интересно взглянуть на ближайший эквивалент Java enum ( RoundingMode), который предлагает еще больше опций. (Это касается не только средних точек.)

Джон Скит
источник
4
я не знаю, является ли это ошибкой, я думаю, что это было разработано, так как .5 так же близко к ближайшему младшему целому числу, как и к ближайшему наибольшему целому числу.
Стэн Р.
3
Я помню это поведение в VB до применения .NET.
Джон Фиала
7
Действительно, стандарт IEEE 754, раздел 4, как указано в документации.
Джон Скит
2
Я был сожжен этим некоторое время назад и подумал, что это было просто безумие. К счастью, они добавили способ указать округление, которое все мы изучали в начальной школе; MidPointRounding.
Ши
26
+1 за «это не ожидаемое поведение [...], это
основной
215

Это называется округлением до четного (или округлением банкира), которое является допустимой стратегией округления для минимизации накопленных ошибок в суммах (MidpointRounding.ToEven). Теория состоит в том, что, если вы всегда округляете число 0,5 в одном и том же направлении, ошибки будут накапливаться быстрее (округление до четности должно минимизировать это) (a) .

Перейдите по этим ссылкам для описания MSDN:

  • Math.Floor, который округляется в сторону отрицательной бесконечности.
  • Math.Ceiling, который округляется в сторону положительной бесконечности.
  • Math.Truncate, который округляется вверх или вниз к нулю.
  • Math.Round, который округляется до ближайшего целого или указанного числа десятичных знаков. Вы можете указать поведение, если оно точно равноудалено между двумя возможностями, такими как округление, чтобы конечная цифра была четной (" Round(2.5,MidpointRounding.ToEven)" становится 2) или чтобы она была дальше от нуля (" Round(2.5,MidpointRounding.AwayFromZero)" стала 3).

Следующая диаграмма и таблица могут помочь:

-3        -2        -1         0         1         2         3
 +--|------+---------+----|----+--|------+----|----+-------|-+
    a                     b       c           d            e

                       a=-2.7  b=-0.5  c=0.3  d=1.5  e=2.8
                       ======  ======  =====  =====  =====
Floor                    -3      -1      0      1      2
Ceiling                  -2       0      1      2      3
Truncate                 -2       0      0      1      2
Round(ToEven)            -3       0      0      2      3
Round(AwayFromZero)      -3      -1      0      2      3

Обратите внимание, что Roundон намного мощнее, чем кажется, просто потому, что он может округляться до определенного количества десятичных знаков. Все остальные всегда округляются до нуля. Например:

n = 3.145;
a = System.Math.Round (n, 2, MidpointRounding.ToEven);       // 3.14
b = System.Math.Round (n, 2, MidpointRounding.AwayFromZero); // 3.15

С другими функциями вы должны использовать умножение / деление для достижения того же эффекта:

c = System.Math.Truncate (n * 100) / 100;                    // 3.14
d = System.Math.Ceiling (n * 100) / 100;                     // 3.15

(a) Конечно, эта теория зависит от того факта, что ваши данные имеют довольно четный разброс значений по четным половинкам (0,5, 2,5, 4,5, ...) и нечетным половинам (1,5, 3,5, ...).

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

paxdiablo
источник
3
Также известен как округление
банкира
Хорошее объяснение! Я хотел убедиться, как накапливается ошибка, и написал скрипт, который показывает, что значения, округленные с использованием округления банкира, в конечном счете, имеют свои суммы и средние значения намного ближе к исходным значениям. github.com/AmadeusW/RoundingDemo (доступны фотографии)
Amadeusz Wieczorek
Через некоторое время: не должен ли eтик (= 2,8) быть правее 2тика?
Superjos
Простой способ запомнить, и предположить, что десятое место равно 5: - одно место и десятое место все нечетные = округление вверх - одно место и десятое место смешаны = округление вниз * Ноль не странно * Перевернуто для отрицательных чисел
Arkham Angel
@ArkhamAngel, что на самом деле , кажется , труднее запомнить , чем просто «сделать последнюю цифру даже» :-)
paxdiablo
42

Из MSDN Math.Round (double a) возвращает:

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

... и поэтому 2.5, находясь на полпути между 2 и 3, округляется до четного числа (2). это называется округлением банкира (или округлением до четности) и является широко используемым стандартом округления.

Та же статья MSDN:

Поведение этого метода соответствует стандарту IEEE 754, раздел 4. Этот вид округления иногда называют округлением до ближайшего или округлением банкира. Это сводит к минимуму ошибки округления, возникающие в результате последовательного округления значения средней точки в одном направлении.

Вы можете указать другое поведение округления, вызвав перегрузки Math.Round, которые принимают MidpointRoundingрежим.

Майкл Петротта
источник
37

Вы должны проверить MSDN для Math.Round:

Поведение этого метода соответствует стандарту IEEE 754, раздел 4. Этот вид округления иногда называют округлением до ближайшего или округлением банкира.

Вы можете указать поведение Math.Roundиспользования перегрузки:

Math.Round(2.5, 0, MidpointRounding.AwayFromZero); // gives 3

Math.Round(2.5, 0, MidpointRounding.ToEven); // gives 2
Дирк Воллмар
источник
31

Характер округления

Рассмотрим задачу округления числа, которое содержит дробь, скажем, до целого числа. В этом случае процесс округления состоит в том, чтобы определить, какое целое число лучше всего представляет число, которое вы округляете.

При обычном или «арифметическом» округлении ясно, что 2,1, 2,2, 2,3 и 2,4 округляются до 2,0; и 2,6, 2,7, 2,8 и 2,9-3,0.

Это оставляет 2,5, что не ближе к 2,0, чем к 3,0. Вы можете выбрать между 2,0 и 3,0, в любом случае будет одинаково.

Для минус числа -2,1, -2,2, -2,3 и -2,4, станет -2,0; и -2,6, 2,7, 2,8 и 2,9 при арифметическом округлении станут -3,0.

Для -2,5 необходим выбор между -2,0 и -3,0.

Другие формы округления

«Округление» берет любое число с десятичными знаками и делает его следующим «целым» числом. Таким образом, не только 2,5 и 2,6 округления до 3,0, но и 2,1 и 2,2.

Округление смещает положительные и отрицательные числа от нуля. Например. 2,5-3,0 и -2,5-3,0.

«Округление» усекает числа, отсекая ненужные цифры. Это приводит к смещению чисел к нулю. Например. 2,5-2,0 и -2,5-2,0

В «округлении банкира» - в его наиболее распространенной форме - округляемое число 0,5 округляется в большую или меньшую сторону, так что результатом округления всегда является четное число. Таким образом, 2,5 округляется до 2,0, от 3,5 до 4,0, от 4,5 до 4,0, от 5,5 до 6,0 и так далее.

«Альтернативное округление» чередует процесс для любого .5 между округлением вниз и округлением вверх.

«Случайное округление» округляет .5 вверх или вниз на совершенно случайной основе.

Симметрия и асимметрия

Функция округления называется «симметричной», если она либо округляет все числа от нуля, либо округляет все числа до нуля.

Функция является «асимметричной», если округляет положительные числа до нуля и отрицательные числа от нуля. Например. От 2,5 до 2,0; и от -2,5 до -3,0.

Также асимметричная функция округляет положительные числа от нуля и отрицательные числа до нуля. Например. От 2,5 до 3,0; и от -2,5 до -2,0.

В большинстве случаев люди думают о симметричном округлении, где -2,5 будет округлено до -3,0, а 3,5 округлено до 4,0. (в C #Round(AwayFromZero))

Патрик Питерс
источник
28

По умолчанию MidpointRounding.ToEven, или округление банкиров ( 2.5 становится 2, 4.5 становится 4 и т. Д. ), Раньше меня ужалило при написании отчетов для бухгалтерии, поэтому я напишу несколько слов о том, что я узнал ранее и о том, эта почта.

Кто эти банкиры, которые округляют до четных чисел (возможно, британские банкиры!)?

Из википедии

Происхождение термина «банкиры» остается более неясным. Если этот метод округления был когда-либо стандартом в банковском деле, доказательства оказались чрезвычайно трудно найти. Напротив, в разделе 2 доклада Европейской комиссии «Введение евро и округление валютных сумм» предполагается, что ранее не было стандартного подхода к округлению в банковской сфере; и это указывает, что суммы «на полпути» должны быть округлены.

Это кажется очень странным способом округления, особенно для банковского дела, если, конечно, банки не используют для получения большого количества депозитов равных сумм. Внесите 2,4 млн. Фунтов стерлингов, но мы назовем это 2 млн. Фунтов стерлингов.

Стандарт IEEE 754 датируется 1985 годом и дает оба способа округления, но с банкиром в соответствии с рекомендациями стандарта. Эта статья в Википедии содержит длинный список того, как языки реализуют округление (поправьте меня, если что-то из нижеперечисленного неверно), и большинство не использует банкиров, но округление, которому вас учили в школе:

  • C / C ++ round () из math.h округляет до нуля (не округление банкира)
  • Java Math.Round округляет до нуля (он складывает результат, добавляет 0,5, приводит к целому числу). В BigDecimal есть альтернатива
  • Perl использует способ, аналогичный C
  • Javascript - это то же самое, что и Java Math.Round.
Крис С
источник
Спасибо за информацию. Я никогда не осознавал этого. Ваш пример о миллионах высмеивает это немного, но даже если вы округлите центы, то выплата процентов по 10 миллионам банковских счетов обойдется банку дорого, если все полценты будут округлены, или будет стоить клиентам много, если все полцента округляются в меньшую сторону. Так что я могу представить, что это согласованный стандарт. Не уверен, действительно ли это используется банкирами. Большинство клиентов не заметят округления, принося много денег, но я могу себе представить, что это обязательно по законам, если вы живете в стране с законами,
удобными для
15

Из MSDN:

По умолчанию Math.Round использует MidpointRounding.ToEven. Большинство людей не знакомы с «округлением до четности» как альтернативой, «округление от нуля» чаще всего преподается в школе. По умолчанию .NET имеет значение «Округление до четности», поскольку оно статистически лучше, поскольку оно не разделяет тенденцию «округления от нуля» к округлению немного чаще, чем округление вниз (при условии, что округляемые числа имеют тенденцию быть положительными. )

http://msdn.microsoft.com/en-us/library/system.math.round.aspx

Кристиан Доносо
источник
3

Поскольку Silverlight не поддерживает опцию MidpointRounding, вы должны написать свою собственную. Что-то вроде:

public double RoundCorrect(double d, int decimals)
{
    double multiplier = Math.Pow(10, decimals);

    if (d < 0)
        multiplier *= -1;

    return Math.Floor((d * multiplier) + 0.5) / multiplier;

}

Примеры, включающие, как использовать это как расширение, см. В посте: .NET и Silverlight Rounding

JBrooks
источник
3

У меня была проблема, когда мой SQL-сервер округлялся до 0,5 к 1, а мое приложение на C # - нет. Таким образом, вы увидите два разных результата.

Вот реализация с int / long. Вот как Java округляется.

int roundedNumber = (int)Math.Floor(d + 0.5);

Это, вероятно, самый эффективный метод, о котором вы могли подумать.

Если вы хотите сохранить двойное значение и использовать десятичную точность, то на самом деле это просто вопрос использования показателей 10 в зависимости от количества десятичных знаков.

public double getRounding(double number, int decimalPoints)
{
    double decimalPowerOfTen = Math.Pow(10, decimalPoints);
    return Math.Floor(number * decimalPowerOfTen + 0.5)/ decimalPowerOfTen;
}

Вы можете ввести отрицательный десятичный знак для десятичных точек, и это слово хорошо.

getRounding(239, -2) = 200
Короткий фитиль
источник
1

Простой способ это:

Math.Ceiling(decimal.Parse(yourNumber + ""));
Налан Мадхесваран
источник
0

Этот пост содержит ответ, который вы ищете:

http://weblogs.asp.net/sfurman/archive/2003/03/07/3537.aspx

В основном это то, что он говорит:

Возвращаемое значение

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

Поведение этого метода соответствует стандарту IEEE 754, раздел 4. Этот вид округления иногда называют округлением до ближайшего или округлением банкира. Если цифры равны нулю, этот вид округления иногда называется округлением до нуля.

Vaccano
источник
0

Silverlight не поддерживает параметр MidpointRounding. Вот метод расширения для Silverlight, который добавляет перечисление MidpointRounding:

public enum MidpointRounding
{
    ToEven,
    AwayFromZero
}

public static class DecimalExtensions
{
    public static decimal Round(this decimal d, MidpointRounding mode)
    {
        return d.Round(0, mode);
    }

    /// <summary>
    /// Rounds using arithmetic (5 rounds up) symmetrical (up is away from zero) rounding
    /// </summary>
    /// <param name="d">A Decimal number to be rounded.</param>
    /// <param name="decimals">The number of significant fractional digits (precision) in the return value.</param>
    /// <returns>The number nearest d with precision equal to decimals. If d is halfway between two numbers, then the nearest whole number away from zero is returned.</returns>
    public static decimal Round(this decimal d, int decimals, MidpointRounding mode)
    {
        if ( mode == MidpointRounding.ToEven )
        {
            return decimal.Round(d, decimals);
        }
        else
        {
            decimal factor = Convert.ToDecimal(Math.Pow(10, decimals));
            int sign = Math.Sign(d);
            return Decimal.Truncate(d * factor + 0.5m * sign) / factor;
        }
    }
}

Источник: http://anderly.com/2009/08/08/silverlight-midpoint-rounding-solution/

jascur2
источник
-1

используя пользовательское округление

public int Round(double value)
{
    double decimalpoints = Math.Abs(value - Math.Floor(value));
    if (decimalpoints > 0.5)
        return (int)Math.Round(value);
    else
        return (int)Math.Floor(value);
}
anand360
источник
>.5производит то же поведение, что и Math.Round. Вопрос в том, что происходит, когда десятичная часть точно 0.5. Math.Round позволяет указать желаемый алгоритм округления
Panagiotis Kanavos
-2

Это ужасно, как и весь ад, но всегда производит правильное арифметическое округление.

public double ArithRound(double number,int places){

  string numberFormat = "###.";

  numberFormat = numberFormat.PadRight(numberFormat.Length + places, '#');

  return double.Parse(number.ToString(numberFormat));

}
Джеймс Монтань
источник
5
То же Math.Roundсамое можно сказать и о том, как вы хотите, чтобы оно было округлено.
конфигуратор
-2

Вот способ, которым я должен был обойти это:

Public Function Round(number As Double, dec As Integer) As Double
    Dim decimalPowerOfTen = Math.Pow(10, dec)
    If CInt(number * decimalPowerOfTen) = Math.Round(number * decimalPowerOfTen, 2) Then
        Return Math.Round(number, 2, MidpointRounding.AwayFromZero)
    Else
        Return CInt(number * decimalPowerOfTen + 0.5) / 100
    End If
End Function

Попытка с 1,905 с двумя десятичными знаками даст 1,91, как и ожидалось, но Math.Round(1.905,2,MidpointRounding.AwayFromZero)дает 1,90! Метод Math.Round абсолютно несовместим и непригоден для большинства основных проблем, с которыми могут столкнуться программисты. Я должен проверить, если (int) 1.905 * decimalPowerOfTen = Math.Round(number * decimalPowerOfTen, 2)я не хочу округлять то, что должно быть округлено.

MikeMuffinMan
источник
Math.Round(1.905,2,MidpointRounding.AwayFromZero)возвращается1.91
Panagiotis Kanavos