Работа с датами в запросах 1С — одна из самых частых задач, с которой сталкиваются разработчики. Однако, когда дело доходит до периодических событий, таких как дни рождения, задача усложняется. Нам нужно игнорировать год рождения человека и сравнивать только месяц и день, при этом учитывая цикличность календаря (после декабря идет январь).
В этой статье мы подробно разберем, как решить задачу поиска "следующего" дня рождения в списке людей относительно текущей записи и вычислить количество дней между ними, используя исключительно язык запросов 1С.
Представим, что у нас есть регистр сведений или справочник с ФИО и датой рождения. Нам необходимо получить таблицу, где для каждого человека указан:
Сложность: Если у человека день рождения 31 декабря, а следующий по списку — 1 января, разница между ними всего 1 день, несмотря на то, что года рождения могут отличаться на десятилетия. Нам нужно "обнулить" года.
Один из самых надежных и понятных способов решения проблемы перехода через год (например, с ноября на февраль) — это создание временной таблицы, которая содержит данные за два условных года подряд.
Давайте рассмотрим алгоритм действий:
Разберем реализацию этого метода в коде запроса.
Сначала подготовим "нормализованные" даты. Мы используем функцию ДОБАВИТЬКДАТЕ и разность лет, чтобы привести год рождения к 2000-му.
ВЫБРАТЬ
ДниРождений.ФИО КАК ФИО,
// Приводим любую дату к 2000 году.
// 2000 год выбран, так как он високосный, и 29 февраля не вызовет ошибку.
ДОБАВИТЬКДАТЕ(ДниРождений.ДеньРождения, ГОД, 2000 - ГОД(ДниРождений.ДеньРождения)) КАК ПриведеннаяДата,
ДниРождений.ДеньРождения КАК РеальнаяДатаРождения
ПОМЕСТИТЬ ВТ_Исходные
ИЗ
РегистрСведений.ДниРождения КАК ДниРождений
;
Теперь создадим "Ленту времени". Мы берем нашу таблицу ВТ_Исходные и объединяем её с самой собой, но во второй части добавляем к дате ровно один год. Таким образом, мы получаем непрерывную последовательность событий.
ВЫБРАТЬ
Т.ФИО,
Т.ПриведеннаяДата,
Т.РеальнаяДатаРождения
ПОМЕСТИТЬ ВТ_ЛентаВремени
ИЗ
ВТ_Исходные КАК Т
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
Т.ФИО,
// Сдвигаем дату на год вперед, чтобы замкнуть цикл (Декабрь -> Январь)
ДОБАВИТЬКДАТЕ(Т.ПриведеннаяДата, ГОД, 1),
Т.РеальнаяДатаРождения
ИЗ
ВТ_Исходные КАК Т
;
Финальный этап — соединение. Для каждой записи из исходной таблицы мы ищем ближайшую следующую дату в ВТ_ЛентаВремени. Мы используем соединение по условию "Дата ленты > Исходная дата", группируем результаты и берем МИНИМУМ.
ВЫБРАТЬ
Исходные.ФИО,
Исходные.РеальнаяДатаРождения,
// Находим минимальную дату, которая больше текущей
МИНИМУМ(Лента.ПриведеннаяДата) КАК СледующаяПриведеннаяДата,
// Считаем разницу дней
РАЗНОСТЬДАТ(Исходные.ПриведеннаяДата, МИНИМУМ(Лента.ПриведеннаяДата), ДЕНЬ) КАК ДнейДоСледующего
ИЗ
ВТ_Исходные КАК Исходные
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_ЛентаВремени КАК Лента
// Условие: ищем только те даты, которые идут ПОЗЖЕ текущей
ПО Лента.ПриведеннаяДата > Исходные.ПриведеннаяДата
// Исключаем сравнение с самим собой (если нужно строго следующий человек)
И Лента.ФИО <> Исходные.ФИО
СГРУППИРОВАТЬ ПО
Исходные.ФИО,
Исходные.РеальнаяДатаРождения,
Исходные.ПриведеннаяДата
Этот метод хорош тем, что функция РАЗНОСТЬДАТ автоматически корректно посчитает переходы через год, так как мы физически добавили год к датам второй половины списка.
Если задача стоит так: "Вывести список, упорядоченный по датам дней рождения в году, и показать, сколько дней от текущего человека до следующего в этом списке", то отлично подойдет подход с нумерацией строк.
Давайте проанализируем логику:
В современных версиях платформы 8.3.х можно использовать функцию АВТОНОМЕРЗАПИСИ() во временных таблицах. Рассмотрим пример.
ВЫБРАТЬ
ДниРождений.ФИО,
// Приводим к 2000 году для сортировки
ДОБАВИТЬКДАТЕ(ДниРождений.ДеньРождения, ГОД, 2000 - ГОД(ДниРождений.ДеньРождения)) КАК СортировочнаяДата,
ДниРождений.ДеньРождения
ПОМЕСТИТЬ ВТ_Подготовка
ИЗ
РегистрСведений.ДниРождения КАК ДниРождений
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
АВТОНОМЕРЗАПИСИ() КАК НомерСтроки,
Т.ФИО,
Т.СортировочнаяДата,
Т.ДеньРождения
ПОМЕСТИТЬ ВТ_Нумерованная
ИЗ
(ВЫБРАТЬ
ВТ_Подготовка.ФИО,
ВТ_Подготовка.СортировочнаяДата,
ВТ_Подготовка.ДеньРождения
ИЗ
ВТ_Подготовка
УПОРЯДОЧИТЬ ПО
ВТ_Подготовка.СортировочнаяДата) КАК Т
;
Теперь соединяем таблицу саму с собой. Обратите внимание на нюанс: у последней записи в году нет "следующей" записи с номером N+1. Для нее следующей должна стать первая запись (круг замыкается).
ВЫБРАТЬ
Тек.ФИО,
Тек.ДеньРождения КАК ДатаРождения,
ЕСТЬNULL(След.ДеньРождения, Первая.ДеньРождения) КАК СледующийДеньРождения,
ВЫБОР
КОГДА След.СортировочнаяДата ЕСТЬ NULL
// Если это последняя запись, считаем до первой записи следующего года
ТОГДА РАЗНОСТЬДАТ(Тек.СортировочнаяДата, ДОБАВИТЬКДАТЕ(Первая.СортировочнаяДата, ГОД, 1), ДЕНЬ)
// Иначе просто разница между текущей и следующей
ИНАЧЕ РАЗНОСТЬДАТ(Тек.СортировочнаяДата, След.СортировочнаяДата, ДЕНЬ)
КОНЕЦ КАК КоличествоДней
ИЗ
ВТ_Нумерованная КАК Тек
// Соединение для получения следующего по списку
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Нумерованная КАК След
ПО (Sled.НомерСтроки = Тек.НомерСтроки + 1)
// Соединение с первой записью для замыкания круга
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Нумерованная КАК Первая
ПО (Первая.НомерСтроки = 1)
В обсуждениях часто предлагают использовать функцию ДЕНЬГОДА(). Давайте разберем, почему этот метод требует осторожности.
Функция ДЕНЬГОДА(Дата) возвращает порядковый номер дня от 1 до 366.
Если в вашем списке есть люди, родившиеся в високосные и невисокосные годы, сортировка по ДЕНЬГОДА может дать сбой в районе конца февраля — начала марта. Человек, родившийся 1 марта 1999 года (день 60), окажется в сортировке "раньше", чем человек, родившийся 29 февраля 2000 года (день 60), или алгоритм посчитает их равными, хотя хронологически 29 февраля идет перед 1 марта.
Поэтому рекомендация использовать приведение всех дат к одному (високосному) году, как показано в Способах 1 и 2, является наиболее профессиональной и безопасной.
При реализации подобных задач следует избегать полного декартова произведения (соединение таблицы с самой собой без условия или по условию ИСТИНА). Давайте посмотрим почему:
Поэтому в Способе 1 мы используем условие ПО Лента.ПриведеннаяДата > Исходные.ПриведеннаяДата. Это значительно сокращает количество перебираемых вариантов (так называемое "треугольное соединение"). В Способе 2 мы используем соединение по конкретному индексу (Номер = Номер + 1), что является самым быстрым вариантом с точки зрения производительности SQL.
Для решения задачи "Кольцевого" сравнения дат рождения: