При получении задач на пропорциональное распределение сумм (например, оклада 50 000 рублей по трем проектам с долями 1, 1, 1) программисты часто сталкиваются с погрешностью округления. При стандартном округлении каждого расчета до двух знаков после запятой (16 666,67 * 3 = 50 000,01) возникает лишняя копейка. В других условиях копейки, наоборот, может не хватать.
Разберем, почему возникает эта проблема и какие алгоритмы применяются в типовых решениях 1С для точного выравнивания итоговой суммы.
Проблема возникает из-за того, что деление числа на части с округлением каждой строки до 2 знаков после запятой теряет или добавляет дробную часть. Простая математическая сумма отдельных строк перестает совпадать с исходным распределяемым значением.
Для решения задачи в программировании на платформах 1С применяется универсальный принцип: распределять не фиксированную пропорцию от начальной суммы на каждом шаге, а вычислять остаток нераспределенной суммы и остаток базы распределения.
Рассмотрим пошаговый алгоритм распределения, в котором для каждого последующего элемента рассчитывается остаток суммы и остаток базы. При таком подходе последняя строка всегда забирает ровно тот остаток, который необходим для полного совпадения итоговой суммы.
Проанализируем пример с распределением 50 000 рублей на 3 равные доли:
50 000 / 3 = 16 666,67. Остаток суммы к распределению: 50 000,00 - 16 666,67 = 33 333,33.33 333,33 / 2 = 16 666,67. Остаток суммы к распределению: 33 333,33 - 16 666,67 = 16 666,66.16 666,66.В результате получаем суммы: 16 666,67 + 16 666,67 + 16 666,66 = 50 000,00. Итоговая сумма сходится идеально.
Посмотрим, как выглядит программная реализация этого метода в цикле обхода строки таблицы:
ВсегоКРаспределению = 50000;
ВсегоБаза = ТаблицаПроектов.Итог("Доля");
ОстатокСуммы = ВсегоКРаспределению;
ОстатокБазы = ВсегоБаза;
КоличествоСтрок = ТаблицаПроектов.Количество();
Индекс = 0;
Для Каждого СтрокаИз ТаблицаПроектов Цикл
Индекс = Индекс + 1;
Если Индекс = КоличествоСтрок Тогда
// На последней строке отдаем весь остаток
СтрокаИз.Сумма = ОстатокСуммы;
Иначе
Если ОстатокБазы <> 0 Тогда
ТекущаяСумма = Окр(ОстатокСуммы * (СтрокаИз.Доля / ОстатокБазы), 2);
Иначе
ТекущаяСумма = 0;
КонецЕсли;
СтрокаИз.Сумма = ТекущаяСумма;
ОстатокСуммы = ОстатокСуммы - ТекущаяСумма;
ОстатокБазы = ОстатокБазы - СтрокаИз.Доля;
КонецЕсли;
КонецЦикл;
Списание разницы на последнюю строку может дать сильное искажение, если у последней строки очень маленькая доля. Чтобы избежать этого, в типовых конфигурациях 1С возникшую разницу (в 1–2 копейки) относятся на строку с наибольшим значением базы.
Разберем этот алгоритм по шагам:
Окр(..., 2).Разница = ИсходнаяСумма - РассчитанныйИтог.Рассмотрим пример кода для данного подхода:
ВсегоКРаспределению = 50000;
ВсегоБаза = ТаблицаПроектов.Итог("Доля");
СтрокаСМаксБазой = Неопределено;
МаксБаза = -1;
РассчитанныйИтог = 0;
Для Каждого СтрокаИз ТаблицаПроектов Цикл
Если ВсегоБаза <> 0 Тогда
СтрокаИз.Сумма = Окр(ВсегоКРаспределению * (СтрокаИз.Доля / ВсегоБаза), 2);
Иначе
СтрокаИз.Сумма = 0;
КонецЕсли;
РассчитанныйИтог = РассчитанныйИтог + СтрокаИз.Сумма;
Если СтрокаИз.Доля > МаксБаза Тогда
МаксБаза = СтрокаИз.Доля;
СтрокаСМаксБазой = СтрокаИз;
КонецЕсли;
КонецЦикл;
// Корректировка копеек на максимальную строку
Разница = ВсегоКРаспределению - РассчитанныйИтог;
Если Разница <> 0 И СтрокаСМаксБазой <> Неопределено Тогда
СтрокаСМаксБазой.Сумма = СтрокаСМаксБазой.Сумма + Разница;
КонецЕсли;
Если проект разрабатывается на базе конфигурации с Библиотекой Стандартных Подсистем (БСП) или типового решения (1С:УНФ, 1С:ERP, 1С:УТ, 1С:КА), не требуется писать алгоритмы выравнивания вручную.
Проанализируем доступные встроенные функции. В общих модулях подсистемы математических вычислений присутствует функция РаспределитьПропорционально(). В нее передается массив распределяемых сумм или таблица с коэффициентами, а на выходе получается сбалансированный массив значений, где погрешность копеек уже автоматически распределена платформенными алгоритмами.
При реализации собственного алгоритма распределения обратим внимание на следующие нюансы:
ВсегоБаза = 0), чтобы предотвратить критическую ошибку деления на ноль.Модуль() или Абс().Окр(..., 2) должно происходить только в момент присвоения итоговой денежной суммы.