При разработке отчетов в системе компоновки данных (СКД) и сложных алгоритмов учета рабочего времени, бронирования или аренды часто возникает задача «выворачивания» периодов. Суть задачи состоит в том, чтобы взять один общий диапазон (например, с 01.01.2017 по 31.12.2017) и набор непересекающихся периодов исключения, а на выходе получить непрерывный список образовавшихся свободных интервалов. При этом всю логику требуется реализовать исключительно средствами языка запросов без постобработки в коде.
Рассмотрим подробнее исходные данные, разберем базовый алгоритм группировки граничных точек, а также проанализируем важные нюансы и альтернативные варианты решения задачи.
Представим, что у нас есть основной интервал ДатаНачалаОсновная – ДатаОкончанияОсновная и произвольный список исключений. Например:
01.01.2017 – 31.12.201713.03.2017 – 18.03.201721.05.2017 – 08.07.201703.09.2017 – 12.11.2017Ожидаемый результат должен вернуть четыре свободных отрезка:
01.01.2017 – 12.03.201719.03.2017 – 20.05.201709.07.2017 – 02.09.201713.11.2017 – 31.12.2017Разберем по шагам логику построения запроса. Свободные интервалы образуются следующими точками:
ДатаНачалаОсновная.ДатаНачалаИсключения - 1 день.ДатаОкончанияИсключения + 1 день.ДатаОкончанияОсновная.Соберем все эти контрольные даты в одну временную таблицу с помощью оператора ОБЪЕДИНИТЬ ВСЕ, после чего пронумеруем полученный отсортированный список дат и сгруппируем их попарно.
Посмотрим на пример запроса для проверки в консоли запросов:
ВЫБРАТЬ
ДАТАВРЕМЯ(2017, 3, 13) КАК ДатаНачалаИсключения,
ДАТАВРЕМЯ(2017, 3, 18) КАК ДатаОкончанияИсключения
ПОМЕСТИТЬ Дано
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
ДАТАВРЕМЯ(2017, 5, 21),
ДАТАВРЕМЯ(2017, 7, 8)
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
ДАТАВРЕМЯ(2017, 9, 3),
ДАТАВРЕМЯ(2017, 11, 12)
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
&ДатаНачалаОсновная КАК Дата
ПОМЕСТИТЬ Даты
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
ДОБАВИТЬКДАТЕ(Дано.ДатаНачалаИсключения, ДЕНЬ, -1)
ИЗ
Дано КАК Дано
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
ДОБАВИТЬКДАТЕ(Дано.ДатаОкончанияИсключения, ДЕНЬ, 1)
ИЗ
Дано КАК Дано
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
&ДатаОкончанияОсновная
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
МИНИМУМ(Даты.Дата) КАК ДатаНачала,
МАКСИМУМ(Даты.Дата) КАК ДатаОкончания
ИЗ
(ВЫБРАТЬ
КОЛИЧЕСТВО(Раньше.Дата) КАК Номер,
Даты.Дата КАК Дата
ИЗ
Даты КАК Раньше
ВНУТРЕННЕЕ СОЕДИНЕНИЕ Даты КАК Даты
ПО Раньше.Дата <= Даты.Дата
СГРУППИРОВАТЬ ПО
Даты.Дата) КАК Даты
СГРУППИРОВАТЬ ПО
ВЫРАЗИТЬ((Даты.Номер - 1) / 2 - 0.5 КАК ЧИСЛО(10, 0))
Проанализируем тонкости, на которые необходимо обратить внимание при практическом применении данного подхода:
ВЫРАЗИТЬ(Число КАК ЧИСЛО) в платформе 1С применяется округление к ближайшему четному числу. Простое деление Даты.Номер / 2 может сгруппировать пары 1 и 2 отдельно от 3 и 4, поэтому для устойчивого разбиения по 2 строки формула группировки должна учитывать смещение или алгоритм целочисленного деления.ГДЕ МИНИМУМ(Даты.Дата) <= МАКСИМУМ(Даты.Дата).23:59:59), смещение через ДОБАВИТЬКДАТЕ следует производить с точностью до секунды: ДОБАВИТЬКДАТЕ(..., СЕКУНДА, 1) и ДОБАВИТЬКДАТЕ(..., СЕКУНДА, -1).Рассмотрим еще один надежный способ, часто применяемый в ЗУП и производственных блоках. Если гранулярность интервалов фиксирована (по дням):
ГДЕ Исключение.Дата IS NULL).ДОБАВИТЬКДАТЕ(ТаблицаДней.Дата, ДЕНЬ, -ТаблицаДней.НомерСтроки). Для всех дней одного непрерывного отрезка это значение будет идентичным.МИНИМУМ(Дата) и МАКСИМУМ(Дата).Такой подход полностью исключает проблемы с перекрытием исключений и гарантирует корректный расчет результирующих диапазонов.