При разработке аналитических отчетов в системе компоновки данных (СКД) часто возникает задача подсчета непрерывных интервалов. Классический пример — определить, сколько месяцев подряд товар находился на остатках склада. Алгоритмически логика выглядит просто: при наличии остатка увеличиваем переменную на единицу, а при нулевом остатке — сбрасываем счетчик в ноль.
Однако при попытке перенести такой алгоритм в СКД разработчики сталкиваются со сложностями. Выясним причину этого ограничения и подробно разберем проверенные способы решения задачи.
Система компоновки данных выполняет реляционные операции над множествами. Язык выражений СКД не поддерживает концепцию изменяемых переменных и циклов. Основные ограничения:
ВычислитьВыражение с параметром Предыдущая позволяет получить срез исходных данных вышестоящей строки, но не дает возможности организовать сквозной накопительный итог с условным сбросом внутри одной формулы.Для реализации такого механизма мы можем применить один из четырех практических подходов.
Рассмотрим вариант, когда расчет выполняется на встроенном языке 1С с передачей полного набора данных группировки через специальную агрегатную функцию СКД. Это позволяет не усложнять исходный запрос.
Разберем по шагам реализацию этого метода:
Функция РассчитатьНепрерывныйПериод(МассивОстатков) Экспорт
Результат = 0;
Если ТипЗнч(МассивОстатков) <> Тип("Массив") Тогда
Возврат 0;
КонецЕсли;
Для Индекс = 1 По МассивОстатков.Количество() Цикл
ТекущийОстаток = МассивОстатков[МассивОстатков.Количество() - Индекс];
Если ТекущийОстаток > 0 Тогда
Результат = Результат + 1;
Иначе
Прервать;
КонецЕсли;
КонецЦикла;
Возврат Результат;
КонецФункции
МесяцевНаСкладе.ВычислитьВыражениеСГруппировкойМассив, передав результат в наш модуль:
МойМодуль.РассчитатьНепрерывныйПериод(ВычислитьВыражениеСГруппировкойМассив("Остаток", "ПериодМесяц"))
При таком подходе СКД собирает значения группировок в массив, а прикладной код быстро вычисляет итоговый счетчик для каждой строки.
Проанализируем классический SQL-подход. Его преимущество состоит в том, что расчет выполняется целиком на стороне СУБД без привлечения внешнего кода.
Логика заключается в поиске даты последнего «разрыва» (месяца, когда остаток был равен нулю или отсутствовал):
МАКСИМУМ(ТаблицаРазрывов.ПериодМесяц)).РАЗНОСТЬДАТ:
ВЫБРАТЬ
Т.Номенклатура КАК Номенклатура,
Т.ПериодМесяц КАК ПериодМесяц,
Т.Остаток КАК Остаток,
ВЫБОР
КОГДА Т.Остаток = 0 ТОГДА 0
КОГДА ЕСТЬNULL(МАКСИМУМ(Разрывы.ПериодМесяц), ДАТАВРЕМЯ(1, 1, 1)) = ДАТАВРЕМЯ(1, 1, 1)
ТОГДА РАЗНОСТЬДАТ(МИНИМУМ(ВсеПериоды.ПериодМесяц), Т.ПериодМесяц, МЕСЯЦ) + 1
ИНАЧЕ РАЗНОСТЬДАТ(МАКСИМУМ(Разрывы.ПериодМесяц), Т.ПериодМесяц, МЕСЯЦ)
КОНЕЦ КАК МесяцевПодряд
ИЗ
ВТ_ОстаткиПоМесяцам КАК Т
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ОстаткиПоМесяцам КАК Разрывы
ПО Т.Номенклатура = Разрывы.Номенклатура
И Т.ПериодМесяц > Разрывы.ПериодМесяц
И Разрывы.Остаток = 0
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ОстаткиПоМесяцам КАК ВсеПериоды
ПО Т.Номенклатура = ВсеПериоды.Номенклатура
СГРУППИРОВАТЬ ПО
Т.Номенклатура,
Т.ПериодМесяц,
Т.Остаток
Если используется современная версия платформы 1С:Предприятие, задачу об «островах и разрывах» можно решить максимально лаконично и производительно с помощью оконных функций.
Посмотрим на пример формирования групп непрерывности:
НОМЕРСТРОКИ() OVER (PARTITION BY Номенклатура ORDER BY ПериодМесяц).НОМЕРСТРОКИ() OVER (PARTITION BY Номенклатура, (Остаток > 0) ORDER BY ПериодМесяц).НОМЕРСТРОКИ() OVER (PARTITION BY Номенклатура, ИдентификаторГруппы ORDER BY ПериодМесяц).Если структура отчета сложная или требуется производить расчеты непосредственно при генерации результирующего табличного документа, используем модуль объекта отчета и событие ПриКомпоновкеРезультата.
Разберем пример обхода элементов компоновки с ручным ведением счетчика:
Процедура ПриКомпоновкеРезультата(ДокументРезультат, ДанныеРасшифровки, СтандартнаяОбработка)
СтандартнаяОбработка = Ложь;
Настройки = КомпоновщикНастроек.ПолучитьНастройки();
КомпоновщикМакета = Новый КомпоновщикМакетаКомпоновкиДанных;
МакетКомпоновки = КомпоновщикМакета.Выполнить(СхемаКомпоновкиДанных, Настройки, ДанныеРасшифровки);
ПроцессорКомпоновки = Новый ПроцессорКомпоновкиДанных;
ПроцессорКомпоновки.Инициализировать(МакетКомпоновки, , ДанныеРасшифровки, Истина);
ПроцессорВывода = Новый ПроцессорВыводаРезультатаКомпоновкиДанныхВТабличныйДокумент;
ПроцессорВывода.УстановитьДокумент(ДокументРезультат);
ПроцессорВывода.НачатьВывод();
ТекущийСчетчик = 0;
Пока Истина Цикл
ЭлементРезультата = ПроцессорКомпоновки.Следующий();
Если ЭлементРезультата = Неопределено Тогда
Прервать;
КонецЕсли;
// Пример проверки значения и модификации параметров вывода
Если ЭлементРезультата.ТипЭлемента = ТипЭлементаРезультатаКомпоновкиДанных.Записи Тогда
Остаток = ЭлементРезультата.ЗначенияПараметров.Найти("Остаток");
Если Остаток <> Неопределено И Остаток.Значение > 0 Тогда
ТекущийСчетчик = ТекущийСчетчик + 1;
Иначе
ТекущийСчетчик = 0;
КонецЕсли;
КонецЕсли;
ПроцессорВывода.ВывестиЭлемент(ЭлементРезультата);
КонецЦикла;
ПроцессорВывода.ЗакончитьВывод();
КонецПроцедуры
Для типовых отчетов на СКД оптимальным выбором является Способ 1 (вызов функции через ВычислитьВыражениеСГруппировкойМассив) благодаря простоте внедрения, либо Способ 3 (оконные функции в запросе), если отчет оперирует большими объемами данных на платформе 8.3.20 и выше.