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

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

При разработке отчетов в системе компоновки данных (СКД) и сложных алгоритмов учета рабочего времени, бронирования или аренды часто возникает задача «выворачивания» периодов. Суть задачи состоит в том, чтобы взять один общий диапазон (например, с 01.01.2017 по 31.12.2017) и набор непересекающихся периодов исключения, а на выходе получить непрерывный список образовавшихся свободных интервалов. При этом всю логику требуется реализовать исключительно средствами языка запросов без постобработки в коде.

Рассмотрим подробнее исходные данные, разберем базовый алгоритм группировки граничных точек, а также проанализируем важные нюансы и альтернативные варианты решения задачи.

Постановка задачи и базовый подход

Представим, что у нас есть основной интервал ДатаНачалаОсновная – ДатаОкончанияОсновная и произвольный список исключений. Например:

  1. Основной диапазон: 01.01.2017 – 31.12.2017
  2. Периоды исключений:
    1. 13.03.2017 – 18.03.2017
    2. 21.05.2017 – 08.07.2017
    3. 03.09.2017 – 12.11.2017

Ожидаемый результат должен вернуть четыре свободных отрезка:

  1. 01.01.2017 – 12.03.2017
  2. 19.03.2017 – 20.05.2017
  3. 09.07.2017 – 02.09.2017
  4. 13.11.2017 – 31.12.2017

Решение через сбор граничных точек и нумерацию строк

Разберем по шагам логику построения запроса. Свободные интервалы образуются следующими точками:

  1. Дата начала первого свободного отрезка совпадает с ДатаНачалаОсновная.
  2. Дата окончания каждого свободного отрезка перед исключением вычисляется как ДатаНачалаИсключения - 1 день.
  3. Дата начала следующего свободного отрезка после исключения вычисляется как ДатаОкончанияИсключения + 1 день.
  4. Дата окончания последнего отрезка совпадает с ДатаОкончанияОсновная.

Соберем все эти контрольные даты в одну временную таблицу с помощью оператора ОБЪЕДИНИТЬ ВСЕ, после чего пронумеруем полученный отсортированный список дат и сгруппируем их попарно.

Посмотрим на пример запроса для проверки в консоли запросов:


ВЫБРАТЬ
    ДАТАВРЕМЯ(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. Банковское округление в запросах 1С: При вызове функции ВЫРАЗИТЬ(Число КАК ЧИСЛО) в платформе 1С применяется округление к ближайшему четному числу. Простое деление Даты.Номер / 2 может сгруппировать пары 1 и 2 отдельно от 3 и 4, поэтому для устойчивого разбиения по 2 строки формула группировки должна учитывать смещение или алгоритм целочисленного деления.
  2. Смежные периоды исключений (стык в стык): Если одно исключение заканчивается, например, 15-го числа, а следующее начинается 16-го, то при добавлении дней мы получим некорректную пару точек, где дата начала будет больше даты окончания. Чтобы избежать пустых интервалов, в итоговую выборку добавляется фильтр ГДЕ МИНИМУМ(Даты.Дата) <= МАКСИМУМ(Даты.Дата).
  3. Точность границ времени: Если границы интервалов содержат время (например, окончание дня 23:59:59), смещение через ДОБАВИТЬКДАТЕ следует производить с точностью до секунды: ДОБАВИТЬКДАТЕ(..., СЕКУНДА, 1) и ДОБАВИТЬКДАТЕ(..., СЕКУНДА, -1).
  4. Пересекающиеся периоды исключений: Если во входных данных периоды исключений могут накладываться друг на друга, их необходимо предварительно нормализовать (схлопнуть) отдельным пакетом запроса перед вычитанием.

Альтернативный способ: Метод дискретных дат (Календарная сетка)

Рассмотрим еще один надежный способ, часто применяемый в ЗУП и производственных блоках. Если гранулярность интервалов фиксирована (по дням):

  1. Формируется последовательность всех дней за период (например, соединением с производственным календарем или сгенерированной таблицей чисел).
  2. Левым соединением отсекаются все даты, попадающие в интервалы исключений (условие ГДЕ Исключение.Дата IS NULL).
  3. Для оставшихся дат вычисляется идентификатор непрерывной цепочки через разность календарной даты и порядкового номера: ДОБАВИТЬКДАТЕ(ТаблицаДней.Дата, ДЕНЬ, -ТаблицаДней.НомерСтроки). Для всех дней одного непрерывного отрезка это значение будет идентичным.
  4. Итоговая таблица группируется по полученному идентификатору с вычислением МИНИМУМ(Дата) и МАКСИМУМ(Дата).

Такой подход полностью исключает проблемы с перекрытием исключений и гарантирует корректный расчет результирующих диапазонов.

← На главную