Как проверить пересечение двух временных периодов в 1С и найти их общий интервал?

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

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

Основной принцип проверки пересечения периодов

Прежде чем углубляться в детали реализации, давайте поймем фундаментальный принцип, который лежит в основе определения пересечения любых двух отрезков на числовой оси, а значит и временных интервалов. Этот принцип является универсальным и применимым в большинстве языков программирования и систем.

Пусть у нас есть два периода: Период 1, определяемый как интервал от НачПериода1 до КонПериода1, и Период 2, определяемый как интервал от НачПериода2 до КонПериода2.

Два периода пересекаются, если выполняются следующие два условия одновременно:

  1. Начало первого периода меньше или равно окончанию второго периода. Это гарантирует, что первый период не полностью заканчивается до начала второго.
  2. Окончание первого периода больше или равно началу второго периода. Это гарантирует, что первый период не полностью начинается после окончания второго.

Формально это можно записать в виде логического выражения:

(НачПериода1 <= КонПериода2) И (КонПериода1 >= НачПериода2)

Если это условие истинно, то периоды пересекаются. В противном случае пересечения нет.

Для нахождения самого интервала пересечения, если оно существует, используем следующий подход:

  1. Начало пересечения: Мы берем максимальную из двух начальных дат. Это гарантирует, что начало интервала пересечения будет позже или равно началам обоих исходных периодов. НачалоПересечения = Макс(НачПериода1, НачПериода2);
  2. Конец пересечения: Мы берем минимальную из двух конечных дат. Это гарантирует, что конец интервала пересечения будет раньше или равен окончаниям обоих исходных периодов. КонецПересечения = Мин(КонПериода1, КонПериода2);

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

Обработка "бесконечных" периодов в 1С

В нашей задаче есть особое условие: если начальная дата периода не заполнена, это означает "-бесконечность", а если конечная дата не заполнена – "+бесконечность". В 1С мы можем эмулировать эти "бесконечные" значения, используя крайние даты, доступные в платформе. Такой подход позволяет нам применять ту же логику проверки, не усложняя ее специальными условиями для пустых дат:

  1. Для "-бесконечности" (когда входная начальная дата не заполнена) мы будем использовать минимально возможную дату в 1С. Обычно это Дата('00010101') или НачалоДня(Дата(1,1,1)).
  2. Для "+бесконечности" (когда входная конечная дата не заполнена) мы будем использовать максимально возможную дату в 1С. Обычно это Дата('99991231') или КонецДня(Дата(9999,12,31)).

При проверке заполненности дат удобно использовать встроенную функцию ЗначениеЗаполнено().

Чтобы наш основной код был чище и понятнее, давайте подготовим вспомогательные функции для получения "бесконечных" дат:


&НаКлиенте
// Функция для получения даты начала периода, учитывая 'бесконечность'
// Если ДатаВхода не заполнена, возвращает минимальную дату (аналог -бесконечности).
// Иначе возвращает исходную дату.
Функция ПолучитьРеальнуюДатуНачала(ДатаВхода)
    Если Не ЗначениеЗаполнено(ДатаВхода) Тогда
        Возврат Дата('00010101'); // Минимальная дата в 1С
    Иначе
        Возврат ДатаВхода;
    КонецЕсли;
КонецФункции

&НаКлиенте
// Функция для получения даты окончания периода, учитывая 'бесконечность'
// Если ДатаВхода не заполнена, возвращает максимальную дату (аналог +бесконечности).
// Иначе возвращает исходную дату.
Функция ПолучитьРеальнуюДатуОкончания(ДатаВхода)
    Если Не ЗначениеЗаполнено(ДатаВхода) Тогда
        Возврат Дата('99991231'); // Максимальная дата в 1С
    Иначе
        Возврат ДатаВхода;
    КонецЕсли;
КонецФункции

Реализация проверки пересечения на встроенном языке 1С

