Как запросом найти ближайший следующий день рождения в списке и посчитать разницу дней?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

Работа с датами в запросах 1С — одна из самых частых задач, с которой сталкиваются разработчики. Однако, когда дело доходит до периодических событий, таких как дни рождения, задача усложняется. Нам нужно игнорировать год рождения человека и сравнивать только месяц и день, при этом учитывая цикличность календаря (после декабря идет январь).

В этой статье мы подробно разберем, как решить задачу поиска "следующего" дня рождения в списке людей относительно текущей записи и вычислить количество дней между ними, используя исключительно язык запросов 1С.

Постановка задачи

Представим, что у нас есть регистр сведений или справочник с ФИО и датой рождения. Нам необходимо получить таблицу, где для каждого человека указан:

  1. Его день рождения.
  2. День рождения следующего человека в хронологическом порядке (в рамках года).
  3. Количество дней до этого следующего события.

Сложность: Если у человека день рождения 31 декабря, а следующий по списку — 1 января, разница между ними всего 1 день, несмотря на то, что года рождения могут отличаться на десятилетия. Нам нужно "обнулить" года.

Способ 1: Метод "Двойной ленты" (Объединение множеств)

Один из самых надежных и понятных способов решения проблемы перехода через год (например, с ноября на февраль) — это создание временной таблицы, которая содержит данные за два условных года подряд.

Давайте рассмотрим алгоритм действий:

  1. Выбираем исходные данные.
  2. Приводим все даты рождения к одному фиксированному году (например, 2000 — так как он високосный, мы не потеряем 29 февраля).
  3. Делаем копию этой выборки, но сдвигаем год на +1 (2001).
  4. Объединяем эти две выборки.
  5. Для каждой даты из "первого" набора ищем минимальную дату из "объединенного" набора, которая строго больше исходной.

Разберем реализацию этого метода в коде запроса.

Сначала подготовим "нормализованные" даты. Мы используем функцию ДОБАВИТЬКДАТЕ и разность лет, чтобы привести год рождения к 2000-му.


ВЫБРАТЬ
    ДниРождений.ФИО КАК ФИО,
    // Приводим любую дату к 2000 году.
    // 2000 год выбран, так как он високосный, и 29 февраля не вызовет ошибку.
    ДОБАВИТЬКДАТЕ(ДниРождений.ДеньРождения, ГОД, 2000 - ГОД(ДниРождений.ДеньРождения)) КАК ПриведеннаяДата,
    ДниРождений.ДеньРождения КАК РеальнаяДатаРождения
ПОМЕСТИТЬ ВТ_Исходные
ИЗ
    РегистрСведений.ДниРождения КАК ДниРождений
;

Теперь создадим "Ленту времени". Мы берем нашу таблицу ВТ_Исходные и объединяем её с самой собой, но во второй части добавляем к дате ровно один год. Таким образом, мы получаем непрерывную последовательность событий.


ВЫБРАТЬ
    Т.ФИО,
    Т.ПриведеннаяДата,
    Т.РеальнаяДатаРождения
ПОМЕСТИТЬ ВТ_ЛентаВремени
ИЗ
    ВТ_Исходные КАК Т

ОБЪЕДИНИТЬ ВСЕ

ВЫБРАТЬ
    Т.ФИО,
    // Сдвигаем дату на год вперед, чтобы замкнуть цикл (Декабрь -> Январь)
    ДОБАВИТЬКДАТЕ(Т.ПриведеннаяДата, ГОД, 1),
    Т.РеальнаяДатаРождения
ИЗ
    ВТ_Исходные КАК Т
;

Финальный этап — соединение. Для каждой записи из исходной таблицы мы ищем ближайшую следующую дату в ВТ_ЛентаВремени. Мы используем соединение по условию "Дата ленты > Исходная дата", группируем результаты и берем МИНИМУМ.


ВЫБРАТЬ
    Исходные.ФИО,
    Исходные.РеальнаяДатаРождения,
    // Находим минимальную дату, которая больше текущей
    МИНИМУМ(Лента.ПриведеннаяДата) КАК СледующаяПриведеннаяДата,
    // Считаем разницу дней
    РАЗНОСТЬДАТ(Исходные.ПриведеннаяДата, МИНИМУМ(Лента.ПриведеннаяДата), ДЕНЬ) КАК ДнейДоСледующего
ИЗ
    ВТ_Исходные КАК Исходные
        ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_ЛентаВремени КАК Лента
        // Условие: ищем только те даты, которые идут ПОЗЖЕ текущей
        ПО Лента.ПриведеннаяДата > Исходные.ПриведеннаяДата
            // Исключаем сравнение с самим собой (если нужно строго следующий человек)
            И Лента.ФИО <> Исходные.ФИО 

СГРУППИРОВАТЬ ПО
    Исходные.ФИО,
    Исходные.РеальнаяДатаРождения,
    Исходные.ПриведеннаяДата

Этот метод хорош тем, что функция РАЗНОСТЬДАТ автоматически корректно посчитает переходы через год, так как мы физически добавили год к датам второй половины списка.

Способ 2: Нумерация строк (Для строгой последовательности)

