В процессе разработки учетных и финансовых механизмов в системе 1С предприятие часто возникает необходимость сегментации временных интервалов. Типичный пример — разделение выбранного пользователем периода на подпериоды, границы которых определяются специфическими датами: фиксированным «днем гашения долга» (например, 30-е число каждого месяца) и последним календарным днем месяца.
Сложность данной задачи заключается в обработке «коротких» месяцев (например, февраля или месяцев, в которых всего 30 дней), а также в необходимости написать производительный, изящный и легко читаемый код без избыточных циклов с посуточным обходом дат. В рамках данной статьи мы детально разберем несколько эффективных подходов к решению этой задачи: от лаконичных алгоритмов на встроенном языке до универсальных запросов.
Предположим, пользователь выбирает период с 01.01.2016 по 25.04.2016. В качестве дня гашения долга установлено 30-е число каждого месяца. Нам требуется получить следующую структуру подпериодов:
Как мы видим, алгоритм должен динамически подстраиваться под количество дней в месяце и корректно обрабатывать ситуации, когда день гашения выходит за пределы календарной длины текущего месяца.
Рассмотрим наиболее производительный программный метод решения задачи на встроенном языке 1С. Метод основан на итеративном сдвиге по месяцам и точечном расчете контрольных дат без использования посуточного перебора в цикле Пока.
Для корректного вычисления смещений мы будем использовать умножение количества дней на число секунд в сутках (86400 секунд). Это стандартный и самый быстрый способ манипуляции датами в платформе 1С.
Функция РазбитьПериодНаПодпериоды(Знач ДатаНачала, Знач ДатаОкончания, Знач ДеньГашения) Экспорт
Подпериоды = Новый Массив;
ТекущаяДата = НачалоДня(ДатаНачала);
КонечнаяДата = КонецДня(ДатаОкончания);
Пока ТекущаяДата <= КонечнаяДата Цикл
// Определяем естественную границу текущего месяца или конец всего периода
КонецМесячногоИнтервала = Мин(КонецМесяца(ТекущаяДата), КонечнаяДата);
// Корректируем день гашения для коротких месяцев (например, февраля)
МаксимальныйДеньМесяца = День(КонецМесяца(ТекущаяДата));
РабочийДеньГашения = Мин(ДеньГашения, МаксимальныйДеньМесяца);
// Формируем точную дату гашения для текущего месяца
ДатаГашенияМесяца = НачалоМесяца(ТекущаяДата) + (РабочийДеньГашения - 1) * 86400;
Если ТекущаяДата < ДатаГашенияМесяца Тогда
// Формируем первый подпериод: от начала до дня гашения
ГраницаПервогоПериода = Мин(КонецДня(ДатаГашенияМесяца), КонечнаяДата);
Подпериоды.Добавить(Новый Структура("ДатаНачала, ДатаОкончания", ТекущаяДата, ГраницаПервогоПериода));
// Если день гашения не совпадает с концом месяца, формируем второй подпериод
Если ГраницаПервогоПериода < КонецМесячногоИнтервала Тогда
ДатаНачалаВторогоПериода = НачалоДня(ГраницаПервогоПериода + 86400);
Подпериоды.Добавить(Новый Структура("ДатаНачала, ДатаОкончания", ДатаНачалаВторогоПериода, КонецМесячногоИнтервала));
КонецЕсли;
Иначе
// Если мы уже зашли за день гашения в текущем месяце
Подпериоды.Добавить(Новый Структура("ДатаНачала, ДатаОкончания", ТекущаяДата, КонецМесячногоИнтервала));
КонецЕсли;
// Переходим на первый день следующего месяца
ТекущаяДата = НачалоМесяца(КонецМесяца(ТекущаяДата) + 86400);
КонецЦикла;
Возврат Подпериоды;
КонецФункции
Проанализируем этот код подробнее. На каждой итерации цикла мы берем текущую дату и определяем максимальное число дней в этом месяце с помощью функции День(КонецМесяца(ТекущаяДата)). С помощью встроенной математической функции Мин() мы гарантируем, что если день гашения (например, 30) больше, чем количество дней в месяце (например, 29 в феврале), то в качестве дня гашения будет взято последнее число месяца. Это избавляет от необходимости писать сложные ветвления Если...Тогда...Иначе.
В некоторых случаях разработчику требуется получить таблицу периодов непосредственно в запросе (например, для последующего соединения с физическими таблицами остатков и оборотов). Использовать для этого реальные таблицы, такие как РегистрСведений.РегламентированныйПроизводственныйКалендарь, не всегда надежно, поскольку календарь в базе данных может быть не заполнен на будущие периоды.
Мы можем применить элегантный прием генерации временной таблицы чисел «на лету» с помощью декартова произведения, а затем рассчитать даты гашения математически.
ВЫБРАТЬ 0 КАК Цифра ПОМЕСТИТЬ ВТ_Цифры ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 1 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 2
ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 3 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 4 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 5
ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 6 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 7 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 8
ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 9
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
(Т1.Цифра + Т2.Цифра * 10) КАК НомерМесяца
ПОМЕСТИТЬ ВТ_Последовательность
ИЗ
ВТ_Цифры КАК Т1
КРОСС СОЕДИНЕНИЕ ВТ_Цифры КАК Т2
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
ДОБАВИТЬКДАТЕ(&НачалоПериода, МЕСЯЦ, П.НомерМесяца) КАК МесяцПериода
ПОМЕСТИТЬ ВТ_Месяцы
ИЗ
ВТ_Последовательность КАК П
ГДЕ
ДОБАВИТЬКДАТЕ(&НачалоПериода, МЕСЯЦ, П.НомерМесяца) <= &КонецПериода
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
ВЫБОР
КОГДА ДЕНЬ(КОНЕЦПЕРИОДА(М.МесяцПериода, МЕСЯЦ)) < &ДеньГашения
ТОГДА КОНЕЦПЕРИОДА(М.МесяцПериода, МЕСЯЦ)
ИНАЧЕ ДОБАВИТЬКДАТЕ(НАЧАЛОПЕРИОДА(М.МесяцПериода, МЕСЯЦ), ДЕНЬ, &ДеньГашения - 1)
КОНЕЦ КАК ДатаГашения,
КОНЕЦПЕРИОДА(М.МесяцПериода, МЕСЯЦ) КАК КонецМесяца,
НАЧАЛОПЕРИОДА(М.МесяцПериода, МЕСЯЦ) КАК НачалоМесяца
ПОМЕСТИТЬ ВТ_Границы
ИЗ
ВТ_Месяцы КАК М
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
ВЫБОР КОГДА Г.НачалоМесяца < &НачалоПериода ТОГДА &НачалоПериода ИНАЧЕ Г.НачалоМесяца КОНЕЦ КАК ДатаНачала,
ВЫБОР КОГДА Г.ДатаГашения > &КонецПериода ТОГДА &КонецПериода ИНАЧЕ Г.ДатаГашения КОНЕЦ КАК ДатаОкончания
ИЗ
ВТ_Границы КАК Г
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
ДОБАВИТЬКДАТЕ(Г.ДатаГашения, ДЕНЬ, 1),
ВЫБОР КОГДА Г.КонецМесяца > &КонецПериода ТОГДА &КонецПериода ИНАЧЕ Г.КонецМесяца КОНЕЦ
ИЗ
ВТ_Границы КАК Г
ГДЕ
Г.ДатаГашения < Г.КонецМесяца
Разберем, как устроен этот запрос. Сначала мы генерируем последовательность чисел от 0 до 99 во временной таблице ВТ_Последовательность. Это дает нам возможность работать со смещениями до 100 месяцев (более 8 лет) вперед от начальной даты. Если ваш рабочий период может быть больше, достаточно добавить еще одно кросс-соединение (умножение на третью таблицу цифр даст диапазон до 1000 месяцев).
Далее мы рассчитываем границы подпериодов. Важнейшая оптимизация здесь кроется в вычислении даты гашения:
ВЫБОР КОГДА ДЕНЬ(КОНЕЦПЕРИОДА(М.МесяцПериода, МЕСЯЦ)) < &ДеньГашения ТОГДА КОНЕЦПЕРИОДА(М.МесяцПериода, МЕСЯЦ) ИНАЧЕ ДОБАВИТЬКДАТЕ(НАЧАЛОПЕРИОДА(М.МесяцПериода, МЕСЯЦ), ДЕНЬ, &ДеньГашения - 1) КОНЕЦ
Эта формула избавляет СУБД от необходимости выполнять ресурсоемкие условные проверки по каждому дню месяца. Если фактическое число дней в месяце меньше требуемого дня гашения, система возвращает последний день месяца. В остальных случаях происходит точное смещение от первого числа месяца.
Если разделение периода необходимо исключительно для построения печатных форм или аналитических отчетов в Системе компоновки данных (СКД), то выполнять программное разделение в коде или писать сложные виртуальные таблицы в запросе не требуется. В СКД встроен эффективный механизм автоматического дополнения периодов.
Для реализации этой возможности выполните следующие шаги:
«Месяц», указав в качестве начальной и конечной дат параметры &НачалоПериода и &КонецПериода.Выбор Когда... Тогда... Иначе... Конец.Такой подход переносит всю нагрузку по генерации отсутствующих временных интервалов на саму платформу 1С, гарантируя максимальное быстродействие отчета при работе с большими объемами данных.
Подводя итоги анализа, выделим ключевые правила использования каждого метода: