Как объединить пересекающиеся интервалы дат в запросе или коде 1С?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

При проектировании учетных систем разработчики часто сталкиваются с задачей анализа временных интервалов. Это могут быть периоды действия скидок, командировки сотрудников, сроки аренды оборудования или действия договоров. Зачастую эти периоды накладываются друг на друга, пересекаются или идут встык. Чтобы получить «чистую» картину и исключить двойной счет времени, нам необходимо выполнить слияние (объединение) таких пересекающихся периодов в единые непрерывные интервалы.

В этой статье мы подробно разберем, как решить эту задачу двумя принципиально разными способами: с помощью декларативного запроса 1С (без использования циклов во встроенном языке) и программно на встроенном языке 1С (с использованием оптимизированного обхода таблиц).

Постановка задачи на простом примере

Представим, что у нас есть три документа со следующими периодами деятельности:

  1. Документ №1: с 10 марта по 15 марта;
  2. Документ №2: с 13 марта по 20 марта;
  3. Документ №3: с 25 марта по 30 марта.

Проанализируем эти данные. Документ №1 и Документ №2 пересекаются в интервале с 13 по 15 марта. Следовательно, их необходимо объединить в один общий непрерывный период — с 10 по 20 марта. Документ №3 стоит обособленно и не пересекается с первыми двумя. В результате объединения мы должны получить ровно два чистых интервала:

Простое группирование с получением минимума и максимума дат здесь не подойдет, так как оно «схлопнет» все данные в один огромный период с 10 по 30 марта, затерев пятидневный разрыв (с 21 по 24 марта). Рассмотрим, как решить эту задачу правильно.

---

Решение 1. Объединение пересекающихся периодов в запросе 1С

Этот изящный алгоритм основан на поиске «непокрытых» (абсолютных) левых и правых границ интервалов через левое соединение таблицы самой с собой (Self-Join). Этот подход позволяет переложить всю вычислительную нагрузку на СУБД.

Пусть наши исходные данные находятся во временной таблице Интервалы с полями От (дата начала) и До (дата окончания).

Шаг 1. Находим левые границы будущих объединенных интервалов

Нам нужно выбрать только те даты начала (От), которые не лежат строго внутри какого-либо другого интервала из нашей же таблицы. То есть, левее этой даты нет открытого периода. Реализуем это с помощью левого соединения:


ВЫБРАТЬ РАЗЛИЧНЫЕ
	Край.От КАК От
ПОМЕСТИТЬ ЛевыеКрая
ИЗ
	Интервалы КАК Край
		ЛЕВОЕ СОЕДИНЕНИЕ Интервалы КАК Интервалы
		ПО (Интервалы.От < Край.От)
			И (Край.От <= Интервалы.До)
ГДЕ
	Интервалы.От ЕСТЬ NULL
;

Шаг 2. Находим правые границы будущих объединенных интервалов

Аналогичным образом находим даты окончания (До), которые не перекрываются другими интервалами «справа»:


ВЫБРАТЬ РАЗЛИЧНЫЕ
	Край.До КАК До
ПОМЕСТИТЬ ПравыеКрая
ИЗ
	Интервалы КАК Край
		ЛЕВОЕ СОЕДИНЕНИЕ Интервалы КАК Интервалы
		ПО (Интервалы.От <= Край.До)
			И (Край.До < Интервалы.До)
ГДЕ
	Интервалы.От ЕСТЬ NULL
;

Шаг 3. Сопоставляем найденные левые и правые границы

Теперь у нас есть две плоские таблицы: с левыми краями и с правыми. Нам остается сопоставить каждый левый край с ближайшим к нему правым краем. Ближайший правый край — это минимальный из тех краев, которые больше или равны левому:


ВЫБРАТЬ
	ЛевыеКрая.От КАК От,
	МИНИМУМ(ПравыеКрая.До) КАК До
ИЗ
	ЛевыеКрая КАК ЛевыеКрая
		ВНУТРЕННЕЕ СОЕДИНЕНИЕ ПравыеКрая КАК ПравыеКрая
		ПО (ЛевыеКрая.От <= ПравыеКрая.До)

ГРУППИРОВАТЬ ПО
	ЛевыеКрая.От
;

Этот метод работает полностью в СУБД и возвращает абсолютно точные линейные интервалы без дублей и перекрытий.

---

