Как определить свободные временные интервалы в расписании кабинета, вычитая занятое время?

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

При работе с планированием ресурсов, таких как кабинеты или время сотрудников, часто возникает задача определения свободных временных интервалов. Нам необходимо взять общий график работы и «вычесть» из него все занятые периоды, чтобы получить отчет по доступному времени. Рассмотрим, как это можно реализовать в 1С, используя различные подходы.

Исходные данные, как правило, представлены в виде двух таблиц значений или временных таблиц:

  1. ТзГрафикРаботы: График работы сотрудника или доступности кабинета. Каждая строка представляет собой интервал работы.
  2. ТЗПациенты: Записи пациентов или занятость кабинета. Каждая строка также представляет собой интервал.

Приведем пример структуры данных:

ТзГрафикРаботы:


Сотрудник1 | 10.01.2023 08:00:00 - 10.01.2023 10:00:00
Сотрудник1 | 10.01.2023 13:00:00 - 10.01.2023 20:00:00

ТЗПациенты:


Пациент1 | 10.01.2023 08:00:00 - 10.01.2023 08:20:00
Пациент2 | 10.01.2023 08:20:00 - 10.01.2023 08:40:00

Желаемый результат (свободное время):


10.01.2023 08:40:00 - 10.01.2023 10:00:00
10.01.2023 13:00:00 - 10.01.2023 20:00:00

Простая циклическая проверка или вычитание интервалов может оказаться неэффективной и сложной для учета всех возможных сценариев, особенно когда записи пациентов не идут подряд или график работы прерывается.

Подход 1: Дискретизация и квантование времени

Один из способов решения данной задачи — это дискретизация времени, то есть разбиение непрерывного временного диапазона на фиксированные, "квантованные" интервалы, например, по 1, 5, 10 или 15 минут. Затем мы работаем уже с этими дискретными "слотами".

Суть метода:

  1. Генерация временной оси: Мы создаем таблицу (или таблицу значений) с мельчайшими временными интервалами, покрывающими весь интересующий нас период. Например, если нам нужна точность до минуты, мы можем сгенерировать все минуты за рабочий день.
  2. Определение статуса: Для каждого такого минутного интервала мы проверяем, попадает ли он в график работы сотрудника и одновременно не попадает ли он в интервалы записи пациентов.
  3. Агрегация: После определения статуса каждого минутного интервала мы можем агрегировать последовательные свободные интервалы в более крупные, чтобы получить итоговый отчет.

Шаги реализации (на примере генерации минутной временной оси):

Сначала создадим временную ось. Это можно сделать программно в цикле:


Функция СформироватьВременнуюОсь(НачалоПериода, КонецПериода, ИнтервалВСекундах = 60)
    ТаблицаВремени = Новый ТаблицаЗначений;
    ТаблицаВремени.Колонки.Добавить("Период");

    ТекущееВремя = НачалоПериода;
    Пока ТекущееВремя < КонецПериода Цикл
        НоваяСтрока = ТаблицаВремени.Добавить();
        НоваяСтрока.Период = ТекущееВремя;
        ТекущееВремя = ДобавитьКДате(ТекущееВремя, ВидДаты.Секунда, ИнтервалВСекундах);
    КонецЦикла;

    Возврат ТаблицаВремени;
КонецФункции

// Пример использования:
НачалоДня = НачалоДня(ТекущаяДата());
КонецДня = КонецДня(ТекущаяДата());
ВременнаяОсь = СформироватьВременнуюОсь(НачалоДня, КонецДня);

Затем, для каждого интервала из ВременнаяОсь мы можем программно проверить его статус или использовать запросы к этой оси, если она помещена во временную таблицу. Этот подход более трудоемок в программной реализации проверки условий, но прост в концепции.

Подход 2: Метод элементарных интервалов с использованием SQL-запросов

Этот подход является более мощным и гибким, особенно для обработки сложных пересечений и большого объема данных. Он основан на идее объединения всех ключевых временных точек и последующего формирования элементарных интервалов.