Теперь, используя основной принцип и функции для обработки "бесконечностей", мы можем составить обработку. Входные параметры будут соответствовать условию задачи: НачПериода1, КонПериода1, НачПериода2, КонПериода2. На выходе мы получим либо сообщение "Пересечения нет", либо интервал НачПериода, КонПериода.

Мы сначала преобразуем входные даты с учетом "бесконечностей", а затем применим логику поиска пересечения:


// Предполагаем, что эти функции (ПолучитьРеальнуюДатуНачала, ПолучитьРеальнуюДатуОкончания)
// уже определены в этом же модуле или доступны из общего модуля.

// Входные параметры функции:
// НачПериода1 - Начало первого периода (может быть не заполнено)
// КонПериода1 - Окончание первого периода (может быть не заполнено)
// НачПериода2 - Начало второго периода (может быть не заполнено)
// КонПериода2 - Окончание второго периода (может быть не заполнено)
// СФормированнымИнтервалом - Необязательный параметр. Если Истина, функция вернет Структуру
//                          с полями "Начало" и "Конец" (или Неопределено, если нет пересечения).
//                          Если Ложь (по умолчанию), функция просто выведет сообщение и вернет Булево.

Функция ПроверитьПересечениеПериодов(НачПериода1, КонПериода1, НачПериода2, КонПериода2, Знач СФормированнымИнтервалом = Ложь)

    // 1. Преобразуем входные даты с учетом "бесконечности"
    ПереработаннаяНачПериода1 = ПолучитьРеальнуюДатуНачала(НачПериода1);
    ПереработаннаяКонПериода1 = ПолучитьРеальнуюДатуОкончания(КонПериода1);
    ПереработаннаяНачПериода2 = ПолучитьРеальнуюДатуНачала(НачПериода2);
    ПереработаннаяКонПериода2 = ПолучитьРеальнуюДатуОкончания(КонПериода2);

    // 2. Применяем основной принцип проверки пересечения
    Если (ПереработаннаяНачПериода1 <= ПереработаннаяКонПериода2) И (ПереработаннаяКонПериода1 >= ПереработаннаяНачПериода2) Тогда
        // 3. Если пересечение есть, находим его границы
        НачалоПересечения = Макс(ПереработаннаяНачПериода1, ПереработаннаяНачПериода2);
        КонецПересечения = Мин(ПереработаннаяКонПериода1, ПереработаннаяКонПериода2);

        // 4. Дополнительная проверка: убеждаемся, что начало пересечения не после его конца
        Если НачалоПересечения <= КонецПересечения Тогда
            Если СФормированнымИнтервалом Тогда
                // Возвращаем структуру с найденным интервалом
                Возврат Новый Структура("Начало, Конец", НачалоПересечения, КонецПересечения);
            Иначе
                // Выводим сообщение для пользователя
                Сообщить("Пересечение есть: с " + Формат(НачалоПересечения, "ДЛФ=Д") + " по " + Формат(КонецПересечения, "ДЛФ=Д"));
                Возврат Истина; // Или другое значение, если требуется
            КонецЕсли;
        Иначе
            // Эта ветка может сработать, если периоды 'касаются' друг друга,
            // но не перекрываются, или в случае некорректных входных данных
            Если СФормированнымИнтервалом Тогда
                Возврат Новый Структура("Начало, Конец", Неопределено, Неопределено); // Или пустая структура
            Иначе
                Сообщить("Пересечения нет.");
                Возврат Ложь;
            КонецЕсли;
        КонецЕсли;
    Иначе
        // 5. Пересечения нет (основное условие не выполнилось)
        Если СФормированнымИнтервалом Тогда
            Возврат Новый Структура("Начало, Конец", Неопределено, Неопределено); // Или пустая структура
        Иначе
            Сообщить("Пересечения нет.");
            Возврат Ложь;
        КонецЕсли;
    КонецЕсли;
КонецФункции

