В финансовой и банковской сферах часто используется метод округления к ближайшему четному (так называемое банковское округление или "banker's rounding"). В отличие от стандартного школьного метода, где 0,5 всегда округляется вверх, банковское округление минимизирует накопленную погрешность при суммировании большого количества чисел. В этой статье мы подробно разберем, как реализовать этот алгоритм на языке 1С, проанализируем различные подходы и выберем наиболее эффективный.
Стандартный метод округления (0,5 -> 1) имеет небольшое смещение в большую сторону. При миллионах операций это смещение превращается в значительные суммы. Банковское округление работает по принципу: если дробная часть равна ровно 0,5, число округляется до ближайшего четного целого. Например:
В платформе 1С встроенная функция Окр() поддерживает режимы РежимОкругления.Окр15как10 (0,5 вниз) и РежимОкругления.Окр15как20 (0,5 вверх), но прямого режима "банковское" не существует. Поэтому нам придется комбинировать эти режимы.
Рассмотрим наиболее изящный способ, предложенный в обсуждении. Суть идеи заключается в том, чтобы программно определять, является ли целая часть числа четной или нечетной, и на основании этого выбирать режим округления для встроенной функции Окр().
Проанализируем пример функции, которая реализует эту логику в одну строку:
Результат = Окр(Число, Разрядность, ?(Число * Pow(10, Разрядность) % 2 < 1, РежимОкругления.Окр15как10, РежимОкругления.Окр15как20));
Разберем по шагам, как это работает:
Число на 10 в степени Разрядность. Это позволяет нам "вытащить" цифру, по которой будет приниматься решение, в разряд единиц.% 2 (остаток от деления на 2) мы проверяем, является ли полученное число четным.РежимОкругления.Окр15как10 (округляем 0,5 вниз, к этому самому четному).РежимОкругления.Окр15как20 (округляем 0,5 вверх, к следующему четному).В ходе дискуссии выяснилось, что иногда требования заказчика подразумевают "банковское округление" только по третьему знаку после запятой, игнорируя последующие хвосты. Рассмотрим более детальный код, который учитывает этот нюанс и удобен для отладки:
Функция ПолучитьБанковскоеОкругление(Число, Разрядность) Экспорт
// 1. Убираем лишние знаки за пределами точности принятия решения
ОбрезанноеЧисло = Цел(Число * Pow(10, Разрядность + 1)) / Pow(10, Разрядность + 1);
// 2. Определяем четность числа в целевом разряде
ЧислоДляПроверкиЧетности = ОбрезанноеЧисло * Pow(10, Разрядность);
// 3. Выбираем режим округления 1С
// Если число четное, то 0.5 округляем вниз (в сторону четного)
// Если нечетное, то 0.5 округляем вверх (тоже в сторону четного)
Если ЧислоДляПроверкиЧетности % 2 < 1 Тогда
НужныйРежим = РежимОкругления.Окр15как10;
Иначе
НужныйРежим = РежимОкругления.Окр15как20;
КонецЕсли;
Возврат Окр(ОбрезанноеЧисло, Разрядность, НужныйРежим);
КонецФункции
Этот вариант более надежен, так как использование Цел() позволяет избежать проблем с "хвостами" типа 0,500000001, которые при чистом банковском округлении должны были бы всегда округляться вверх, но по специфическим ТЗ часто должны считаться как 0,5.
Выясним причину, по которой некоторые программисты предпочитают избегать оператора % (остаток от деления) при работе с плавающей точкой. Иногда это связано с особенностями хранения дробных чисел в памяти. Рассмотрим альтернативный подход, который использует только логику сравнения результатов встроенных функций 1С:
Если Окр(Число, Разрядность, РежимОкругления.Окр15как10) = Окр(Число, Разрядность, РежимОкругления.Окр15как20) Тогда
// Число не пограничное (не заканчивается на 5), режим не важен
Режим = РежимОкругления.Окр15как20;
Иначе
// Мы попали ровно на середину (0.5). Проверяем результат на четность.
РезультатВниз = Окр(Число, Разрядность, РежимОкругления.Окр15как10);
Если (РезультатВниз * Pow(10, Разрядность)) % 2 = 0 Тогда
Режим = РежимОкругления.Окр15как10;
Иначе
Режим = РежимОкругления.Окр15как20;
КонецЕсли;
КонецЕсли;
Возврат Окр(Число, Разрядность, Режим);
Важные замечания:
Окр() в 1С ведут себя предсказуемо, но всегда рекомендуется протестировать их на отрицательных суммах возвратов.Число обладает фиксированной точностью (до 32 знаков), что избавляет нас от многих проблем типа double в других языках, но осторожность при делении и умножении на Pow() всё же не помешает.Подводя итог, можно сказать, что наиболее универсальным и производительным решением для 1С является Решение 1 или Решение 2. Они позволяют избежать медленных строковых манипуляций и работают на уровне базовой арифметики платформы.