Ключевая идея: Мы собираем все моменты начала и окончания всех интервалов (как рабочего графика, так и записей пациентов) в один список. Затем сортируем этот список и формируем новые, самые мелкие интервалы, границы которых определяются этими объединенными точками. Для каждого такого элементарного интервала мы легко можем определить его статус, проверяя, попадает ли он в исходные интервалы графика и записей.

Разберем этот подход по шагам, используя язык запросов 1С:

Шаг 1: Подготовка исходных данных во временные таблицы

Для удобства работы с запросом сначала поместим исходные данные в две временные таблицы. Это позволит нам работать с ними как с обычными таблицами базы данных.


// Временная таблица с графиком работы (ТзГрафикРаботы)
ВЫБРАТЬ
    1 КАК Врач,
    ДАТАВРЕМЯ(2023, 1, 10, 8, 0, 0) КАК ДатаНачало,
    ДАТАВРЕМЯ(2023, 1, 10, 10, 0, 0) КАК ДатаКонец
ПОМЕСТИТЬ ВТ_График
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
    1,
    ДАТАВРЕМЯ(2023, 1, 10, 13, 0, 0),
    ДАТАВРЕМЯ(2023, 1, 10, 20, 0, 0)
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
    2,
    ДАТАВРЕМЯ(2023, 1, 10, 10, 0, 0),
    ДАТАВРЕМЯ(2023, 1, 10, 15, 0, 0)
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
    2,
    ДАТАВРЕМЯ(2023, 1, 10, 17, 0, 0),
    ДАТАВРЕМЯ(2023, 1, 10, 20, 0, 0)
;

// Временная таблица с записями пациентов (ТЗПациенты)
ВЫБРАТЬ
    1 КАК Пациент,
    1 КАК Врач,
    ДАТАВРЕМЯ(2023, 1, 10, 8, 0, 0) КАК ДатаНачало,
    ДАТАВРЕМЯ(2023, 1, 10, 8, 20, 0) КАК ДатаКонец
ПОМЕСТИТЬ ВТ_Пациенты
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
    2,
    1,
    ДАТАВРЕМЯ(2023, 1, 10, 8, 20, 0),
    ДАТАВРЕМЯ(2023, 1, 10, 8, 40, 0)
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
    3,
    1,
    ДАТАВРЕМЯ(2023, 1, 10, 9, 20, 0),
    ДАТАВРЕМЯ(2023, 1, 10, 9, 40, 0)
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
    4,
    1,
    ДАТАВРЕМЯ(2023, 1, 10, 19, 40, 0),
    ДАТАВРЕМЯ(2023, 1, 10, 20, 0, 0)
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
    1,
    2,
    ДАТАВРЕМЯ(2023, 1, 10, 11, 20, 0),
    ДАТАВРЕМЯ(2023, 1, 10, 11, 40, 0)
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
    2,
    2,
    ДАТАВРЕМЯ(2023, 1, 10, 18, 0, 0),
    ДАТАВРЕМЯ(2023, 1, 10, 18, 0, 0)
;

Шаг 2: Объединение всех граничных точек и формирование элементарных интервалов

На этом шаге мы собираем все даты начала и окончания из обеих временных таблиц. Важно, чтобы конечная дата интервала была не просто точкой, а фактически началом следующего интервала или завершением текущего. Часто конечная дата интервала является не включающей (эксклюзивной). Для корректного формирования интервалов "конец - начало следующего" нам потребуются уникальные даты. Затем мы используем функцию СЛЕДУЮЩИЙ() (аналог LEAD() в SQL) для получения следующей уникальной точки и формирования интервала между текущей точкой и следующей.

