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