При разработке бизнес-логики в среде «1С:Предприятие» программисты регулярно сталкиваются с необходимостью расчета дат: определением ближайшего рабочего дня, вычислением даты окончания срока договора или проверкой производственного календаря. Одной из классических задач является поиск предыдущего рабочего дня относительно заданной даты.
Начинающие разработчики часто пытаются решить эту задачу с помощью рекурсивного вызова функций. Однако такой подход таит в себе скрытые архитектурные проблемы, логические ошибки и риски снижения производительности системы. В этой статье мы подробно разберем, почему «ломается» рекурсивный алгоритм поиска даты, как исправить эту ошибку и какие профессиональные альтернативные подходы существуют в платформе 1С.
Проанализируем исходный код функции, предложенный автором темы на форуме:
Функция ПолучитьПредыдущийРабочийДень(ПроверяемаяДата)
ПроверяемаяДата = ПроверяемаяДата - 86400;
Нашли = ГрафикиРаботы.ЭтоРабочийДень(ПроверяемаяДата, Справочники.ГрафикиРаботы.НайтиПоНаименованию("Основной график"));
Если Нашли Тогда
Возврат ПроверяемаяДата;
Иначе
ПолучитьПредыдущийРабочийДень(ПроверяемаяДата);
КонецЕсли;
КонецФункции
При пошаговой отладке этого кода разработчик замечает странное поведение: когда алгоритм доходит до нужной рабочей даты, управление заходит в ветку Если Нашли Тогда, срабатывает оператор Возврат, однако в самый первый (вызывающий) уровень рекурсии возвращается значение Неопределено.
Выясним причину этой ошибки. Дело в том, что рекурсивный вызов функции порождает новые уровни (кадры) в стеке вызовов. Каждый уровень должен явно передать полученное значение на уровень выше. В ветке Иначе автор просто вызывает функцию ПолучитьПредыдущийРабочийДень(ПроверяемаяДата), но никак не использует и не возвращает результат этого вызова.
Когда самый глубокий уровень рекурсии находит рабочую дату, он возвращает её на предыдущий уровень. Но на предыдущем уровне выполнение кода в ветке Иначе завершается, управление переходит к концу функции, и так как оператор Возврат для данной ветки не был указан, функция неявно возвращает дефолтное значение — Неопределено. Этот пустой результат транслируется по всей цепочке вызовов обратно к пользователю.
Чтобы рекурсивный вариант работал корректно, строчку в секции Иначе обязательно нужно дополнить оператором Возврат:
Функция ПолучитьПредыдущийРабочийДень(ПроверяемаяДата)
ПроверяемаяДата = ПроверяемаяДата - 86400;
Нашли = ГрафикиРаботы.ЭтоРабочийДень(ПроверяемаяДата, Справочники.ГрафикиРаботы.НайтиПоНаименованию("Основной график"));
Если Нашли Тогда
Возврат ПроверяемаяДата;
Иначе
// Обязательно возвращаем результат рекурсивного вызова!
Возврат ПолучитьПредыдущийРабочийДень(ПроверяемаяДата);
КонецЕсли;
КонецФункции
Несмотря на то, что это исправляет синтаксическую логику, использовать рекурсию для решения подобных задач в реальных проектах категорически не рекомендуется. Если в базе данных не будет заполнен производственный календарь (например, пустые графики на годы вперед или назад), программа уйдет в бесконечную рекурсию и аварийно завершит работу с ошибкой «Переполнение стека» (Stack Overflow).
Для исключения рисков переполнения стека и упрощения отладки рекурсию следует заменить обычным циклом. Мы перекладываем итерационный поиск даты на простую циклическую конструкцию с ограничением максимальной глубины поиска (например, 30 или 365 дней), чтобы избежать зависания системы при незаполненных календарях.
Рассмотрим пример правильной реализации поиска через цикл:
Функция ПолучитьПредыдущийРабочийДень(Знач ПроверяемаяДата, Знач ГрафикСсылка = Неопределено, ГлубинаПоиска = 30) Экспорт
Если ГрафикСсылка = Неопределено Тогда
// Избегаем поиска по наименованию в цикле, получаем ссылку один раз
ГрафикСсылка = Справочники.ГрафикиРаботы.НайтиПоНаименованию("Основной график");
КонецЕсли;
РабочийДень = Неопределено;
ТекущаяДата = НачалоДня(ПроверяемаяДата);
Для НомерДня = 1 По ГлубинаПоиска Цикл
ТекущаяДата = ТекущаяДата - 86400; // Сдвиг на 1 день назад
Если ГрафикиРаботы.ЭтоРабочийДень(ТекущаяДата, ГрафикСсылка) Тогда
РабочийДень = ТекущаяДата;
Прервать; // День найден, прерываем цикл
КонецЕсли;
КонецЦикла;
Возврат РабочийДень;
КонецФункции
Этот код гораздо надежнее рекурсивного. Он гарантирует, что даже при полной поломке производственного календаря алгоритм сделает не более ГлубинаПоиска итераций и вернет управляемое значение Неопределено, которое система сможет корректно обработать без аварийного падения.
Даже перейдя на циклы, многие разработчики допускают грубые архитектурные ошибки, существенно снижающие быстродействие системы. Проанализируем их подробнее:
ГрафикиРаботы.ЭтоРабочийДень() под капотом обращается к базе данных (обычно к регистру сведений с графиками работы). Вызывая эту функцию внутри цикла, мы генерируем десятки мелких запросов к СУБД. Это классический антипаттерн «запросы в цикле», приводящий к сильным задержкам при многопользовательской работе.Справочники.ГрафикиРаботы.НайтиПоНаименованию() выполняет тяжелый неявный запрос к СУБД для поиска ссылки. Поиск ссылки должен производиться строго один раз до начала цикла, а еще лучше — передаваться в функцию в качестве готового параметра.86400) очень важно приводить проверяемую дату к началу дня с помощью функции НачалоДня(). В противном случае из-за накопившейся разницы в часах и минутах сравнение дат в СУБД может отработать некорректно.Как же решить задачу эффективно и профессионально? Рассмотрим два наиболее оптимальных подхода.
Если ваша конфигурация построена на базе БСП (а это практически все типовые решения 1С: ERP, ЗУП, КА, УТ, БП), вам вообще не нужно писать собственные велосипеды с циклами. В БСП уже разработан быстрый, оптимизированный и кэшируемый механизм для работы с календарями.
Для решения нашей задачи идеально подходит функция ДатыБлижайшихРабочихДней из общего модуля КалендарныеГрафики.
Посмотрим на пример её применения:
// Готовим исходные данные
МассивНачальныхДат = Новый Массив;
// Начинаем поиск со вчерашнего дня относительно проверяемой даты
МассивНачальныхДат.Добавить(НачалоДня(ПроверяемаяДата) - 86400);
// Получаем соответствие рабочих дат через БСП
СоответствиеРабочихДат = КалендарныеГрафики.ДатыБлижайшихРабочихДней(
СсылкаНаГрафик,
МассивНачальныхДат,
Истина // ПолучатьПредшествующие = Истина (ищет в прошлое)
);
// Извлекаем результат
ПредыдущийРабочийДень = СоответствиеРабочихДат.Получить(МассивНачальныхДат[0]);
В чем преимущество данного метода? Метод БСП спроектирован с учетом пакетных запросов к базе данных и механизмов внутреннего кэширования. Он отрабатывает мгновенно и не создает лишней нагрузки на сервер СУБД.
Если разработка ведется в полностью самописной конфигурации, где нет встроенных модулей БСП, наилучшим решением будет использование прямого запроса к базе данных. Это позволяет получить нужную дату за одно обращение к СУБД без циклов и рекурсий.
Информация о графиках и рабочих днях в 1С обычно хранится в регистре сведений (например, РегистрСведений.КалендарныеГрафики). Чтобы получить ближайший предшествующий рабочий день, мы выберем ровно одну запись (ВЫБРАТЬ ПЕРВЫЕ 1), дата которой строго меньше проверяемой, отсортировав результат по убыванию даты.
Разберем реализацию этого метода на языке 1С:
Функция ПолучитьПредыдущийРабочийДеньЗапросом(Знач ПроверяемаяДата, СсылкаНаГрафик) Экспорт
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ ПЕРВЫЕ 1
| КалендарныеГрафики.ДатаГрафика КАК ДатаГрафика
|ИЗ
| РегистрСведений.КалендарныеГрафики КАК КалендарныеГрафики
|ГДЕ
| КалендарныеГрафики.ГрафикРаботы = &ГрафикРаботы
| И КалендарныеГрафики.ДеньВключенВГрафик = ИСТИНА
| И КалендарныеГрафики.ДатаГрафика < &ПроверяемаяДата
|
|УПОРЯДОЧИТЬ ПО
| ДатаГрафика УБЫВ";
Запрос.УстановитьПараметр("ГрафикРаботы", СсылкаНаГрафик);
Запрос.УстановитьПараметр("ПроверяемаяДата", НачалоДня(ПроверяемаяДата));
РезультатЗапроса = Запрос.Выполнить();
Если Не РезультатЗапроса.Пустой() Тогда
Выборка = РезультатЗапроса.Выбрать();
Выборка.Следующий();
Возврат Выборка.ДатаГрафика;
Иначе
// Обработка ситуации, когда график в прошлых периодах не заполнен
Возврат Неопределено;
КонецЕсли;
КонецФункции
Почему этот подход считается лучшим в промышленной разработке?
Подводя итог, при решении задач с поиском дат в 1С всегда старайтесь избегать рекурсивных методов. Используйте готовый инструментарий БСП, а при его отсутствии отдавайте предпочтение прямым запросам к базе данных с корректным приведением дат к началу дня.