Задача вывода списка сотрудников-именинников при старте программы или по расписанию — одна из классических учебных и практических задач в разработке на платформе 1С:Предприятие. На первый взгляд алгоритм кажется простым: получить текущую дату, прибавить несколько дней и выбрать сотрудников из справочника. Однако на практике разработчики часто сталкиваются с логическими ошибками, из-за которых запрос возвращает пустой результат или работает некорректно при смене месяца и года.
В этой статье мы подробно разберем все ошибки исходного кода, выясним причины их возникновения и рассмотрим три надежных способа реализации выборки именинников на языке запросов 1С.
Рассмотрим исходный фрагмент кода, который вызвал затруднения:
Запрос = Новый Запрос;
Запрос.Текст = "ВЫБРАТЬ
| Сотрудники.Фамилия,
| Сотрудники.Имя,
| Сотрудники.Отчество,
| Сотрудники.ДатаРождения
|ИЗ
| Справочник.Сотрудники КАК Сотрудники
|ГДЕ
| Сотрудники.ДатаРождения МЕЖДУ &ДатаНачала И &ДатаКонец";
Запрос.УстановитьПараметр("ДатаКонец", НачалоДня(ТекущаяДата));
Запрос.УстановитьПараметр("ДатаНачала", КонецДня(ТекущаяДата+3*86400));
РезультатЗапроса = Запрос.Выполнить;
ВыборкаДетальныеЗаписи = РезультатЗапроса.Выбрать;
Пока ВыборкаДетальныеЗаписи.Следующий Цикл
КонецЦикла;
Сообщить("Скоро День Рождения " + ВыборкаДетальныеЗаписи.Фамилия + ВыборкаДетальныеЗаписи.Имя+ВыборкаДетальныеЗаписи.ДатаРождения);
Разберем по шагам, почему данный код не работает и возвращает пустой результат:
Сотрудники.ДатаРождения МЕЖДУ &ДатаНачала И &ДатаКонец сравнивает дату рождения целиком, включая год. Если сотрудник родился в 1985 году, а параметры передают даты текущего года (например, 2024), условие никогда не выполнится. Исключением будут только младенцы, родившиеся буквально на днях.МЕЖДУ: В языке запросов 1С конструкция Поле МЕЖДУ Значение1 И Значение2 требует, чтобы Значение1 было строго меньше или равно Значение2. В примере параметр &ДатаНачала был установлен на 3 дня вперед, а &ДатаКонец — на сегодня. При таком порядке оператор всегда возвращает Ложь.Сообщить(...) расположен после ключевого слова КонецЦикла. Цикл Пока Выборка.Следующий() отрабатывает вхолостую, курсор доходит до конца выборки, и обращение к полям после цикла приводит либо к пустой строке, либо к ошибке выполнения.ТекущаяДата(), Запрос.Выполнить(), Результат.Выбрать()).ДЕНЬ(Дата) МЕЖДУ &ДеньНачала И &ДеньКонец И МЕСЯЦ(Дата) МЕЖДУ &МесяцНачала И &МесяцКонец полностью ломается при переходе между месяцами. Например, если сегодня 30 января, а мы смотрим на 3 дня вперед (до 2 февраля), день начала (30) больше дня окончания (2), и условие 30 МЕЖДУ 30 И 2 вернет Ложь. Аналогично ситуация сломается на стыке декабря и января при смене года.ТекущаяДата() + 3 * 86400. Выражение ТекущаяДата() + 3 прибавит всего 3 секунды.Теперь рассмотрим, как правильно решить данную задачу несколькими способами.
Поскольку интервал проверки небольшой (всего несколько дней), самым надежным и устойчивым к переходам через месяц и год способом является формирование таблицы конкретных дней и месяцев в коде 1С с последующей передачей ее в запрос как параметра.
Посмотрим на пример полной реализации:
// 1. Формируем таблицу значений с целевыми датами
ТаблицаДат = Новый ТаблицаЗначений;
ТаблицаДат.Колонки.Добавить("День", Новый ОписаниеТипов("Число"));
ТаблицаДат.Колонки.Добавить("Месяц", Новый ОписаниеТипов("Число"));
ТекДата = НачалоДня(ТекущаяДата());
ИнтервалДней = 3;
Для Счетчик = 0 По ИнтервалДней Цикл
АнализируемаяДата = ТекДата + Счетчик * 86400;
НоваяСтрока = ТаблицаДат.Добавить();
НоваяСтрока.День = День(АнализируемаяДата);
НоваяСтрока.Месяц = Месяц(АнализируемаяДата);
// Обработка 29 февраля в невисокосный год
Если НоваяСтрока.День = 28 И НоваяСтрока.Месяц = 2 И День(КонецМесяца(АнализируемаяДата)) = 28 Тогда
СтрокаВисокосная = ТаблицаДат.Добавить();
СтрокаВисокосная.День = 29;
СтрокаВисокосная.Месяц = 2;
КонецЕсли;
КонецЦикла;
// 2. Выполняем запрос с соединением по временной таблице
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Дни.День КАК День,
| Дни.Месяц КАК Месяц
|ПОМЕСТИТЬ ВТ_ЦелевыеДни
|ИЗ
| &ТаблицаДат КАК Дни
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| Сотрудники.Ссылка КАК Сотрудник,
| Сотрудники.Фамилия КАК Фамилия,
| Сотрудники.Имя КАК Имя,
| Сотрудники.Отчество КАК Отчество,
| Сотрудники.ДатаРождения КАК ДатаРождения
|ИЗ
| Справочник.Сотрудники КАК Сотрудники
| ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_ЦелевыеДни КАК Дни
| ПО (ДЕНЬ(Сотрудники.ДатаРождения) = Дни.День)
| И (МЕСЯЦ(Сотрудники.ДатаРождения) = Дни.Месяц)
|ГДЕ
| НЕ Сотрудники.ПометкаУдаления";
Запрос.УстановитьПараметр("ТаблицаДат", ТаблицаДат);
РезультатЗапроса = Запрос.Выполнить();
Если НЕ РезультатЗапроса.Пустой() Тогда
Выборка = РезультатЗапроса.Выбрать();
Пока Выборка.Следующий() Цикл
ТекстСообщения = СтрШаблон("Скоро день рождения: %1 %2 %3 (Дата рождения: %4)",
Выборка.Фамилия,
Выборка.Имя,
Выборка.Отчество,
Формат(Выборка.ДатаРождения, "ДФ=dd.MM.yyyy"));
Сообщить(ТекстСообщения);
КонецЦикла;
Иначе
Сообщить("В ближайшие 3 дня именинников нет.");
КонецЕсли;
Преимущества данного подхода:
Если требуется решить задачу исключительно средствами языка запросов без подготовки внешних таблиц, можно использовать функции ДОБАВИТЬКДАТЕ и РАЗНОСТЬДАТ. Суть метода заключается в том, чтобы вычислить разницу в годах между датой рождения и текущим годом, а затем сместить дату рождения на это количество лет вперед.
Разберем пример такого запроса:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Сотрудники.Фамилия КАК Фамилия,
| Сотрудники.Имя КАК Имя,
| Сотрудники.Отчество КАК Отчество,
| Сотрудники.ДатаРождения КАК ДатаРождения
|ИЗ
| Справочник.Сотрудники КАК Сотрудники
|ГДЕ
| НЕ Сотрудники.ПометкаУдаления
| И (ДОБАВИТЬКДАТЕ(Сотрудники.ДатаРождения, ГОД, РАЗНОСТЬДАТ(Сотрудники.ДатаРождения, &ТекущаяДата, ГОД))
| МЕЖДУ &ДатаНачала И &ДатаКонец
| ИЛИ ДОБАВИТЬКДАТЕ(Сотрудники.ДатаРождения, ГОД, РАЗНОСТЬДАТ(Сотрудники.ДатаРождения, &ТекущаяДата, ГОД) + 1)
| МЕЖДУ &ДатаНачала И &ДатаКонец)";
ТекущийМомент = НачалоДня(ТекущаяДата());
Запрос.УстановитьПараметр("ТекущаяДата", ТекущийМомент);
Запрос.УстановитьПараметр("ДатаНачала", ТекущийМомент);
Запрос.УстановитьПараметр("ДатаКонец", КонецДня(ТекущийМомент + 3 * 86400));
РезультатЗапроса = Запрос.Выполнить();
Выборка = РезультатЗапроса.Выбрать();
Пока Выборка.Следующий() Цикл
Сообщить("Именинник: " + Выборка.Фамилия + " " + Выборка.Имя + " (" + Формат(Выборка.ДатаРождения, "ДФ=dd.MM.yyyy") + ")");
КонецЦикла;
Обратите внимание: второе условие с прибавлением + 1 года необходимо для корректной обработки дней рождения в начале января, когда проверка выполняется в последних числах декабря.
Еще один популярный алгоритмический прием — перевод даты в число вида MMDD (номер месяца умножается на 100 и к нему прибавляется день). Например, 5 мая превращается в 505, а 25 декабря — в 1225.
Проанализируем логику условий в запросе:
ДатаС = НачалоДня(ТекущаяДата());
ДатаПо = НачалоДня(ТекущаяДата() + 3 * 86400);
КодС = Месяц(ДатаС) * 100 + День(ДатаС);
КодПо = Месяц(ДатаПо) * 100 + День(ДатаПо);
Запрос = Новый Запрос;
Если КодС <= КодПо Тогда
// Интервал в рамках одного календарного года
Запрос.Текст =
"ВЫБРАТЬ
| Сотрудники.Фамилия,
| Сотрудники.Имя,
| Сотрудники.ДатаРождения
|ИЗ
| Справочник.Сотрудники КАК Сотрудники
|ГДЕ
| НЕ Сотрудники.ПометкаУдаления
| И (МЕСЯЦ(Сотрудники.ДатаРождения) * 100 + ДЕНЬ(Сотрудники.ДатаРождения)) МЕЖДУ &КодС И &КодПо";
Иначе
// Интервал переходит через Новый год (например, с 30 декабря по 2 января)
Запрос.Текст =
"ВЫБРАТЬ
| Сотрудники.Фамилия,
| Сотрудники.Имя,
| Сотрудники.ДатаРождения
|ИЗ
| Справочник.Сотрудники КАК Сотрудники
|ГДЕ
| НЕ Сотрудники.ПометкаУдаления
| И ((МЕСЯЦ(Сотрудники.ДатаРождения) * 100 + ДЕНЬ(Сотрудники.ДатаРождения)) >= &КодС
| ИЛИ (МЕСЯЦ(Сотрудники.ДатаРождения) * 100 + ДЕНЬ(Сотрудники.ДатаРождения)) <= &КодПо)";
КонецЕсли;
Запрос.УстановитьПараметр("КодС", КодС);
Запрос.УстановитьПараметр("КодПо", КодПо);
РезультатЗапроса = Запрос.Выполнить();
Выборка = РезультатЗапроса.Выбрать();
Пока Выборка.Следующий() Цикл
Сообщить("Скоро день рождения: " + Выборка.Фамилия + " " + Выборка.Имя);
КонецЦикла;
ПометкаУдаления.Формат(Выборка.ДатаРождения, "ДФ=dd.MM.yyyy") или СтрШаблон() для объединения ФИО со строками.ПриНачалеРаботыСистемы() или на начальной странице (рабочем столе) пользователя.