// Пример использования функции на форме обработки:
// Процедура КнопкаПроверитьНажатие(Команда)
//     Начало1 = ПолеНачалоПериода1; // Например, реквизит формы
//     Конец1  = ПолеКонецПериода1;  // Например, реквизит формы
//     Начало2 = ПолеНачалоПериода2; // Например, реквизит формы
//     Конец2  = ПолеКонецПериода2;  // Например, реквизит формы
//
//     // Вариант 1: Просто вывести сообщение
//     ПроверитьПересечениеПериодов(Начало1, Конец1, Начало2, Конец2);
//
//     // Вариант 2: Получить интервал пересечения для дальнейшей обработки
//     РезультатПересечения = ПроверитьПересечениеПериодов(Начало1, Конец1, Начало2, Конец2, Истина);
//     Если РезультатПересечения.Начало <> Неопределено Тогда
//         Сообщить("Найденный интервал: с " + Формат(РезультатПересечения.Начало, "ДЛФ=Д") + " по " + Формат(РезультатПересечения.Конец, "ДЛФ=Д"));
//     Иначе
//         Сообщить("Пересечения не найдено.");
//     КонецЕсли;
// КонецПроцедуры

Обратите внимание: Мы добавили параметр СФормированнымИнтервалом. Это позволяет использовать функцию более универсально: либо просто вывести сообщение для пользователя, либо вернуть структуру с датами пересечения, что более удобно для дальнейшей программной обработки.

Альтернативный подход: Проверка отсутствия пересечения

Как верно было подмечено в сообщениях форума, иногда проще сформулировать условие отсутствия пересечения. Два периода не пересекаются, если один период заканчивается раньше, чем начинается другой. Иными словами, один интервал находится полностью слева от другого, или полностью справа.

Формально это выглядит так:


(ПереработаннаяКонПериода1 < ПереработаннаяНачПериода2) ИЛИ (ПереработаннаяКонПериода2 < ПереработаннаяНачПериода1)

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

Пример реализации проверки пересечения с помощью запроса 1С

Если вам необходимо проверить пересечение заданного интервала с множеством периодов, хранящихся в базе данных (например, в регистрах, документах или справочниках), то удобнее и производительнее использовать язык запросов 1С. Запрос позволяет эффективно отфильтровать нужные данные на стороне СУБД, минимизируя передачу данных на клиент.

Рассмотрим пример запроса, который позволяет найти все записи, чей период пересекается с заданными параметрами &НачалоЗаданногоПериода и &КонецЗаданногоПериода.

Для корректной работы запроса крайне важно учесть все варианты пересечений: когда заданный период полностью внутри существующего, когда существующий полностью внутри заданного, и когда они перекрываются частично. К счастью, универсальное условие пересечения, которое мы рассмотрели выше, отлично работает и в запросах.

Оптимизированное и корректное условие для запроса будет выглядеть так:


ВЫБРАТЬ
    МояТаблица.Ссылка,
    МояТаблица.НачалоПериода КАК НачалоХранимогоПериода,
    МояТаблица.КонецПериода КАК КонецХранимогоПериода
ИЗ
    Справочник.МоиПериоды КАК МояТаблица // Пример таблицы с периодами
ГДЕ
    (МояТаблица.НачалоПериода <= &КонецЗаданногоПериода)
    И (МояТаблица.КонецПериода >= &НачалоЗаданногоПериода)

В этом примере Справочник.МоиПериоды – это любой объект базы данных (регистр сведений, документ, справочник и т.д.), который содержит поля НачалоПериода и КонецПериода. Параметры &НачалоЗаданногоПериода и &КонецЗаданногоПериода должны быть переданы в запрос с учетом обработки "бесконечностей", как мы это делали ранее, используя функции ПолучитьРеальнуюДатуНачала() и ПолучитьРеальнуюДатуОкончания() перед выполнением запроса.

Этот простой, но мощный запрос покрывает все случаи пересечения и является наиболее эффективным способом работы с большими объемами данных.

Использование стандартных функций Библиотеки Стандартных Подсистем (БСП)

