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