Решение 2. Программное объединение во встроенном языке 1С

Если периодов в вашей таблице очень много (десятки тысяч), то тяжелые запросы с левыми соединениями "себя с собой" могут сильно нагрузить СУБД. В таких случаях гораздо эффективнее решить задачу на встроенном языке 1С с помощью алгоритма «шкалы времени».

Рассмотрим готовую функцию, которая принимает на вход таблицу значений с колонками ДатаНачала и ДатаОкончания, а возвращает свернутую таблицу с объединенными периодами:


Функция ПолучитьОбъединенныеПериоды(ТЗ)
	Результат = Новый ТаблицаЗначений;
	ТипДата = Новый ОписаниеТипов("Дата", Новый КвалификаторыДаты(ЧастиДаты.Дата));
	Результат.Колонки.Добавить("ДатаНачала", ТипДата);
	Результат.Колонки.Добавить("ДатаОкончания", ТипДата);
	
	Если ТЗ.Количество() = 0 Тогда
		Возврат Результат;
	КонецЕсли;
	
	// Находим общий охват дат для построения массива
	МинимальнаяДата = ТЗ[0].ДатаНачала;
	МаксимальнаяДата = ТЗ[0].ДатаОкончания;
	
	Для Сч = 1 По ТЗ.Количество() - 1 Цикл
		Стр = ТЗ[Сч];
		Если Стр.ДатаНачала < МинимальнаяДата Тогда
			МинимальнаяДата = Стр.ДатаНачала;
		КонецЕсли;
		Если Стр.ДатаОкончания > МаксимальнаяДата Тогда
			МаксимальнаяДата = Стр.ДатаОкончания;
		КонецЕсли;
	КонецЦикла;
	
	КоличествоДнейВПериоде = (МаксимальнаяДата - МинимальнаяДата) / 86400 + 1;
	МассивДней = Новый Массив(КоличествоДнейВПериоде);
	
	// Заполняем массив дней признаком активности
	Для Каждого Стр Из ТЗ Цикл
		НачальныйИндекс = Цел((Стр.ДатаНачала - МинимальнаяДата) / 86400);
		КонечныйИндекс = Цел((Стр.ДатаОкончания - МинимальнаяДата) / 86400);
		Для Сч = НачальныйИндекс По КонечныйИндекс Цикл
			МассивДней[Сч] = Истина;
		КонецЦикла;
	КонецЦикла;
	
	// Собираем обратно непрерывные периоды из массива
	ПредыдущийДень = Неопределено;
	ТекущийПериод = Неопределено;
	
	Для Сч = 0 По КоличествоДнейВПериоде - 1 Цикл
		День = МассивДней[Сч];
		
		Если День = Неопределено Тогда
			Если ПредыдущийДень <> Неопределено Тогда
				ТекущийПериод.ДатаОкончания = МинимальнаяДата + (Сч - 1) * 86400;
			КонецЕсли;
		Иначе
			Если ПредыдущийДень = Неопределено Тогда
				ТекущийПериод = Результат.Добавить();
				ТекущийПериод.ДатаНачала = МинимальнаяДата + Сч * 86400;
			КонецЕсли;
		КонецЕсли;
		ПредыдущийДень = День;
	КонецЦикла;
	
	// Закрываем последний период, если он дошел до конца диапазона
	Если ПредыдущийДень <> Неопределено Тогда
		ТекущийПериод.ДатаОкончания = МинимальнаяДата + (КоличествоДнейВПериоде - 1) * 86400;
	КонецЕсли;
	
	Возврат Результат;
КонецФункции

Как работает этот код: мы проецируем все наши интервалы на «ось времени» в виде плоского массива дней. Каждый день, попадающий в любой из интервалов, помечается значением Истина. Затем мы просто обходим массив по порядку и склеиваем идущие подряд дни со значением Истина в один новый интервал. Этот метод гарантирует линейное время выполнения O(N).

---

Сравнение методов и ограничения производительности

Выбор оптимального алгоритма напрямую зависит от объема ваших данных и контекста исполнения:

Для оптимизации работы в запросах также часто применяют готовую таблицу-календарь (производственный календарь) или генератор чисел, чтобы быстро «развернуть» периоды в строки по дням, сгруппировать их по уникальности и собрать обратно. Это промежуточный и весьма надежный вариант.

← На главную