В конфигурациях, разработанных на базе Библиотеки Стандартных Подсистем (БСП) – таких как 1С:ERP, 1С:Комплексная автоматизация, 1С:Управление торговлей и другие – часто уже существуют готовые функции для работы с интервалами и датами. Использование этих функций является наиболее предпочтительным вариантом, так как они протестированы, оптимизированы и поддерживаются разработчиками платформы.

В одном из сообщений форума упоминалась функция ОбщегоНазначенияБЗК.ПериодПересеченияДвухИнтервалов. В зависимости от версии БСП и конфигурации, подобные функции могут находиться в различных общих модулях, например:

Мы рекомендуем всегда проверять наличие таких готовых функций в вашей конкретной конфигурации БСП перед тем, как писать собственную реализацию. Использование готовых механизмов значительно сократит время разработки, повысит надежность решения и обеспечит единообразие кода с другими частями системы.

Объединение пересекающихся периодов (более сложная задача)

Иногда возникает задача не просто проверить пересечение, а объединить несколько пересекающихся или даже соприкасающихся периодов в один, сократив таким образом количество строк в таблице периодов. Например, из списка:


ДатаНачала   ДатаОкончания
05.06.2023   17.06.2023
15.06.2023   25.06.2023

Мы хотим получить:


ДатаНачала   ДатаОкончания
05.06.2023   25.06.2023

Это более сложная задача, которая требует итеративного подхода. Ключевым моментом для корректного и эффективного объединения является предварительная сортировка таблицы по дате начала. Без сортировки алгоритм будет значительно сложнее и менее надежен.

Рассмотрим алгоритм и пример кода, который решает эту задачу для таблицы значений:

  1. Подготовка таблицы: Убедимся, что у нас есть таблица значений с колонками для даты начала и даты окончания.
  2. Сортировка: Обязательно отсортируйте таблицу по колонке "ДатаНачала" в возрастающем порядке. Это критически важно, так как позволяет обрабатывать периоды последовательно.
  3. Итеративное объединение: Проходим по отсортированной таблице. Если текущий период пересекается или соприкасается со следующим, мы объединяем их, расширяя текущий период и удаляя следующий. Повторяем, пока не останется непересекающихся периодов.

Пример кода для объединения периодов:


// Предполагаем, что у нас есть таблица значений ТаблицаПериодов
// с колонками "ДатаНачала" и "ДатаОкончания".

// Примеры данных для демонстрации:
// ТаблицаПериодов = Новый ТаблицаЗначений;
// ТаблицаПериодов.Колонки.Добавить("ДатаНачала", Новый ОписаниеТипов("Дата"));
// ТаблицаПериодов.Колонки.Добавить("ДатаОкончания", Новый ОписаниеТипов("Дата"));
//
// ТаблицаПериодов.Добавить().ДатаНачала = '20230605'; ТаблицаПериодов.Последняя().ДатаОкончания = '20230617';
// ТаблицаПериодов.Добавить().ДатаНачала = '20230615'; ТаблицаПериодов.Последняя().ДатаОкончания = '20230625';
// ТаблицаПериодов.Добавить().ДатаНачала = '20230701'; ТаблицаПериодов.Последняя().ДатаОкончания = '20230710';
// ТаблицаПериодов.Добавить().ДатаНачала = '20230705'; ТаблицаПериодов.Последняя().ДатаОкончания = '20230715';
// ТаблицаПериодов.Добавить().ДатаНачала = '20230801'; ТаблицаПериодов.Последняя().ДатаОкончания = '20230805';
// ТаблицаПериодов.Добавить().ДатаНачала = '20230626'; ТаблицаПериодов.Последняя().ДатаОкончания = '20230630'; // Соприкасается с первым объединенным
// ТаблицаПериодов.Добавить().ДатаНачала = '20230901'; ТаблицаПериодов.Последняя().ДатаОкончания = '20230905';
// ТаблицаПериодов.Добавить().ДатаНачала = '20230906'; ТаблицаПериодов.Последняя().ДатаОкончания = '20230910'; // Соприкасается