Если задача стоит так: "Вывести список, упорядоченный по датам дней рождения в году, и показать, сколько дней от текущего человека до следующего в этом списке", то отлично подойдет подход с нумерацией строк.

Давайте проанализируем логику:

  1. Мы также приводим даты к одному году (чтобы правильно отсортировать).
  2. Сортируем список по возрастанию даты.
  3. Нумеруем записи от 1 до N.
  4. Соединяем таблицу саму с собой со смещением: запись №1 соединяем с записью №2, №2 с №3 и так далее.

В современных версиях платформы 8.3.х можно использовать функцию АВТОНОМЕРЗАПИСИ() во временных таблицах. Рассмотрим пример.


ВЫБРАТЬ
    ДниРождений.ФИО,
    // Приводим к 2000 году для сортировки
    ДОБАВИТЬКДАТЕ(ДниРождений.ДеньРождения, ГОД, 2000 - ГОД(ДниРождений.ДеньРождения)) КАК СортировочнаяДата,
    ДниРождений.ДеньРождения
ПОМЕСТИТЬ ВТ_Подготовка
ИЗ
    РегистрСведений.ДниРождения КАК ДниРождений
;

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
    АВТОНОМЕРЗАПИСИ() КАК НомерСтроки,
    Т.ФИО,
    Т.СортировочнаяДата,
    Т.ДеньРождения
ПОМЕСТИТЬ ВТ_Нумерованная
ИЗ
    (ВЫБРАТЬ
        ВТ_Подготовка.ФИО,
        ВТ_Подготовка.СортировочнаяДата,
        ВТ_Подготовка.ДеньРождения
    ИЗ
        ВТ_Подготовка
    УПОРЯДОЧИТЬ ПО
        ВТ_Подготовка.СортировочнаяДата) КАК Т
;

Теперь соединяем таблицу саму с собой. Обратите внимание на нюанс: у последней записи в году нет "следующей" записи с номером N+1. Для нее следующей должна стать первая запись (круг замыкается).


ВЫБРАТЬ
    Тек.ФИО,
    Тек.ДеньРождения КАК ДатаРождения,
    ЕСТЬNULL(След.ДеньРождения, Первая.ДеньРождения) КАК СледующийДеньРождения,
    
    ВЫБОР
        КОГДА След.СортировочнаяДата ЕСТЬ NULL 
        // Если это последняя запись, считаем до первой записи следующего года
        ТОГДА РАЗНОСТЬДАТ(Тек.СортировочнаяДата, ДОБАВИТЬКДАТЕ(Первая.СортировочнаяДата, ГОД, 1), ДЕНЬ)
        // Иначе просто разница между текущей и следующей
        ИНАЧЕ РАЗНОСТЬДАТ(Тек.СортировочнаяДата, След.СортировочнаяДата, ДЕНЬ)
    КОНЕЦ КАК КоличествоДней

ИЗ
    ВТ_Нумерованная КАК Тек
        // Соединение для получения следующего по списку
        ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Нумерованная КАК След
        ПО (Sled.НомерСтроки = Тек.НомерСтроки + 1)
        
        // Соединение с первой записью для замыкания круга
        ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Нумерованная КАК Первая
        ПО (Первая.НомерСтроки = 1)

Способ 3: Использование ДЕНЬГОДА (Почему это опасно?)

В обсуждениях часто предлагают использовать функцию ДЕНЬГОДА(). Давайте разберем, почему этот метод требует осторожности.

Функция ДЕНЬГОДА(Дата) возвращает порядковый номер дня от 1 до 366.

Если в вашем списке есть люди, родившиеся в високосные и невисокосные годы, сортировка по ДЕНЬГОДА может дать сбой в районе конца февраля — начала марта. Человек, родившийся 1 марта 1999 года (день 60), окажется в сортировке "раньше", чем человек, родившийся 29 февраля 2000 года (день 60), или алгоритм посчитает их равными, хотя хронологически 29 февраля идет перед 1 марта.

Поэтому рекомендация использовать приведение всех дат к одному (високосному) году, как показано в Способах 1 и 2, является наиболее профессиональной и безопасной.

Важные замечания по производительности

При реализации подобных задач следует избегать полного декартова произведения (соединение таблицы с самой собой без условия или по условию ИСТИНА). Давайте посмотрим почему:

Поэтому в Способе 1 мы используем условие ПО Лента.ПриведеннаяДата > Исходные.ПриведеннаяДата. Это значительно сокращает количество перебираемых вариантов (так называемое "треугольное соединение"). В Способе 2 мы используем соединение по конкретному индексу (Номер = Номер + 1), что является самым быстрым вариантом с точки зрения производительности SQL.

Резюме

Для решения задачи "Кольцевого" сравнения дат рождения:

  1. Нормализуйте даты: приводите год рождения всех сотрудников к одному високосному году (например, 2000).
  2. Используйте объединение или индексы: Для простых случаев подойдет объединение выборки с копией самой себя (+1 год). Для больших выборок используйте нумерацию строк во временной таблице.
  3. Избегайте ДЕНЬГОДА: Эта функция ненадежна при сравнении дат из разных лет.
← На главную