Обратите внимание на граничные условия: если интервал заканчивается в 08:20:00, а следующий начинается в 08:20:00, то между ними нет промежутка. Если же интервал заканчивается в 08:20:00, а следующий начинается в 08:20:01, то между ними есть 1 секунда, которую нужно учесть. Для упрощения, часто интервал [Начало; Конец), т.е. "Начало включительно, Конец не включительно". Если мы хотим, чтобы Конец был включительно, нужно от следующего начала отнять 1 секунду.


ВЫБРАТЬ РАЗЛИЧНЫЕ
    ВсеДаты.Врач,
    ВсеДаты.ДатаТочка
ПОМЕСТИТЬ ВТ_УникальныеДаты
ИЗ
    (ВЫБРАТЬ
        ВТ_График.Врач КАК Врач,
        ВТ_График.ДатаНачало КАК ДатаТочка
    ИЗ
        ВТ_График КАК ВТ_График
    ОБЪЕДИНИТЬ ВСЕ
    ВЫБРАТЬ
        ВТ_График.Врач,
        ВТ_График.ДатаКонец
    ИЗ
        ВТ_График КАК ВТ_График
    ОБЪЕДИНИТЬ ВСЕ
    ВЫБРАТЬ
        ВТ_Пациенты.Врач,
        ВТ_Пациенты.ДатаНачало
    ИЗ
        ВТ_Пациенты КАК ВТ_Пациенты
    ОБЪЕДИНИТЬ ВСЕ
    ВЫБРАТЬ
        ВТ_Пациенты.Врач,
        ВТ_Пациенты.ДатаКонец
    ИЗ
        ВТ_Пациенты КАК ВТ_Пациенты) КАК ВсеДаты
;

ВЫБРАТЬ
    ВТ_УникальныеДаты.Врач,
    ВТ_УникальныеДаты.ДатаТочка КАК ДатаНачалаИнтервала,
    ДОБАВИТЬКДАТЕ(СЛЕДУЮЩИЙ(ВТ_УникальныеДаты.ДатаТочка) ПО Врач УПОРЯДОЧИТЬ ПО ДатаТочка, ВИДДАТЫ.СЕКУНДА, -1) КАК ДатаКонцаИнтервала
ПОМЕСТИТЬ ВТ_ЭлементарныеИнтервалы
ИЗ
    ВТ_УникальныеДаты КАК ВТ_УникальныеДаты
ГДЕ
    НЕ СЛЕДУЮЩИЙ(ВТ_УникальныеДаты.ДатаТочка) ПО Врач УПОРЯДОЧИТЬ ПО ДатаТочка ЕСТЬ NULL
;

Пояснения к Шагу 2:

Шаг 3: Определение статуса каждого элементарного интервала

Теперь, когда у нас есть все элементарные интервалы, мы можем присоединить к ним исходные таблицы графика и пациентов, чтобы определить статус каждого интервала.


ВЫБРАТЬ
    ЭИ.Врач,
    ЭИ.ДатаНачалаИнтервала,
    ЭИ.ДатаКонцаИнтервала,
    ВЫБОР
        КОГДА График.Врач ЕСТЬ NULL ТОГДА "НЕ РАБОТАЕТ"
        КОГДА Пациент.Врач ЕСТЬ NULL ТОГДА "СВОБОДНО"
        ИНАЧЕ "ЗАПИСАН ПАЦИЕНТ " + СТРОКА(Пациент.Пациент)
    КОНЕЦ КАК Статус
ИЗ
    ВТ_ЭлементарныеИнтервалы КАК ЭИ
        ЛЕВОЕ СОЕДИНЕНИЕ ВТ_График КАК График
        ПО ЭИ.Врач = График.Врач
            И ЭИ.ДатаКонцаИнтервала МЕЖДУ График.ДатаНачало И График.ДатаКонец
        ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Пациенты КАК Пациент
        ПО ЭИ.Врач = Пациент.Врач
            И ЭИ.ДатаКонцаИнтервала МЕЖДУ Пациент.ДатаНачало И Пациент.ДатаКонец