// Шаг 1: Сортируем таблицу по дате начала (обязательно!)
// Если даты начала одинаковые, сортируем по дате окончания для детерминированности,
// хотя для логики объединения это не строго необходимо, но хорошая практика.
ТаблицаПериодов.Сортировать("ДатаНачала Возр, ДатаОкончания Возр");

ИндексТекущегоПериода = 0;
// Мы проходим до предпоследнего элемента, так как сравниваем текущий со следующим
Пока ИндексТекущегоПериода < ТаблицаПериодов.Количество - 1 Цикл
    // Получаем текущий и следующий периоды
    ТекущаяСтрока = ТаблицаПериодов[ИндексТекущегоПериода];
    СледующаяСтрока = ТаблицаПериодов[ИндексТекущегоПериода + 1];

    ТекущийПериодНачало    = ТекущаяСтрока.ДатаНачала;
    ТекущийПериодОкончание = ТекущаяСтрока.ДатаОкончания;

    СледующийПериодНачало    = СледующаяСтрока.ДатаНачала;
    СледующийПериодОкончание = СледующаяСтрока.ДатаОкончания;

    // Проверяем на пересечение или соприкосновение.
    // Периоды пересекаются, если НачалоТекущего <= КонецСледующего И КонецТекущего >= НачалоСледующего.
    // Для соприкосновения достаточно, если КонецТекущего + 1 день = НачалоСледующего (для дат без времени).
    // Мы можем объединить условие: если конец текущего периода не раньше, чем начало следующего (с учетом смежности).
    Если ТекущийПериодОкончание >= СледующийПериодНачало ИЛИ (ТекущийПериодОкончание + 1 = СледующийПериодНачало) Тогда
        // Если есть пересечение или соприкосновение, объединяем периоды
        // Начало объединенного периода - это минимум из начал
        ТекущаяСтрока.ДатаНачала = Мин(ТекущийПериодНачало, СледующийПериодНачало);
        // Конец объединенного периода - это максимум из концов
        ТекущаяСтрока.ДатаОкончания = Макс(ТекущийПериодОкончание, СледующийПериодОкончание);
        
        // Удаляем следующий период, так как он был объединен с текущим
        ТаблицаПериодов.Удалить(ИндексТекущегоПериода + 1);
        // Важно: не увеличиваем ИндексТекущегоПериода, так как текущий (измененный) период
        // может объединиться с новым 'следующим' (который теперь на месте удаленного)
    Иначе
        // Если нет пересечения или соприкосновения, переходим к следующему периоду
        ИндексТекущегоПериода = ИндексТекущегоПериода + 1;
    КонецЕсли;
КонецЦикла;

// В результате в ТаблицаПериодов останутся только объединенные, непересекающиеся периоды.
// Примеры:
// Исходные данные:
// 05.06.2023 17.06.2023
// 15.06.2023 25.06.2023
// 26.06.2023 28.06.2023
// 01.07.2023 10.07.2023
// 05.07.2023 15.07.2023
//
// После выполнения кода (результат в ТаблицаПериодов):
// 05.06.2023 28.06.2023 (объединились первые три)
// 01.07.2023 15.07.2023 (объединились следующие два)

Важное замечание: Представленный выше алгоритм с предварительной сортировкой и итеративным слиянием является более понятным, надежным и эффективным, чем сложная логика с отрицательными индексами, которая была предложена в сообщении 16. Он последовательно объединяет периоды, сокращая таблицу и гарантируя корректный результат.

Заключение

Мы рассмотрели различные методы проверки пересечения временных периодов в 1С. От простых логических условий и обработки "бесконечных" дат до использования запросов и стандартных функций БСП. Выбор конкретного подхода зависит от контекста задачи: требуется ли проверка двух периодов, множества периодов из базы данных, или же необходимо объединить набор пересекающихся интервалов. Всегда старайтесь использовать наиболее простой и производительный метод, а если есть возможность – уже готовые решения из стандартных библиотек, так как они, как правило, наиболее отлажены и оптимизированы для платформы 1С.

← На главную