Работа с временными интервалами — одна из самых трудоемких задач в разработке на платформе 1С. Часто возникает ситуация, когда в систему вводятся документы установки цен, периоды действия которых накладываются друг на друга. Например, одна цена установлена на весь год, а в середине года вводится промо-цена на две недели. Нам необходимо построить итоговую цепочку периодов, где каждый отрезок времени имеет только одно актуальное значение цены, а периоды не пересекаются.
В этой статье мы подробно разберем, как реализовать механизм «разрезания» и слияния интервалов с помощью языка запросов и программных методов.
Рассмотрим ситуацию: у нас есть исходная таблица с полями Начало, Окончание и Цена. Главная сложность заключается в том, что новый документ может «вклиниться» внутрь существующего периода, полностью его перекрыть или сдвинуть его границы. Чтобы решить задачу чисто запросом, нам нужно отойти от понятия «документ» и перейти к понятию «граничная точка времени».
Проанализируем основные этапы решения задачи:
Рассмотрим наиболее эффективный подход с использованием временных таблиц. Этот метод позволяет решить задачу на уровне СУБД, что критически важно для производительности при больших объемах данных.
Шаг 1. Собираем все точки перелома периодов
Сначала объединим все даты начала и окончания в один список. Это позволит нам получить все возможные моменты времени, когда цена могла измениться.
ВЫБРАТЬ Начало КАК ДатаГраницы ПОМЕСТИТЬ ВсеДаты ИЗ ИсхТаблица
ОБЪЕДИНИТЬ
ВЫБРАТЬ Окончание ИЗ ИсхТаблица;
Шаг 2. Формируем элементарные интервалы
Теперь проанализируем список дат и составим из них пары. Для каждой даты нам нужно найти ближайшую к ней дату «в будущем». Рассмотрим, как это сделать через соединение таблицы самой с собой.
ВЫБРАТЬ
Т1.ДатаГраницы КАК НачалоПериода,
МИНИМУМ(Т2.ДатаГраницы) КАК КонецПериода
ПОМЕСТИТЬ СеткаИнтервалов
ИЗ
ВсеДаты КАК Т1
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВсеДаты КАК Т2 ПО Т1.ДатаГраницы < Т2.ДатаГраницы
СГРУППИРОВАТЬ ПО
Т1.ДатаГраницы;
В результате мы получили таблицу СеткаИнтервалов, которая содержит «нарезанные» периоды, следующие друг за другом без пропусков и наложений.
После того как сетка готова, нам нужно «наложить» на нее наши исходные данные. Рассмотрим ситуацию, когда на один элементарный интервал претендуют несколько цен. В этом случае вводится понятие приоритета. Чаще всего приоритетной считается та запись, которая была внесена в систему позже (имеет больший МоментВремени или ДатаРегистрации).
Выясним, как соединить интервалы с данными:
ВЫБРАТЬ
Сетка.НачалоПериода,
Сетка.КонецПериода,
МАКСИМУМ(Исходники.Цена) КАК Цена // Здесь может быть сложная логика выбора по приоритету
ИЗ
СеткаИнтервалов КАК Сетка
ЛЕВОЕ СОЕДИНЕНИЕ ИсхТаблица КАК Исходники
ПО Сетка.НачалоПериода >= Исходники.Начало
И Сетка.КонецПериода <= Исходники.Окончание
СГРУППИРОВАТЬ ПО
Сетка.НачалоПериода,
Сетка.КонецПериода;
Важный нюанс: Если в результате левого соединения цена оказывается NULL, это означает, что в данный промежуток времени цена не была установлена. В зависимости от бизнес-логики, такие периоды можно либо исключить, либо оставить с нулевой ценой.
Если задача выполняется в рамках обработки или отчета и данных не миллионы, рассмотрим программный метод обработки ТаблицыЗначений. Он зачастую более гибок, если логика вытеснения слишком сложна для SQL.
Разберем алгоритм по шагам:
Начало.Такой метод позволяет легко реализовать логику «вытеснения», знакомую многим по работе с механизмом зарплаты в 1С. Кстати, в платформе 1С объект РегистрРасчета обладает встроенной виртуальной таблицей ФактическийПериодДействия, которая делает именно это — автоматически разрезает периоды с учетом приоритетов видов расчета. Если ваша задача масштабна, можно проанализировать возможность использования этого объекта, даже если она не связана напрямую с расчетом зарплаты.
Проанализируем частую ошибку: использование пустой даты (01.01.0001) в качестве признака открытого периода. В запросах 1С это может привести к неверному сравнению Дата < ДатаГраницы.
Рекомендуем приводить открытые периоды к «дате бесконечности» (например, 31.12.3999). Рассмотрим пример использования функции ВЫБОР в запросе для корректной обработки таких случаев:
ВЫБРАТЬ
Начало,
ВЫБОР
КОГДА Окончание = ДАТАВРЕМЯ(1, 1, 1) ТОГДА ДАТАВРЕМЯ(3999, 12, 31)
ИНАЧЕ Окончание
КОНЕЦ КАК КонецПериода
ИЗ
РегистрСведений.Цены;
Это гарантирует, что при соединении с таблицей ВсеДаты открытый период будет корректно участвовать в формировании сетки интервалов и не «выпадет» из логики МИНИМУМ(Т2.ДатаГраницы).
После «разрезания» интервалов часто возникают ситуации, когда два соседних периода имеют одну и ту же цену (например, если два исходных документа имели одинаковую цену и стояли встык). В финальном результате их желательно объединить обратно. Эта задача известна как «Gaps and Islands».
Посмотрим, как это реализовать:
Текущее.Начало = Предыдущее.Конец).МИНИМУМ даты начала первой строки группы и МАКСИМУМ даты окончания последней.Этот финальный шаг сделает ваш отчет или регистр чистым и удобным для анализа пользователями, убирая лишнюю «дробность» данных.