Задача поиска пересекающихся временных отрезков — одна из классических в разработке на платформе 1С. Она возникает при проверке графиков отпусков, анализе занятости помещений, контроле пересечения периодов действия цен или скидок. В этой статье мы подробно разберем несколько способов решения этой задачи: от простого запроса до эффективного алгоритма "сканирующей прямой" (sweep-line).
Самый распространенный способ найти пересечения — это соединить таблицу саму с собой. Основная сложность здесь заключается в правильном формулировании условия наложения периодов. Рассмотрим универсальную формулу: два интервала [Начало1, Конец1] и [Начало2, Конец2] пересекаются тогда и только тогда, когда начало первого меньше конца второго, а конец первого больше начала второго.
Проанализируем пример реализации такого запроса. Предположим, у нас есть временная таблица ВТ_Отпуска с полями Сотрудник, ДатаНачала и ДатаОкончания. Нам нужно найти пары сотрудников одного отдела, чьи отпуска пересекаются.
ВЫБРАТЬ
Т1.Сотрудник КАК Сотрудник1,
Т2.Сотрудник КАК Сотрудник2,
ВЫБОР
КОГДА Т1.ДатаНачала > Т2.ДатаНачала ТОГДА Т1.ДатаНачала
ИНАЧЕ Т2.ДатаНачала
КОНЕЦ КАК ДатаНачалаПересечения,
ВЫБОР
КОГДА Т1.ДатаОкончания < Т2.ДатаОкончания ТОГДА Т1.ДатаОкончания
ИНАЧЕ Т2.ДатаОкончания
КОНЕЦ КАК ДатаОкончанияПересечения
ИЗ
ВТ_Отпуска КАК Т1
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_Отпуска КАК Т2
ПО Т1.Подразделение = Т2.Подразделение
И Т1.Сотрудник <> Т2.Сотрудник // Исключаем сравнение сотрудника с самим собой
ГДЕ
Т1.ДатаНачала <= Т2.ДатаОкончания
И Т1.ДатаОкончания >= Т2.ДатаНачала
Важный нюанс: условие Т1.ДатаНачала <= Т2.ДатаОкончания И Т1.ДатаОкончания >= Т2.ДатаНачала является наиболее полным. Оно корректно обрабатывает даже те случаи, когда один интервал полностью поглощает другой. Если вы используете оператор МЕЖДУ (как предлагалось в некоторых сообщениях на форуме), вы рискуете упустить случаи полного вложения, если не пропишете условия для обеих границ обоих интервалов.
Если данных очень много (десятки тысяч строк в ТаблицеЗначений) или вам нужно не просто найти пары пересечений, а объединить их в группы ("острова"), запрос может работать медленно из-за квадратичной сложности соединения. В таких случаях мы рекомендуем использовать алгоритм обхода событий.
Разберем алгоритм по шагам:
+1, для дат окончания -1.Посмотрим на пример реализации этого метода на языке 1С:
// Подготовим таблицу событий
ТЗ_События = Новый ТаблицаЗначений;
ТЗ_События.Колонки.Добавить("Дата", Новый ОписаниеТипов("Дата"));
ТЗ_События.Колонки.Добавить("Тип", Новый ОписаниеТипов("Число")); // 1 - начало, -1 - конец
Для Каждого СтрокаОтпуска Из ТаблицаОтпусков Цикл
НоваяСтрока = ТЗ_События.Добавить();
НоваяСтрока.Дата = СтрокаОтпуска.ДатаНачала;
НоваяСтрока.Тип = 1;
НоваяСтрока = ТЗ_События.Добавить();
// Добавляем секунду или используем границы дня в зависимости от логики
НоваяСтрока.Дата = КонецДня(СтрокаОтпуска.ДатаОкончания);
НоваяСтрока.Тип = -1;
КонецЦикла;
ТЗ_События.Сортировать("Дата Возр, Тип Убыв");
СчетчикПересечений = 0;
Для Каждого Событие Из ТЗ_События Цикл
СчетчикПересечений = СчетчикПересечений + Событие.Тип;
Если СчетчикПересечений > 1 Тогда
// Здесь фиксируем факт нахождения внутри пересечения
// Например, можно запомнить дату начала пересечения
КонецЕсли;
КонецЦикла;
Выясним причину частых ошибок при работе с датами в 1С. В платформе тип Дата всегда содержит время. Если ваш интервал заканчивается 05.03.2019, то без указания времени это будет 05.03.2019 00:00:00. Если другой интервал начинается в этот же день, пересечение может быть формальным (в одну секунду) или вообще не зафиксироваться.
Рекомендации по работе с границами:
НачалоДня(), а дату окончания к КонецДня().НАЧАЛОПЕРИОДА(Дата, ДЕНЬ) для приведения всех данных к единому стандарту.< или >), то интервалы, которые "соприкасаются" границами (один закончился, другой в ту же секунду начался), не будут считаться пересекающимися.Иногда задача стоит иначе: нужно объединить все пересекающиеся или идущие впритык периоды в один большой непрерывный интервал. Проанализируем, как это сделать запросом. Это классическая задача Gaps and Islands (Разрывы и Острова).
Для решения этой задачи в запросе мы сначала определяем, является ли текущая строка началом нового "острова" (то есть её дата начала больше максимальной даты окончания всех предыдущих строк). Затем, используя нарастающий итог, присваиваем каждой строке идентификатор группы. После этого достаточно выполнить простую группировку СГРУППИРОВАТЬ ПО идентификатору группы, вычислив МИНИМУМ(ДатаНачала) и МАКСИМУМ(ДатаОкончания).
Рассмотренные методы позволяют гибко решать задачи любой сложности: от простых проверок в форме документа до построения сложных аналитических отчетов по занятости ресурсов.