Saturday, November 13, 2010

Об округлении, банкирах и математиках

Казалось бы, что может быть проще, округлить дробь до целого? И чего тут обсуждать, нас всех в школе учили это делать. Однако… берем Java:
        
System.out.println(Math.round(2.5));
System.out.println(Math.round(3.5));

Получаем
3
4
Всё правильно? Да, так нас учили в школе, меня по-крайней мере. Советский союз - колыбель инженеров, а это стандартное математическое округление.
Но, теперь возьмем C#
  
Console.WriteLine(Math.Round(2.5));
Console.WriteLine(Math.Round(3.5));

Получаем
2
4
Оп-п-паньки. Неожиданно, правда? Получается что в C# округление работает не так как в Java. Лезем в документацию и читаем:

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

И ведь действительно, есть такой стандарт и действительно так принято. США – колыбель банкиров. И говорят, их так учат округлять в школе.

Но вернемся к нашему программированию. Чтобы сделать всё еще более запутанным, попробуем вот такой Java код
  
System.out.println(new DecimalFormat("0").format(2.5));
System.out.println(new DecimalFormat("0").format(3.5));

В результате
2
4

Вот такая вот петрушка, а теперь представьте что есть приложение где серверная часть написана на Java а клиентская на .Net и какие прелестные и неуловимые баги это может породить. Или пусть даже всё написано на Java, но например значения в таблице мы форматируем с помощью DecimalFormat а «итого» считаем с помощью Math.round(). Счастливых вам ночей с дебаггером, дорогие программисты…

К счастью, в .Net метод Round перегружен Round(Decimal) / Round(Decimal, MidpointRounding) и при необходимости можно явно указать способ округления. Таким образом, привести C# к Java достаточно просто:
  
Console.WriteLine(Math.Round(2.5, MidpointRounding.AwayFromZero));
Console.WriteLine(Math.Round(3.5, MidpointRounding.AwayFromZero));

К сожалению, я не знаю простого способа привести Java к C#. Нужно либо использовать BigDecimal в конструктор которого передать MathContext с типом округления ROUND_HALF_EVEN либо писать собственный метод.

5 comments:

  1. В g++ тоже есть аналогичный момент:

    #include
    #include

    int main ()
    {
    printf ("formatting of 2.5 is %.0f\n",2.5);
    printf ("formatting of 3.5 is %.0f\n",3.5);

    printf ("formatting of 2.51 is %.0f\n",2.51);
    printf ("formatting of 3.51 is %.0f\n",3.51);

    return 0;
    }

    $ g++ test.c
    $ ./a.exe
    formatting of 2.5 is 2
    formatting of 3.5 is 3
    formatting of 2.51 is 3
    formatting of 3.51 is 4



    т.е. мы привыкли, что 2.5 округляется в большую сторону, ан нет.

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

    ReplyDelete
  3. Если бы до ближайшего четного, то Round(2.99999) было бы 2

    ReplyDelete
  4. Console.WriteLine(Math.Round(1099.485,2, MidpointRounding.AwayFromZero));

    выводит: 1099,48
    а надо 1099,49!!!!!

    и как это сделать?

    ReplyDelete
  5. В данном случае проблема возникает из-за двоичного представления чисел с плавающей точкой. Чтобы решить проблему Вам следует использовать decimal вместо double, вот так:

    Console.WriteLine(Math.Round(1099.485M, 2, MidpointRounding.AwayFromZero));

    ReplyDelete