В практической разработке на платформе 1С часто возникает задача: для произвольной даты рождения определить ближайший к текущей дате день рождения человека. При этом ближайшим может оказаться как уже прошедший праздник в текущем году, так и будущий (в текущем или следующем календарном году).
Попытки решить задачу «в лоб» через ручной перебор дат, сложные вложенные условия или конструирование дат через Дата(Год, Месяц, День) часто приводят к громоздкому коду и скрытым ошибкам (например, при обработке людей, родившихся 29 февраля). Разберем по шагам надежные, компактные и производительные способы решения этой задачи как на встроенном языке 1С, так и в текстах запросов.
Прежде чем перейти к алгоритмам, проанализируем критические нюансы платформы 1С:
Дата(2023, 2, 29) в невисокосном году вызовет аварийное завершение программы с исключением «Преобразование значения к типу Дата не может быть выполнено». Использовать конструктор даты с тремя числами для 29 февраля без дополнительных проверок нельзя.ДобавитьМесяц(): Встроенная функция ДобавитьМесяц(ИсходнаяДата, Смещение) корректно нормализует границы месяцев. Если исходная дата равна 29.02.2020, то при смещении на 12 месяцев платформа корректно вернет 28.02.2021. Важно: смещение всегда нужно рассчитывать строго от исходной даты рождения, иначе при последовательном прибавлении лет високосная дата навсегда останется 28 февраля.НачалоДня().Рассмотрим наиболее элегантный математический подход. Вместо анализа трех вариантов (прошлый, текущий и будущий годы) достаточно определить количество полных лет человека и сравнить всего две даты: предыдущий день рождения и следующий за ним.
Разберем по шагам реализацию данного метода:
КоличествоЛет * 12 месяцев.(КоличествоЛет + 1) * 12 месяцев.Посмотрим на пример функции:
Функция ПолучитьБлижайшийДеньРождения(Знач ДатаРождения, Знач ТекущаяДата = Неопределено) Экспорт
Если Не ЗначениеЗаполнено(ДатаРождения) Тогда
Возврат '00010101';
КонецЕсли;
Если ТекущаяДата = Неопределено Тогда
ТекущаяДата = ТекущаяДата();
КонецЕсли;
ТекДата = НачалоДня(ТекущаяДата);
ДР = НачалоДня(ДатаРождения);
// 1. Вычисляем количество полных лет
КоличествоЛет = Год(ТекДата) - Год(ДР);
Если (Месяц(ДР) * 100 + День(ДР)) > (Месяц(ТекДата) * 100 + День(ТекДата)) Тогда
КоличествоЛет = КоличествоЛет - 1;
КонецЕсли;
// 2. Получаем две контрольные точки: прошедший ДР и будущий ДР
ПрошедшийДР = ДобавитьМесяц(ДР, КоличествоЛет * 12);
БудущийДР = ДобавитьМесяц(ДР, (КоличествоЛет + 1) * 12);
// 3. Вычисляем расстояния в секундах
ИнтервалПрошедший = ТекДата - ПрошедшийДР;
ИнтервалБудущий = БудущийДР - ТекДата;
// 4. Возвращаем дату с минимальным интервалом
Если ИнтервалПрошедший <= ИнтервалБудущий Тогда
Возврат ПрошедшийДР;
Иначе
Возврат БудущийДР;
КонецЕсли;
КонецФункции
Преимущества такого решения:
Если требуется лаконичный код без предварительного расчета полных лет, можно использовать смещение годов в цикле, пока модуль разницы уменьшается. Рассмотрим этот алгоритм:
Функция НайтиБлижайшийДР_Сдвигом(Знач ДатаРождения, Знач ТекДата) Экспорт
ТекДата = НачалоДня(ТекДата);
ДР = НачалоДня(ДатаРождения);
// Вычисляем смещение лет до текущего календарного года
Смещение = Год(ТекДата) - Год(ДР);
ДР_Тек = ДобавитьМесяц(ДР, Смещение * 12);
ДР_Пред = ДобавитьМесяц(ДР, (Смещение - 1) * 12);
ДР_След = ДобавитьМесяц(ДР, (Смещение + 1) * 12);
РазницаТек = Макс(ТекДата - ДР_Тек, ДР_Тек - ТекДата);
РазницаПред = Макс(ТекДата - ДР_Пред, ДР_Пред - ТекДата);
РазницаСлед = Макс(ТекДата - ДР_След, ДР_След - ТекДата);
МинРазница = Мин(РазницаТек, РазницаПред, РазницаСлед);
Если МинРазница = РазницаТек Тогда
Возврат ДР_Тек;
ИначеЕсли МинРазница = РазницаПред Тогда
Возврат ДР_Пред;
Иначе
Возврат ДР_След;
КонецЕсли;
КонецФункции
Когда необходимо обработать целый список физических лиц или сотрудников, оптимально выполнить все вычисления в одном запросе к базе данных. Выясним, как построить такой запрос с использованием функций ДОБАВИТЬКДАТЕ и РАЗНОСТЬДАТ.
Разберем логику запроса:
-1, 0, 1 год).
ВЫБРАТЬ
Сотрудники.Ссылка КАК Сотрудник,
Сотрудники.ДатаРождения КАК ДатаРождения,
ДОБАВИТЬКДАТЕ(
Сотрудники.ДатаРождения,
ГОД,
ГОД(&Период) - ГОД(Сотрудники.ДатаРождения) + Смещения.Коэффициент
) КАК ВариантДняРождения
ПОМЕСТИТЬ ВТ_ВсеВарианты
ИЗ
Справочник.ФизическиеЛица КАК Сотрудники
ПОЛНОЕ СОЕДИНЕНИЕ (
ВЫБРАТЬ -1 КАК Коэффициент
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ 0
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ 1
) КАК Смещения ПО ИСТИНА
ГДЕ
Сотрудники.ДатаРождения <> ДАТАВРЕМЯ(1, 1, 1)
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
Варианты.Сотрудник КАК Сотрудник,
Варианты.ВариантДняРождения КАК ДеньРождения,
ВЫБОР
КОГДА РАЗНОСТЬДАТ(Варианты.ВариантДняРождения, &Период, ДЕНЬ) < 0
ТОГДА -РАЗНОСТЬДАТ(Варианты.ВариантДняРождения, &Период, ДЕНЬ)
ИНАЧЕ РАЗНОСТЬДАТ(Варианты.ВариантДняРождения, &Период, ДЕНЬ)
КОНЕЦ КАК ДнейДоСобытия
ПОМЕСТИТЬ ВТ_Разницы
ИЗ
ВТ_ВсеВарианты КАК Варианты
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
ВТ_Разницы.Сотрудник КАК Сотрудник,
МИНИМУМ(ВТ_Разницы.ДнейДоСобытия) КАК МинДней
ПОМЕСТИТЬ ВТ_Минимумы
ИЗ
ВТ_Разницы КАК ВТ_Разницы
СГРУППИРОВАТЬ ПО
ВТ_Разницы.Сотрудник
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
ВТ_Разницы.Сотрудник КАК Сотрудник,
МАКСИМУМ(ВТ_Разницы.ДеньРождения) КАК БлижайшийДеньРождения
ИЗ
ВТ_Разницы КАК ВТ_Разницы
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_Минимумы КАК ВТ_Минимумы
ПО ВТ_Разницы.Сотрудник = ВТ_Минимумы.Сотрудник
И ВТ_Разницы.ДнейДоСобытия = ВТ_Минимумы.МинДней
СГРУППИРОВАТЬ ПО
ВТ_Разницы.Сотрудник
Такой запрос отрабатывает на стороне СУБД, корректно обрабатывает високосные смещения за счет внутренней механики функции ДОБАВИТЬКДАТЕ и позволяет быстро получить результат сразу по всему списку сотрудников или контрагентов.