При работе с планированием ресурсов, таких как кабинеты или время сотрудников, часто возникает задача определения свободных временных интервалов. Нам необходимо взять общий график работы и «вычесть» из него все занятые периоды, чтобы получить отчет по доступному времени. Рассмотрим, как это можно реализовать в 1С, используя различные подходы.
Исходные данные, как правило, представлены в виде двух таблиц значений или временных таблиц:
ТзГрафикРаботы: График работы сотрудника или доступности кабинета. Каждая строка представляет собой интервал работы.ТЗПациенты: Записи пациентов или занятость кабинета. Каждая строка также представляет собой интервал.Приведем пример структуры данных:
ТзГрафикРаботы:
Сотрудник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, 5, 10 или 15 минут. Затем мы работаем уже с этими дискретными "слотами".
Суть метода:
Шаги реализации (на примере генерации минутной временной оси):
Сначала создадим временную ось. Это можно сделать программно в цикле:
Функция СформироватьВременнуюОсь(НачалоПериода, КонецПериода, ИнтервалВСекундах = 60)
ТаблицаВремени = Новый ТаблицаЗначений;
ТаблицаВремени.Колонки.Добавить("Период");
ТекущееВремя = НачалоПериода;
Пока ТекущееВремя < КонецПериода Цикл
НоваяСтрока = ТаблицаВремени.Добавить();
НоваяСтрока.Период = ТекущееВремя;
ТекущееВремя = ДобавитьКДате(ТекущееВремя, ВидДаты.Секунда, ИнтервалВСекундах);
КонецЦикла;
Возврат ТаблицаВремени;
КонецФункции
// Пример использования:
НачалоДня = НачалоДня(ТекущаяДата());
КонецДня = КонецДня(ТекущаяДата());
ВременнаяОсь = СформироватьВременнуюОсь(НачалоДня, КонецДня);
Затем, для каждого интервала из ВременнаяОсь мы можем программно проверить его статус или использовать запросы к этой оси, если она помещена во временную таблицу. Этот подход более трудоемок в программной реализации проверки условий, но прост в концепции.
Этот подход является более мощным и гибким, особенно для обработки сложных пересечений и большого объема данных. Он основан на идее объединения всех ключевых временных точек и последующего формирования элементарных интервалов.
Ключевая идея: Мы собираем все моменты начала и окончания всех интервалов (как рабочего графика, так и записей пациентов) в один список. Затем сортируем этот список и формируем новые, самые мелкие интервалы, границы которых определяются этими объединенными точками. Для каждого такого элементарного интервала мы легко можем определить его статус, проверяя, попадает ли он в исходные интервалы графика и записей.
Разберем этот подход по шагам, используя язык запросов 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:
ВТ_УникальныеДаты собирает все уникальные даты начала и конца из обеих таблиц, группируя их по врачу. Это создает набор всех "переломных" точек на временной оси.ВТ_ЭлементарныеИнтервалы использует СЛЕДУЮЩИЙ() (аналог SQL LEAD()) для каждой уникальной даты. Он берет текущую дату (ДатаТочка) как начало нового элементарного интервала и следующую уникальную дату (от которой отнимается 1 секунда, чтобы конец интервала был включительно, если не хотите пересечений по секунде) как конец интервала. Функция ДОБАВИТЬКДАТЕ(..., СЕКУНДА, -1) корректирует конечную дату.УПОРЯДОЧИТЬ ПО ДатаТочка внутри СЛЕДУЮЩИЙ() гарантирует правильный порядок следования дат.ПО Врач в СЛЕДУЮЩИЙ() указывает на группировку по врачу, чтобы интервалы формировались корректно для каждого сотрудника независимо.НЕ СЛЕДУЮЩИЙ(...) ЕСТЬ NULL отсекает последнюю дату для каждого врача, поскольку для нее нет "следующей" даты.Шаг 3: Определение статуса каждого элементарного интервала
Теперь, когда у нас есть все элементарные интервалы, мы можем присоединить к ним исходные таблицы графика и пациентов, чтобы определить статус каждого интервала.
ВЫБРАТЬ
ЭИ.Врач,
ЭИ.ДатаНачалаИнтервала,
ЭИ.ДатаКонцаИнтервала,
ВЫБОР
КОГДА График.Врач ЕСТЬ NULL ТОГДА "НЕ РАБОТАЕТ"
КОГДА Пациент.Врач ЕСТЬ NULL ТОГДА "СВОБОДНО"
ИНАЧЕ "ЗАПИСАН ПАЦИЕНТ " + СТРОКА(Пациент.Пациент)
КОНЕЦ КАК Статус
ИЗ
ВТ_ЭлементарныеИнтервалы КАК ЭИ
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_График КАК График
ПО ЭИ.Врач = График.Врач
И ЭИ.ДатаКонцаИнтервала МЕЖДУ График.ДатаНачало И График.ДатаКонец
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Пациенты КАК Пациент
ПО ЭИ.Врач = Пациент.Врач
И ЭИ.ДатаКонцаИнтервала МЕЖДУ Пациент.ДатаНачало И Пациент.ДатаКонец
ГДЕ
НЕ ЭИ.ДатаКонцаИнтервала ЕСТЬ NULL
Пояснения к Шагу 3:
ЛЕВОЕ СОЕДИНЕНИЕ с таблицами ВТ_График и ВТ_Пациенты. Это важно, потому что нам нужны все элементарные интервалы, даже те, которые не попадают ни в график, ни в записи.ЭИ.ДатаКонцаИнтервала МЕЖДУ .... Здесь мы проверяем, попадает ли хотя бы одна точка из нашего элементарного интервала в интервал графика или пациента. Если интервал [Начало; Конец], то достаточно проверить ЭИ.ДатаНачалаИнтервала >= График.ДатаНачало И ЭИ.ДатаКонцаИнтервала <= График.ДатаКонец (полное вхождение). Если же мы используем МЕЖДУ, это более общее условие. В случае, когда элементарные интервалы короткие (например, по 1 секунде), можно просто использовать ЭИ.ДатаНачалаИнтервала МЕЖДУ График.ДатаНачало И График.ДатаКонец.ВЫБОР КОГДА ... ИНАЧЕ ... КОНЕЦ позволяет нам определить статус. Если для интервала не нашлось соответствия в ВТ_График, значит, в это время сотрудник не работает (статус "НЕ РАБОТАЕТ"). Если нашлось в графике, но не нашлось в ВТ_Пациенты, то это свободное время (статус "СВОБОДНО"). В противном случае, кабинет занят пациентом (статус "ЗАПИСАН ПАЦИЕНТ").Примерный результат выполнения запроса для Врача 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 потребуется дополнительный запрос. Этот запрос будет находить "начало группы" и "конец группы" смежных интервалов с одинаковым статусом.
Для этого мы можем использовать оконные функции или последовательные соединения. Например, найти интервалы, где текущий статус отличается от предыдущего, чтобы определить начало нового блока.
Важные нюансы и дополнительные соображения:
ДатаКонцаИнтервала, чтобы исключить пересечения по одной и той же точке.ГРУППИРОВАТЬ ПО и ГРУППОВОЕ_СОЕДИНЕНИЕ() для получения списка всех пациентов, если таковые есть. Однако чаще всего, на одно и то же время в одном кабинете может быть записан только один пациент.Кабинет вместо Врач).В итоге, наиболее универсальным и надежным подходом для решения этой задачи является метод элементарных интервалов, реализованный с помощью мощных SQL-запросов. Он позволяет точно определить статус каждого мельчайшего промежутка времени и гибко обрабатывать различные сценарии пересечений.