При разработке отчетов или обработок (например, для вывода списка именинников на ближайшие дни) программисты часто используют, казалось бы, самое простое и интуитивное решение — функцию ДЕНЬГОДА() в запросе. Однако этот подход таит в себе классическую логическую ловушку, на которую регулярно натыкаются как начинающие, так и опытные разработчики. В этой статье мы подробно разберем, почему ломается стандартная логика сравнения дней года, и рассмотрим наиболее надежные способы решения этой задачи в платформе «1С:Предприятие 8».
Проанализируем ситуацию, описанную автором темы. В запросе используется следующее условие:
ГДЕ ДЕНЬГОДА(Сотрудники.ФизическоеЛицо.ДатаРождения) МЕЖДУ ДЕНЬГОДА(&НачДата) И ДЕНЬГОДА(&КонДата)
На первый взгляд все логично. Но при тестировании выясняется странная вещь: в результат запроса за 20 марта 2019 года (невисокосный год) неожиданно попадает сотрудник, родившийся 19 марта 1988 года (високосный год). При проверке оказывается, что функция ДЕНЬГОДА() для обеих дат возвращает абсолютно одинаковое значение — 79.
Давайте разберем математику этого процесса по шагам:
Из-за наличия 29 февраля во всех високосных годах происходит сдвиг на один день для абсолютно всех последующих дат года (начиная с 1 марта). Именно поэтому использовать функцию ДЕНЬГОДА() для сопоставления дней рождения в запросах категорически нельзя. Вчерашний именинник при таком алгоритме будет ошибочно поздравлен сегодня.
Рассмотрим несколько проверенных и корректных альтернативных вариантов решения этой задачи.
Один из самых лаконичных и быстрых способов избавиться от влияния года в запросе — перевести дату рождения и границы анализируемого периода в простое дробное число формата ММ.ДД.
Для этого мы берем порядковый номер месяца и прибавляем к нему день, деленный на 100. Например, для даты 19 марта мы получим: 3 + 19/100 = 3.19. Для 20 марта это будет 3.20. Год рождения при таком математическом преобразовании полностью игнорируется.
Посмотрим на пример реализации этого условия в запросе:
ВЫБРАТЬ
Сотрудники.Ссылка КАК Сотрудник,
Сотрудники.ФизическоеЛицо.ДатаРождения КАК ДатаРождения
ИЗ
Справочник.Сотрудники КАК Сотрудники
ГДЕ
(МЕСЯЦ(Сотрудники.ФизическоеЛицо.ДатаРождения) + ДЕНЬ(Сотрудники.ФизическоеЛицо.ДатаРождения) / 100)
МЕЖДУ (МЕСЯЦ(&НачДата) + ДЕНЬ(&НачДата) / 100) И (МЕСЯЦ(&КонДата) + ДЕНЬ(&КонДата) / 100)
Преимущества метода: Высокая скорость работы, простота написания кода, полное отсутствие зависимости от високосных лет.
Важное ограничение: Данный метод в его простом виде не сработает напрямую, если анализируемый период пересекает границу Нового года (например, когда мы ищем именинников в интервале с 28 декабря по 3 января). В этом случае придется усложнять запрос логическим оператором ИЛИ, разделяя проверку на две части (до конца года и с начала нового года).
Еще один популярный способ — искусственно «перенести» день рождения каждого сотрудника в текущий расчетный год. Для этого мы вычисляем разницу в годах между текущей датой и годом рождения сотрудника, а затем прибавляем полученное число лет к его дате рождения.
Проанализируем пример запроса, реализующего эту логику:
ВЫБРАТЬ
Сотрудники.Ссылка КАК Сотрудник,
Сотрудники.ФизическоеЛицо.ДатаРождения КАК ДатаРождения,
ДОБАВИТЬКДАТЕ(Сотрудники.ФизическоеЛицо.ДатаРождения, ГОД, ГОД(&ТекущаяДата) - ГОД(Сотрудники.ФизическоеЛицо.ДатаРождения)) КАК ДеньРожденияВТекущемГоду
ИЗ
Справочник.Сотрудники КАК Сотрудники
ГДЕ
ДОБАВИТЬКДАТЕ(Сотрудники.ФизическоеЛицо.ДатаРождения, ГОД, ГОД(&ТекущаяДата) - ГОД(Сотрудники.ФизическоеЛицо.ДатаРождения))
МЕЖДУ &НачДата И &КонДата
Обратите внимание на нюанс с 29 февраля: Если сотрудник родился 29 февраля, а текущий год является невисокосным, СУБД при сложении дат автоматически скорректирует результат на 28 февраля. Для большинства бизнес-сценариев это абсолютно приемлемо, так как люди, родившиеся в этот редкий день, обычно празднуют день рождения именно 28 февраля в невисокосные годы.
Если ваша задача требует стопроцентной точности в любые периоды времени, включая предновогодние дни, рекомендуется передавать в запрос уже подготовленные на уровне встроенного языка параметры. Мы можем заранее определить, переходит ли анализируемый период через границу года, и передать в запрос соответствующие флаги и даты.
Рассмотрим структуру условий в таком универсальном запросе:
ВЫБРАТЬ
Сотрудники.Ссылка КАК Сотрудник,
Сотрудники.ФизическоеЛицо.ДатаРождения КАК ДатаРождения
ИЗ
Справочник.Сотрудники КАК Сотрудники
ГДЕ
ВЫБОР
КОГДА &ПериодПадаетНаОдинГод ТЕГДА
(МЕСЯЦ(Сотрудники.ФизическоеЛицо.ДатаРождения) + ДЕНЬ(Сотрудники.ФизическоеЛицо.ДатаРождения) / 100)
МЕЖДУ &НачалоЧислом И &КонецЧислом
ИНАЧЕ
(МЕСЯЦ(Сотрудники.ФизическоеЛицо.ДатаРождения) + ДЕНЬ(Сотрудники.ФизическоеЛицо.ДатаРождения) / 100) >= &НачалоЧислом
ИЛИ (МЕСЯЦ(Сотрудники.ФизическоеЛицо.ДатаРождения) + ДЕНЬ(Сотрудники.ФизическоеЛицо.ДатаРождения) / 100) <= &КонецЧислом
КОНЕЦ
Здесь параметры &НачалоЧислом и &КонецЧислом рассчитываются в коде 1С перед запуском запроса по формуле Месяц + День/100, а булево значение &ПериодПадаетНаОдинГод проверяет, равен ли год начала периода году его окончания.
Если вы разрабатываете отчет в современной конфигурации на базе БСП (Библиотеки стандартных подсистем), например, в ЗУП 3.1 или ERP, вам не обязательно писать сложные расчетные условия вручную.
В Системе компоновки данных (СКД) для этих целей в метаданных часто уже присутствует предопределенное виртуальное поле «День рождения» (не путайте со стандартным реквизитом «Дата рождения»). Платформа внутри этого виртуального поля сама нормализует все даты, корректно обрабатывая високосные периоды и стыки лет. Достаточно добавить это поле в отбор вашего отчета и указать нужный интервал — система все сделает за вас в фоновом режиме.