При расчете долей и процентных соотношений в прикладных решениях 1С разработчики часто сталкиваются с классической проблемой: при независимом математическом округлении каждого значения до двух знаков после запятой общая сумма процентов по группе строк дает 100.01% или 99.99%. Например, если распределить показатель между двумя позициями с точными значениями 78.125% и 21.875%, стандартное округление превратит их в 78.13% и 21.88%, что в сумме дает расхождение.
Выясним причину этой проблемы, проанализируем особенности округления в платформе 1С:Предприятие и разберем по шагам надежные практические способы решения — от изменения логики хранения данных до готовых программных алгоритмов.
В платформе 1С округление во встроенном языке и в языке запросов работает по-разному:
Окр() позволяет задать режим: РежимОкругления.Окр15как20 (арифметическое округление) или РежимОкругления.Окр15как10 (банковское округление к ближайшему четному числу).ВЫРАЗИТЬ(... КАК ЧИСЛО(Длина, Точность)) транслируется непосредственно в функции СУБД, где алгоритм округления фиксирован.Однако ни математическое, ни банковское округление отдельных строк не гарантирует сохранение общей суммы в 100%, поскольку округление каждого слагаемого выполняется изолированно и не учитывает баланс всей совокупности. Для получения идеального итога требуется алгоритмическое распределение остатка погрешности.
Рассмотрим алгоритм распределения, при котором сумма 100% распределяется последовательно по строкам таблицы значений с непрерывным пересчетом остатка базы и остатка процентов. За счет динамического уменьшения остатка погрешность автоматически нивелируется на последних строках цикла.
Посмотрим на пример реализации данного алгоритма:
// ТЗ - исходная ТаблицаЗначений с колонками "Сумма" и "Процент"
БазаРаспределения = ТЗ.Итог("Сумма");
СуммаРаспределения = 100;
Для Каждого Стр Из ТЗ Цикл
Если БазаРаспределения = 0 Тогда
Стр.Процент = 0;
Иначе
// Округляем до 2 знаков текущую долю от оставшейся суммы
Стр.Процент = Окр(СуммаРаспределения * Стр.Сумма / БазаРаспределения, 2);
// Уменьшаем распределяемую сумму и базу на обработанные значения
СуммаРаспределения = СуммаРаспределения - Стр.Процент;
БазаРаспределения = БазаРаспределения - Стр.Сумма;
КонецЕсли;
КонецЦикла;
Этот код выполняется за минимальное время даже на массивах в десятки тысяч строк и гарантирует, что итоговая сумма процентов по всем строкам таблицы будет строго равна 100.00%.
Если в конфигурацию интегрирована Библиотека стандартных подсистем (БСП), не требуется писать собственные циклы пересчета. В общем модуле ОбщегоНазначенияКлиентСервер предусмотрена специализированная процедура.
Разберем вызов этой функции:
// МассивКоэффициентов содержит исходные количественные или суммовые показатели
МассивКоэффициентов = Новый Массив;
МассивКоэффициентов.Добавить(78.125);
МассивКоэффициентов.Добавить(21.875);
// Распределяем 100% с точностью 2 знака после запятой
МассивПроцентов = ОбщегоНазначенияКлиентСервер.РаспределитьСуммуПропорциональноКоэффициентам(100, МассивКоэффициентов, 2);
// На выходе получаем массив значений, сумма которых строго равна 100
Функция БСП использует метод наибольших остатков (алгоритм Гамильтона), благодаря чему погрешность в 0.01% относится не на случайную или последнюю строку, а на элемент с наибольшей отброшенной дробной частью, что обеспечивает максимальную математическую справедливость.
Если данные необходимо получить сразу готовым запросом без постобработки в коде 1С, применим технику расчета через накопительный итог. Разница между текущей суммой накопительного процента и предыдущей дает корректный процент для каждой строки.
Рассмотрим пример запроса для распределения остатков номенклатуры по складам:
ВЫБРАТЬ
ТоварыНаСкладахОстатки.Номенклатура КАК Номенклатура,
ТоварыНаСкладахОстатки.Склад КАК Склад,
ТоварыНаСкладахОстатки.КоличествоОстаток КАК КоличествоОстаток
ПОМЕСТИТЬ ВТ_Остатки
ИЗ
РегистрНакопления.ТоварыНаСкладах.Остатки КАК ТоварыНаСкладахОстатки
ГДЕ
ТоварыНаСкладахОстатки.КоличествоОстаток > 0
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
ВТ_Остатки.Номенклатура КАК Номенклатура,
СУММА(ВТ_Остатки.КоличествоОстаток) КАК КоличествоВсего
ПОМЕСТИТЬ ВТ_ИтогиНоменклатуры
ИЗ
ВТ_Остатки КАК ВТ_Остатки
СГРУППИРОВАТЬ ПО
ВТ_Остатки.Номенклатура
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
ВТ_Остатки.Номенклатура КАК Номенклатура,
ВТ_Остатки.Склад КАК Склад,
ВТ_Остатки.КоличествоОстаток / ВТ_ИтогиНоменклатуры.КоличествоВсего * 100 КАК ПроцентТочный
ПОМЕСТИТЬ ВТ_Проценты
ИЗ
ВТ_Остатки КАК ВТ_Остатки
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_ИтогиНоменклатуры КАК ВТ_ИтогиНоменклатуры
ПО ВТ_Остатки.Номенклатура = ВТ_ИтогиНоменклатуры.Номенклатура
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
Проценты.Номенклатура КАК Номенклатура,
Проценты.Склад КАК Склад,
ВЫРАЗИТЬ(СУММА(ПроцентыДо.ПроцентТочный) КАК ЧИСЛО(10, 2)) КАК НакопленныйПроцент
ПОМЕСТИТЬ ВТ_Накопительно
ИЗ
ВТ_Проценты КАК Проценты
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Проценты КАК ПроцентыДо
ПО Проценты.Номенклатура = ПроцентыДо.Номенклатура
И Проценты.Склад >= ПроцентыДо.Склад
СГРУППИРОВАТЬ ПО
Проценты.Номенклатура,
Проценты.Склад
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
Накопительно.Номенклатура КАК Номенклатура,
Накопительно.Склад КАК Склад,
Накопительно.НакопленныйПроцент - ЕСТЬNULL(МАКСИМУМ(Предыдущие.НакопленныйПроцент), 0) КАК Процент
ИЗ
ВТ_Накопительно КАК Накопительно
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Накопительно КАК Предыдущие
ПО Накопительно.Номенклатура = Предыдущие.Номенклатура
И Накопительно.Склад > Предыдущие.Склад
СГРУППИРОВАТЬ ПО
Накопительно.Номенклатура,
Накопительно.Склад,
Накопительно.НакопленныйПроцент
Следует учитывать, что соединение по нестрогому неравенству >= на очень больших выборках данных может создавать повышенную нагрузку на СУБД, поэтому на высоконагруженных базах предпочтительнее выполнять расчет в коде встроенного языка.
Проанализируем методические рекомендации по проектированию структуры метаданных:
Сумма(Количество) / ВычислитьВыражение("Сумма(Количество)", "ОбщийИтог") * 100. В этом случае СКД автоматически выведет в строках округленные значения, а в поле общих итогов корректно отобразит 100%.