Нередко перед разработчиками 1С встает задача вычислить не просто календарную разницу между двумя датами, а определить точную продолжительность периода с учетом рабочего графика. Например, нужно рассчитать срок выполнения задачи, который должен измеряться только в рабочие часы по производственному календарю. Стандартные средства форматирования даты здесь бессильны, так как они работают со значением типа "Дата", а не с вычисляемым интервалом. В этой статье мы подробно разберем, как реализовать такой расчет с помощью кода на 1С.
Проанализируем задачу: нам нужно получить из двух дат (начала и окончания) строку вида "1 д 5 час 30 мин". При этом в расчет должны попадать только рабочие дни согласно производственному календарю и только рабочее время внутри этих дней (например, с 9:00 до 18:00).
Прежде чем приступать к написанию кода, определим логику вычислений. Весь интервал можно разбить на три части:
Такой подход позволяет корректно обработать "граничные" условия и избежать ошибок в расчетах. Главная задача — эффективно получить информацию о том, какие дни в нашем интервале являются рабочими. Делать это в цикле с запросом для каждого дня — крайне непроизводительно. Оптимальным решением будет получить все рабочие дни в заданном диапазоне одним запросом.
Рассмотрим финальный, оптимизированный вариант кода, который решает поставленную задачу. Он состоит из нескольких вспомогательных функций, что делает его более читаемым и удобным для отладки.
Основная управляющая функция — ПолучитьРабочихМинутМеждуДатами. Она принимает на вход даты начала и окончания, часы начала и окончания рабочего дня, а также опционально — конкретный календарь.
&НаСервере
Функция ПолучитьРабочихМинутМеждуДатами(ДатаНачала, ДатаОкончания, ЧасН, ЧасО, Календарь = Неопределено)
// Начальные проверки на корректность данных
Если ДатаНачала >= ДатаОкончания ИЛИ ЧасН < 0 ИЛИ ЧасО > 24 ИЛИ ЧасН >= ЧасО Тогда
Возврат 0;
КонецЕсли;
ПродолжительностьРабДня = (ЧасО - ЧасН) * 60;
НачалоДняН = НачалоДня(ДатаНачала);
НачалоДняК = НачалоДня(ДатаОкончания);
// Получаем все рабочие дни в интервале одним запросом
РабочиеДниМножество = ПолучитьРабочиеДниВИнтервале(НачалоДняН, НачалоДняК, Календарь);
// Обработка случая, когда обе даты в одном дне
Если НачалоДняН = НачалоДняК Тогда
Если РабочиеДниМножество.Получить(НачалоДняН) <> Неопределено Тогда
Возврат РасчетМинутВДень(ДатаНачала, ДатаОкончания, ЧасН, ЧасО);
Иначе
Возврат 0;
КонецЕсли;
КонецЕсли;
Общееминуты = 0;
// 1. Обрабатываем начальный день
Если РабочиеДниМножество.Получить(НачалоДняН) <> Неопределено Тогда
Общееминуты = Общееминуты + РасчетМинутВДень(ДатаНачала, КонецДня(НачалоДняН), ЧасН, ЧасО);
КонецЕсли;
// 2. Обрабатываем конечный день
Если РабочиеДниМножество.Получить(НачалоДняК) <> Неопределено Тогда
Общееминуты = Общееминуты + РасчетМинутВДень(НачалоДня(НачалоДняК), ДатаОкончания, ЧасН, ЧасО);
КонецЕсли;
// 3. Обрабатываем полные дни между
КоличествоДней = РабочиеДниМножество.Количество();
// Вычитаем начальный и конечный дни, если они рабочие и уже были посчитаны
Если РабочиеДниМножество.Получить(НачалоДняН) <> Неопределено Тогда
КоличествоДней = КоличествоДней - 1;
КонецЕсли;
Если РабочиеДниМножество.Получить(НачалоДняК) <> Неопределено Тогда
КоличествоДней = КоличествоДней - 1;
КонецЕсли;
// Убедимся, что количество дней не отрицательное
КоличествоДней = Макс(КоличествоДней, 0);
Общееминуты = Общееминуты + КоличествоДней * ПродолжительностьРабДня;
Возврат Общееминуты;
КонецФункции
Ключевая часть алгоритма — функция ПолучитьРабочиеДниВИнтервале. Она выполняет запрос к регистру сведений КалендарныеГрафики, который хранит информацию о производственных календарях. Запрос выбирает все даты из указанного диапазона, которые помечены как рабочие (ДеньВключенВГрафик = Истина). Результат запроса для удобства и скорости доступа помещается в структуру данных Соответствие.
&НаСервере
Функция ПолучитьРабочиеДниВИнтервале(ДатаНачала, ДатаОкончания, Календарь = Неопределено)
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| т1.ДатаГрафика КАК Дата
|ИЗ
| РегистрСведений.КалендарныеГрафики КАК т1
|ГДЕ
| т1.ДатаГрафика >= &ДатаНачала
| И т1.ДатаГрафика <= &ДатаОкончания
| И т1.ДеньВключенВГрафик = Истина";
Если Календарь <> Неопределено Тогда
Запрос.Текст = Запрос.Текст + " И т1.Календарь = &Календарь";
Запрос.УстановитьПараметр("Календарь", Календарь);
КонецЕсли;
Запрос.УстановитьПараметр("ДатаНачала", ДатаНачала);
Запрос.УстановитьПараметр("ДатаОкончания", ДатаОкончания);
РезультатЗапроса = Запрос.Выполнить();
Выборка = РезультатЗапроса.Выбрать();
// Используем Соответствие для быстрой проверки принадлежности даты к рабочим дням
Результат = Новый Соответствие;
Пока Выборка.Следующий() Цикл
Результат.Вставить(Выборка.Дата, Истина);
КонецЦикла;
Возврат Результат;
КонецФункции
Важный момент: в запросе также можно учитывать предпраздничные дни. Для этого нужно анализировать поле с видом дня (например, ГрафикиРаботы.ВидДня) и проверять его на значения ЗНАЧЕНИЕ(Перечисление.ВидыДнейПроизводственногоКалендаря.Рабочий) и ЗНАЧЕНИЕ(Перечисление.ВидыДнейПроизводственногоКалендаря.Предпраздничный). В этом случае для предпраздничных дней нужно будет использовать сокращенную продолжительность рабочего дня.
Функция РасчетМинутВДень отвечает за вычисление рабочих минут в рамках одного дня. Она принимает дату начала, дату окончания и границы рабочего времени. Логика функции "обрезает" переданный интервал так, чтобы он не выходил за рамки рабочего дня.
Например, если задача началась в 8:00, а рабочий день с 9:00, то отсчет начнется с 9:00. Аналогично, если задача закончилась в 20:00, а рабочий день до 18:00, то в зачет пойдет время только до 18:00.
&НаСервере
Функция РасчетМинутВДень(ДатаНачала, ДатаОкончания, ЧасН, ЧасО)
НачалоРабочегоДня = НачалоДня(ДатаНачала) + ЧасН * 3600;
КонецРабочегоДня = НачалоДня(ДатаНачала) + ЧасО * 3600;
// Ограничиваем расчет рабочим временем дня
Если ДатаНачала < НачалоРабочегоДня Тогда
ДатаНачала = НачалоРабочегоДня;
КонецЕсли;
Если ДатаОкончания > КонецРабочегоДня Тогда
ДатаОкончания = КонецРабочегоДня;
КонецЕсли;
// Если после "обрезки" интервал стал некорректным, возвращаем 0
Если ДатаНачала >= ДатаОкончания Тогда
Возврат 0;
КонецЕсли;
Возврат (ДатаОкончания - ДатаНачала) / 60;
КонецФункции
После того как функция ПолучитьРабочихМинутМеждуДатами вернет общее количество рабочих минут, его необходимо преобразовать в требуемый строковый формат. Для этого используются операции целочисленного деления и получения остатка от деления.
// Пример использования
ОбщееКоличествоМинут = ПолучитьРабочихМинутМеждуДатами(ДатаНачалаСобытия, ДатаОкончанияСобытия, 9, 18, МойКалендарь);
Если ОбщееКоличествоМинут > 0 Тогда
МинутВЧасе = 60;
МинутВДне = 24 * МинутВЧасе;
Дней = Цел(ОбщееКоличествоМинут / МинутВДне);
ОстатокМинут = ОбщееКоличествоМинут % МинутВДне;
Часов = Цел(ОстатокМинут / МинутВЧасе);
Минут = ОстатокМинут % МинутВЧасе;
РезультатСтрокой = Формат(Дней, "ЧГ=0") + " д " + Формат(Часов, "ЧГ=0") + " час " + Формат(Минут, "ЧГ=0") + " мин";
// РезультатСтрокой будет иметь вид, например, "1 д 5 час 30 мин"
Иначе
РезультатСтрокой = "0 д 0 час 0 мин";
КонецЕсли;
Для более красивого вывода с правильными склонениями ("1 день", "2 дня", "5 дней") рекомендуется использовать специальные функции. В Библиотеке стандартных подсистем (БСП) есть готовые решения, например, в общем модуле ОбщегоНазначенияКлиентСервер.СформироватьСтрокуСЧислом(), которые позволяют корректно склонять существительные после числительных.
Стоит отметить, что в некоторых типовых конфигурациях, особенно в 1С:ЗУП, ERP и КА, могут существовать встроенные функции для работы с графиками и календарями. Перед разработкой собственного решения всегда полезно изучить общие модули конфигурации на предмет наличия готового API для расчета разности дат по графикам. Это может значительно сократить время разработки.
Представленный код решает задачу для фиксированного рабочего графика (например, пятидневка с 9 до 18). В реальной жизни могут встречаться более сложные сценарии:
Для таких случаев алгоритм потребуется существенно усложнить, получая данные не из общего производственного календаря, а из регистров, хранящих информацию о рабочем времени конкретных сотрудников.
Тем не менее, представленный подход является надежной и производительной основой для решения большинства задач по расчету рабочего времени в 1С.