При разработке систем учета рабочего времени, интеграции со СКУД (системами контроля и управления доступом) или расчете заработной платы программисты 1С регулярно сталкиваются с задачей сопоставления двух временных интервалов. Нам часто требуется определить, пересекается ли фактическое время нахождения сотрудника на работе с его плановой сменой, и если да, то в каком объеме.
На первый взгляд задача кажется тривиальной, однако при ее реализации в запросах 1С легко совершить логические ошибки, из-за которых система будет выдавать неверные результаты в пограничных сценариях. В этой статье мы подробно разберем, почему стандартные подходы с оператором МЕЖДУ часто не работают, проанализируем решение автора темы, а также рассмотрим классические, наиболее производительные и элегантные алгоритмы решения этой задачи как в запросах, так и во встроенном языке 1С.
Первое решение, которое приходит на ум начинающему разработчику — использовать встроенный оператор МЕЖДУ (аналог BETWEEN в SQL) для проверки того, попадают ли границы фактической работы внутрь плановой смены. Давайте проанализируем типичный пример такого условия:
ВЫБОР
КОГДА ФактическоеНачало МЕЖДУ ПлановоеНачало И ПлановоеОкончание
ИЛИ ФактическоеОкончание МЕЖДУ ПлановоеНачало И ПлановоеОкончание
ТОГДА ИСТИНА
ИНАЧЕ ЛОЖЬ
КОНЕЦ
Кажется, что это условие покрывает большинство ситуаций. Действительно, если сотрудник пришел или ушел во время смены, система вернет значение ИСТИНА. Однако давайте рассмотрим критический сценарий, упомянутый в Сообщении 15 обсуждения:
Подставим эти значения в наше "наивное" условие:
ЛОЖЬ ИЛИ ЛОЖЬ равен ЛОЖЬ.Несмотря на то, что сотрудник полностью отработал всю плановую смену (и даже перевыполнил ее, придя раньше и уйдя позже), система покажет, что его рабочее время не пересекается со сменой. Это происходит потому, что плановый интервал полностью поглощен фактическим интервалом работы, и ни одна из его границ не попала внутрь.
Чтобы обойти проблему поглощения одного интервала другим, автор темы в итоге пришел к следующему рабочему, но крайне тяжеловесному и избыточному условию в запросе:
ВЫБОР
КОГДА ВТПроверка.ВремяНачала МЕЖДУ СменыРаботыСотрудниковРасписание.ВремяНачала И СменыРаботыСотрудниковРасписание.ВремяОкончания
ИЛИ ВТПроверка.ВремяОкончания МЕЖДУ СменыРаботыСотрудниковРасписание.ВремяНачала И СменыРаботыСотрудниковРасписание.ВремяОкончания
ИЛИ ВТПроверка.ВремяНачала <= СменыРаботыСотрудниковРасписание.ВремяНачала
И ВТПроверка.ВремяОкончания >= СменыРаботыСотрудниковРасписание.ВремяОкончания
ТОГДА ИСТИНА
ИНАЧЕ ЛОЖЬ
КОНЕЦ
Давайте проанализируем эту конструкцию. Автор добавил третью ветку проверки: если фактическое начало меньше или равно плановому началу, а фактическое окончание больше или равно плановому окончанию (то есть произошло полное поглощение интервала), то это тоже считается пересечением.
Данный код действительно решает задачу и корректно отрабатывает все возможные геометрические варианты взаимного расположения двух отрезков на временной шкале. Однако с точки зрения производительности базы данных и лаконичности кода у него есть серьезные недостатки:
ИЛИ, что затрудняет построение оптимального плана выполнения запроса в СУБД (индексы могут не использоваться должным образом).В теории множеств и проектировании баз данных существует классическое изящное решение задачи пересечения интервалов. В Сообщении 5 один из участников форума совершенно верно указал направление мысли: легче идти от обратного.
Два интервала [A1, A2] и [B1, B2] НЕ пересекаются только в двух случаях:
A2 < B1.A1 > B2.Следовательно, условием НЕпересечения является выражение: (A2 < B1) ИЛИ (A1 > B2).
Применив к этому выражению закон де Моргана (отрицание дизъюнкции), мы получаем максимально простое и математически безупречное условие пересечения двух интервалов:
A1 <= B2 И A2 >= B1
Давайте переведем эту формулу на язык запросов 1С. Пусть первый интервал — это фактическое время работы сотрудника (ВремяНачалаРаботы, ВремяОкончанияРаботы), а второй — интервал смены (ВремяНачалаСмены, ВремяОкончанияСмены). Запрос будет выглядеть следующим образом:
ВЫБОР
КОГДА ВремяНачалаРаботы <= ВремяОкончанияСмены
И ВремяОкончанияРаботы >= ВремяНачалаСмены
ТОГДА ИСТИНА
ИНАЧЕ ЛОЖЬ
КОНЕЦ
Давайте проверим эту формулу на наших проблемных примерах:
08:00 <= 17:00 (Да) И 15:00 >= 06:00 (Да). Итог: ИСТИНА.
05:00 <= 17:00 (Да) И 18:00 >= 06:00 (Да). Итог: ИСТИНА.
00:00 <= 18:00 (Да) И 06:00 >= 16:00 (Нет). Итог: ЛОЖЬ.
Как видим, эта простая формула из двух сравнений безошибочно обрабатывает абсолютно все сценарии взаимного расположения интервалов. Она работает быстрее, не нагружает СУБД и легко читается в коде.
Очень часто бизнесу недостаточно просто получить булево значение "пересекаются или нет". Требуется узнать точную длительность пересечения. Например, если сотрудник пришел в 07:00, а ушел в 16:00 при смене с 06:00 до 15:00, то время пересечения составляет ровно 8 часов (с 07:00 до 15:00).
Для вычисления точных границ пересечения нам нужно найти:
Макс(Начало1, Начало2).Мин(Конец1, Конец2).В языке запросов 1С нет прямых функций Макс и Мин для строк таблицы (они работают только как агрегатные функции группировки). Поэтому мы реализуем логику выбора максимума и минимума через конструкцию ВЫБОР КОГДА. Посмотрим на готовый пример запроса, который вычисляет время пересечения в часах:
ВЫБРАТЬ
ВЫБОР
// Сначала проверяем, есть ли вообще пересечение по нашей классической формуле
КОГДА ВремяНачалаРаботы <= ВремяОкончанияСмены
И ВремяОкончанияРаботы >= ВремяНачалаСмены
ТОГДА РАЗНОСТЬДАТ(
// Находим Максимум из дат начала
ВЫБОР
КОГДА ВремяНачалаРаботы > ВремяНачалаСмены
ТОГДА ВремяНачалаРаботы
ИНАЧЕ ВремяНачалаСмены
КОНЕЦ,
// Находим Минимум из дат окончания
ВЫБОР
КОГДА ВремяОкончанияРаботы < ВремяОкончанияСмены
ТОГДА ВремяОкончанияРаботы
ИНАЧЕ ВремяОкончанияСмены
КОНЕЦ,
СЕКУНДА
) / 3600 // Переводим секунды в часы
ИНАЧЕ 0
КОНЕЦ КАК ЧасовВ Смене
ИЗ
ВТДанные КАК ВТДанные
Этот запрос вернет точное количество часов (дробным числом), которые сотрудник отработал непосредственно внутри своей плановой смены.
Если задача проверки пересечения решается не в запросе к базе данных, а в процедурах встроенного языка 1С (например, при обработке табличной части на клиенте или сервере), мы можем написать исключительно лаконичный код. Платформа 1С содержит глобальные функции Макс() и Мин(), что позволяет нам использовать следующее изящное свойство: если максимум из дат начала меньше минимума из дат окончания, периоды пересекаются.
// Очищенный и лаконичный способ проверки пересечения двух периодов
ИнтервалыПересекаются = Макс(НачалоРаботы, НачалоСмены) < Мин(ОкончаниеРаботы, ОкончаниеСмены);
Если ИнтервалыПересекаются Тогда
// Вычисляем длительность пересечения в секундах
СекундОтработано = Мин(ОкончаниеРаботы, ОкончаниеСмены) - Макс(НачалоРаботы, НачалоСмены);
ЧасовОтработано = СекундОтработано / 3600;
КонецЕсли;
Этот код легко читается, быстро выполняется и не требует написания сложных ветвлений Если ... Тогда ... Иначе.
Рассмотрим подводный камень, о котором часто забывают при проектировании подобных алгоритмов. В СКУД и кадровых системах время работы часто хранится в виде простых значений типа "Время" (например, 08:00, 22:00, 06:00). На уровне платформы 1С такие значения физически записываются как дата начала нашей эры: 0001-01-01 HH:MM:SS.
Что произойдет, если сотрудник работает в ночную смену, например, с 22:00 до 06:00 следующего дня?
Если мы будем сравнивать эти значения "в лоб" как время, стандартные формулы полностью сломаются. В базе данных значение окончания смены (01.01.0001 06:00:00) математически меньше значения начала смены (01.01.0001 22:00:00). Любое сравнение вида ВремяОкончания >= ВремяНачала вернет ложный результат, так как система считает, что 6 часов утра — это "раньше", чем 10 часов вечера того же самого условного дня.
Для корректного сопоставления интервалов нам необходимо выполнить нормализацию дат — перевести относительное время суток в сквозную непрерывную шкалу времени (привязать к реальной дате смены). Рассмотрим, как это сделать в запросе 1С:
ДатаСмены = 15.10.2023 00:00:00).Пример реализации нормализации границ смены в запросе 1С:
ВЫБРАТЬ
ВТ_Смены.Сотрудник,
// Вычисляем реальное сквозное начало смены
ДОБАВИТЬКДАТЕ(ВТ_Смены.ДатаСмены, СЕКУНДА, ВТ_Смены.СменаНачалоСекунда) КАК РеальноеНачалоСмены,
// Вычисляем реальный конец смены с учетом перехода через полночь
ВЫБОР
КОГДА ВТ_Смены.СменаОкончаниеСекунда < ВТ_Смены.СменаНачалоСекунда
// Если конец меньше начала, значит смена закончилась на следующий день
ТОГДА ДОБАВИТЬКДАТЕ(ДОБАВИТЬКДАТЕ(ВТ_Смены.ДатаСмены, СЕКУНДА, ВТ_Смены.СменаОкончаниеСекунда), ДЕНЬ, 1)
ИНАЧЕ
ДОБАВИТЬКДАТЕ(ВТ_Смены.ДатаСмены, СЕКУНДА, ВТ_Смены.СменаОкончаниеСекунда)
КОНЕЦ КАК РеальноеОкончаниеСмены
ПОМЕСТИТЬ ВТ_НормализованныеСмены
ИЗ
ВТ_Смены КАК ВТ_Смены
Аналогичную нормализацию необходимо провести и для фактического времени работы сотрудника (ведь он также мог прийти вечером, а уйти утром следующего дня). После того как обе пары дат приведены к непрерывной шкале реального времени (например, начало работы 15.10.2023 23:00, окончание работы 16.10.2023 07:00), вы можете смело применять нашу классическую формулу пересечения:
РеальноеНачалоРаботы <= РеальноеОкончаниеСмены И РеальноеОкончаниеРаботы >= РеальноеНачалоСмены
Этот подход гарантирует 100% точность расчетов в абсолютно любых конфигурациях графиков работы: дневных, ночных, суточных или скользящих.