8 июня 2013 в 20:50

Банковское округление? оказывается, есть и такое...

Пару месяцев назад нужно было написать на php небольшой скрипт, который должен был подсчитывать большое количество финансовых данных — брать от сумм банковских переводов процент и подсчитывать сумму. При вычислении процентов появлялись суммы с десяти- и стотысячными долями значений, которые при последующем суммировании и округлении давали весьма интересные эффекты. Например, скрипт генерировал отчет, в котором суммы исходные и рассчитанные различались на 10-20 копеек. Начал разбираться с этим, и открыл для себя новый тип окрулегия — банковское округление.



Сначала я пошел неправильным путем — начал прогонять рассчитанные проценты через функцию round. Но это не помогло. Все равно были расхождения на копейки. Пришлось засесть за чтение теории, и всего через 10 минут выяснилось, что для борьбы с такими эффектами применяется метод «бухгалтерского округления», или за рубежом — «bank rounding».

Способ простой — если тысячные доли рубля равны 5, то смотрится цифра перед ней (единицы копеек). Если она четная, то тысячные просто отбрасываются, а если нечетная — то копейки округляются вверх. То есть по сравнению с математическим округлением разница такая (это тестовый массив чисел, подсчитанных руками).

4.565335,   // bank: 4.56, math: 4.57
34.565783,  // bank: 34.56, math: 34.57
56.355532,  // bank: 56.36, math: 56.36
9.4557642,  // bank: 9.45, math: 9.45
10.345643,  // bank: 10.34, math: 10.35
7.235345,   // bank: 7.24, math: 7.24
9.285456,   // bank: 9.28, math: 9.29
3.225,      // bank: 3.22, math: 3.23
10.25527,   // bank: 10.26, math: 10.26
11.41525,   // bank: 11.42, math: 11.42
0.105,      // bank: 0.10, math: 0.11
0.115,      // bank: 0.12, math 0.12

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

Осталось только найти реализацию этой функции в Интернете, и дело сделано. Конечно, алгоритм простейший, но всегда лучше полагаться на чей-то успешный опыт. А вот тут и ждал меня мега облом. Я не нашел реализацию этой функции для PHP! Вернее, нашел одну на StackOverflow, но она работала через преобразование в строку и выделение функцией substr десятых долей суммы. Для друих языков находил реализации через логарифмы.

Меня это не устроило, поэтому написал за минуту свою функцию. Пояснять ее смысла нет, она «прозрачна».

function bank_round ($val)
{
$tmp = intval (abs ($val) * 100);
if ($tmp % 2 != 0) $tmp +=1;
if ($val < 0) $tmp = 0 — $tmp;
return $tmp / 100;
}

Результат ее исполнения:

Демонстрация функции банковского округления

Немного поясню этот результат.

  • Первый столбец — это сырые данные с большим числом цифр после запятой. В первую строку «итого» (под «------») записана их арифметическая сумма, а во вторую строку ниже — результат математического округления.
  • Второй столбец, это сумма чисел, каждое из которых было округлено по математическим правилам. Как видно, такой подход дает расхождение в 0,4.
  • И, наконец, третий столбец — это сумма чисел, каждое из которых было округлено по правилам «банковского округления». Результат, как говорится,  налицо.

В общем — функция примитивная, но вдруг кому опыт пригодиться?

Поделиться заметкой
Опубликовать в Google Plus
Опубликовать в LiveJournal
Опубликовать в Мой Мир
Опубликовать в Одноклассники

Комментариев: 7

  • Haris
    26 августа 2014 в 15:13

    Nepravilno!!!!!

    Pravilno vot tak:

    4.565335, // bank: 4.57, math: 4.57

    34.565783, // bank: 34.57, math: 34.57

    56.355532, // bank: 56.36, math: 56.36

    9.4557642, // bank: 9.46, math: 9.46

    10.345643, // bank: 10.35, math: 10.35

    7.235345, // bank: 7.24, math: 7.24

    9.285456, // bank: 9.29, math: 9.29

    3.225, // bank: 3.22 , math: 3.23 (PRAVILNO)

    10.25527, // bank: 10.26, math: 10.26

    11.41525, // bank: 11.42, math: 11.42

    0.105, // bank: 0.10, math: 0.11 (PRAVILNO)

    0.115, // bank: 0.12, math 0.12 (PRAVILNO)

    Ответить

  • Дмитрий
    25 марта 2015 в 15:16

    кхм... всё круто и правильно, если не смотреть в функцию...

    и особенно если не тестировать функцию.

    В общем, уважаемый, во-первых вы забыли указать проверку на эту самую ключевую пятерку в самой функции... отсюда вывод... не пишите функцию за минуту, и тем более — не советуйте её использовать. исправьте или удалите код функции. а статью оставьте)

    За саму статью, и за пояснения — спасибо.

    Ответить

    Stan_1

    Да, возможно. :) Но пусть код будет — может кто за основу возьмет. :) Плюс ниже уже дали правильный вариант.

    Ответить

  • Дмитрий
    25 марта 2015 в 16:34

    /**

    * Округление по банковскому методу

    *

    * @param int|float $val

    * @param int $precision количество цифр после запятой

    * @return float число с банковским округлением

    */

    function to_bank_amount ($val, $precision = 2) {

    $q = pow (10, $precision);

    $x = intval (abs ($val) * $q * 10);

    if ($val && (($x % 10) == 5)){

    $tmp = intval (abs ($val) * $q);

    if ($tmp % 2 != 0) $tmp += 1;

    if ($val < 0) $tmp = 0 — $tmp;

    $amount = $tmp / $q;

    }else{

    $amount = round ($val,$precision);

    }

    return (float) $amount;

    }

    Ответить

  • Толсто
    8 февраля 2017 в 16:12

    У вас ошибка в тестовом массиве данных посчитаном руками: записано «bank: 9.45» должно быть «bank 9.46».

    Ответить

  • Андрей
    10 июля 2018 в 21:16

    А почему нельзя использовать то, что уже есть в php?

    function bank_round ($value) {

    return round ($value, 2, PHP_ROUND_HALF_EVEN);

    }

    Ответить

    Stan_1

    Не знал про это. Спасибо!

    Ответить

Ваш комментарий:

Поля, помеченные символом * обязательны для заполнения.

CAPTCHA image