ГДЕ
    НЕ ЭИ.ДатаКонцаИнтервала ЕСТЬ NULL

Пояснения к Шагу 3:

Примерный результат выполнения запроса для Врача 1 (после Шага 3):


Врач | ДатаНачалаИнтервала | ДатаКонцаИнтервала | Статус
-----|---------------------|--------------------|----------------------
1    | 2023-01-10 08:00:00 | 2023-01-10 08:19:59 | ЗАПИСАН ПАЦИЕНТ 1
1    | 2023-01-10 08:20:00 | 2023-01-10 08:39:59 | ЗАПИСАН ПАЦИЕНТ 2
1    | 2023-01-10 08:40:00 | 2023-01-10 09:19:59 | СВОБОДНО
1    | 2023-01-10 09:20:00 | 2023-01-10 09:39:59 | ЗАПИСАН ПАЦИЕНТ 3
1    | 2023-01-10 09:40:00 | 2023-01-10 09:59:59 | СВОБОДНО
1    | 2023-01-10 10:00:00 | 2023-01-10 12:59:59 | НЕ РАБОТАЕТ
1    | 2023-01-10 13:00:00 | 2023-01-10 19:39:59 | СВОБОДНО
1    | 2023-01-10 19:40:00 | 2023-01-10 19:59:59 | ЗАПИСАН ПАЦИЕНТ 4

Шаг 4 (Опционально): Агрегация смежных интервалов с одинаковым статусом

Если нам нужны не все элементарные интервалы, а только агрегированные длинные интервалы с одинаковым статусом (например, все последовательные "СВОБОДНО" объединить в один большой интервал), то после Шага 3 потребуется дополнительный запрос. Этот запрос будет находить "начало группы" и "конец группы" смежных интервалов с одинаковым статусом.

Для этого мы можем использовать оконные функции или последовательные соединения. Например, найти интервалы, где текущий статус отличается от предыдущего, чтобы определить начало нового блока.

Важные нюансы и дополнительные соображения:

  1. Точность интервалов: Определите необходимую точность (секунды, минуты). От этого зависит, как вы будете обрабатывать граничные условия (например, конец одного интервала в 08:20:00 и начало другого в 08:20:00). В нашем примере мы отнимаем 1 секунду от ДатаКонцаИнтервала, чтобы исключить пересечения по одной и той же точке.
  2. Перекрытие записей пациентов: Если допустимо, чтобы несколько пациентов были записаны на одно и то же время (например, на разные процедуры, но в один кабинет, или в разные кабинеты, но к одному врачу), то подход с элементарными интервалами все равно будет корректен. Запрос в Шаге 3 для статуса "ЗАПИСАН ПАЦИЕНТ" покажет первого найденного пациента, или же можно использовать ГРУППИРОВАТЬ ПО и ГРУППОВОЕ_СОЕДИНЕНИЕ() для получения списка всех пациентов, если таковые есть. Однако чаще всего, на одно и то же время в одном кабинете может быть записан только один пациент.
  3. Масштабируемость: Использование SQL-запросов и временных таблиц обычно обеспечивает хорошую производительность для большого объема данных, так как основная нагрузка ложится на СУБД.
  4. Контекст задачи: Важно помнить, что мы определяем плановое свободное время. Фактические опоздания или задержки пациентов — это другая задача, которая обычно решается на этапе оперативного учета, а не планирования.
  5. Гибкость определения ресурсов: Врач, кабинет, оборудование (например, МРТ) — все это может быть "ресурсом", для которого нужно составить расписание. Описанный алгоритм легко адаптируется под любой тип ресурса, достаточно добавить соответствующее поле для группировки (например, Кабинет вместо Врач).

В итоге, наиболее универсальным и надежным подходом для решения этой задачи является метод элементарных интервалов, реализованный с помощью мощных SQL-запросов. Он позволяет точно определить статус каждого мельчайшего промежутка времени и гибко обрабатывать различные сценарии пересечений.

← На главную