Как в 1С разбить период на подпериоды по произвольному дню месяца и концу месяца?

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

В процессе разработки учетных и финансовых механизмов в системе 1С предприятие часто возникает необходимость сегментации временных интервалов. Типичный пример — разделение выбранного пользователем периода на подпериоды, границы которых определяются специфическими датами: фиксированным «днем гашения долга» (например, 30-е число каждого месяца) и последним календарным днем месяца.

Сложность данной задачи заключается в обработке «коротких» месяцев (например, февраля или месяцев, в которых всего 30 дней), а также в необходимости написать производительный, изящный и легко читаемый код без избыточных циклов с посуточным обходом дат. В рамках данной статьи мы детально разберем несколько эффективных подходов к решению этой задачи: от лаконичных алгоритмов на встроенном языке до универсальных запросов.

Постановка задачи и анализ граничных условий

Предположим, пользователь выбирает период с 01.01.2016 по 25.04.2016. В качестве дня гашения долга установлено 30-е число каждого месяца. Нам требуется получить следующую структуру подпериодов:

  1. 01.01.2016 – 30.01.2016 (первый интервал до дня гашения включительно);
  2. 31.01.2016 – 31.01.2016 (однодневный интервал, соответствующий концу месяца);
  3. 01.02.2016 – 29.02.2016 (поскольку в феврале 2016 года всего 29 дней, а 30-го числа не существует, система должна автоматически ограничить подпериод концом месяца);
  4. 01.03.2016 – 30.03.2016 (интервал до дня гашения);
  5. 31.03.2016 – 31.03.2016 (интервал конца месяца);
  6. 01.04.2016 – 25.04.2016 (последний интервал, ограниченный датой окончания общего периода).

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

Решение 1: Быстрый и лаконичный алгоритм на встроенном языке

Рассмотрим наиболее производительный программный метод решения задачи на встроенном языке 1С. Метод основан на итеративном сдвиге по месяцам и точечном расчете контрольных дат без использования посуточного перебора в цикле Пока.

Для корректного вычисления смещений мы будем использовать умножение количества дней на число секунд в сутках (86400 секунд). Это стандартный и самый быстрый способ манипуляции датами в платформе 1С.


Функция РазбитьПериодНаПодпериоды(Знач ДатаНачала, Знач ДатаОкончания, Знач ДеньГашения) Экспорт
    
    Подпериоды = Новый Массив;
    ТекущаяДата = НачалоДня(ДатаНачала);
    КонечнаяДата = КонецДня(ДатаОкончания);
    
    Пока ТекущаяДата <= КонечнаяДата Цикл
        
        // Определяем естественную границу текущего месяца или конец всего периода
        КонецМесячногоИнтервала = Мин(КонецМесяца(ТекущаяДата), КонечнаяДата);
        
        // Корректируем день гашения для коротких месяцев (например, февраля)
        МаксимальныйДеньМесяца = День(КонецМесяца(ТекущаяДата));
        РабочийДеньГашения = Мин(ДеньГашения, МаксимальныйДеньМесяца);
        
        // Формируем точную дату гашения для текущего месяца
        ДатаГашенияМесяца = НачалоМесяца(ТекущаяДата) + (РабочийДеньГашения - 1) * 86400;
        
        Если ТекущаяДата < ДатаГашенияМесяца Тогда
            
            // Формируем первый подпериод: от начала до дня гашения
            ГраницаПервогоПериода = Мин(КонецДня(ДатаГашенияМесяца), КонечнаяДата);
            Подпериоды.Добавить(Новый Структура("ДатаНачала, ДатаОкончания", ТекущаяДата, ГраницаПервогоПериода));
            
            // Если день гашения не совпадает с концом месяца, формируем второй подпериод
            Если ГраницаПервогоПериода < КонецМесячногоИнтервала Тогда
                ДатаНачалаВторогоПериода = НачалоДня(ГраницаПервогоПериода + 86400);
                Подпериоды.Добавить(Новый Структура("ДатаНачала, ДатаОкончания", ДатаНачалаВторогоПериода, КонецМесячногоИнтервала));
            КонецЕсли;
            
        Иначе
            
            // Если мы уже зашли за день гашения в текущем месяце
            Подпериоды.Добавить(Новый Структура("ДатаНачала, ДатаОкончания", ТекущаяДата, КонецМесячногоИнтервала));
            
        КонецЕсли;
        
        // Переходим на первый день следующего месяца
        ТекущаяДата = НачалоМесяца(КонецМесяца(ТекущаяДата) + 86400);
        
    КонецЦикла;
    
    Возврат Подпериоды;
    
КонецФункции

Проанализируем этот код подробнее. На каждой итерации цикла мы берем текущую дату и определяем максимальное число дней в этом месяце с помощью функции День(КонецМесяца(ТекущаяДата)). С помощью встроенной математической функции Мин() мы гарантируем, что если день гашения (например, 30) больше, чем количество дней в месяце (например, 29 в феврале), то в качестве дня гашения будет взято последнее число месяца. Это избавляет от необходимости писать сложные ветвления Если...Тогда...Иначе.

Решение 2: Формирование периодов запросом без обращения к базе данных

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

Мы можем применить элегантный прием генерации временной таблицы чисел «на лету» с помощью декартова произведения, а затем рассчитать даты гашения математически.


ВЫБРАТЬ 0 КАК Цифра ПОМЕСТИТЬ ВТ_Цифры ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 1 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 2 
ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 3 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 4 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 5 
ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 6 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 7 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 8 
ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 9
;

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
    (Т1.Цифра + Т2.Цифра * 10) КАК НомерМесяца
ПОМЕСТИТЬ ВТ_Последовательность
ИЗ
    ВТ_Цифры КАК Т1
    КРОСС СОЕДИНЕНИЕ ВТ_Цифры КАК Т2
;

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
    ДОБАВИТЬКДАТЕ(&НачалоПериода, МЕСЯЦ, П.НомерМесяца) КАК МесяцПериода
ПОМЕСТИТЬ ВТ_Месяцы
ИЗ
    ВТ_Последовательность КАК П
ГДЕ
    ДОБАВИТЬКДАТЕ(&НачалоПериода, МЕСЯЦ, П.НомерМесяца) <= &КонецПериода
;

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
    ВЫБОР 
        КОГДА ДЕНЬ(КОНЕЦПЕРИОДА(М.МесяцПериода, МЕСЯЦ)) < &ДеньГашения 
        ТОГДА КОНЕЦПЕРИОДА(М.МесяцПериода, МЕСЯЦ) 
        ИНАЧЕ ДОБАВИТЬКДАТЕ(НАЧАЛОПЕРИОДА(М.МесяцПериода, МЕСЯЦ), ДЕНЬ, &ДеньГашения - 1) 
    КОНЕЦ КАК ДатаГашения,
    КОНЕЦПЕРИОДА(М.МесяцПериода, МЕСЯЦ) КАК КонецМесяца,
    НАЧАЛОПЕРИОДА(М.МесяцПериода, МЕСЯЦ) КАК НачалоМесяца
ПОМЕСТИТЬ ВТ_Границы
ИЗ
    ВТ_Месяцы КАК М
;

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
    ВЫБОР КОГДА Г.НачалоМесяца < &НачалоПериода ТОГДА &НачалоПериода ИНАЧЕ Г.НачалоМесяца КОНЕЦ КАК ДатаНачала,
    ВЫБОР КОГДА Г.ДатаГашения > &КонецПериода ТОГДА &КонецПериода ИНАЧЕ Г.ДатаГашения КОНЕЦ КАК ДатаОкончания
ИЗ
    ВТ_Границы КАК Г

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

ВЫБРАТЬ
    ДОБАВИТЬКДАТЕ(Г.ДатаГашения, ДЕНЬ, 1),
    ВЫБОР КОГДА Г.КонецМесяца > &КонецПериода ТОГДА &КонецПериода ИНАЧЕ Г.КонецМесяца КОНЕЦ
ИЗ
    ВТ_Границы КАК Г
ГДЕ
    Г.ДатаГашения < Г.КонецМесяца

Разберем, как устроен этот запрос. Сначала мы генерируем последовательность чисел от 0 до 99 во временной таблице ВТ_Последовательность. Это дает нам возможность работать со смещениями до 100 месяцев (более 8 лет) вперед от начальной даты. Если ваш рабочий период может быть больше, достаточно добавить еще одно кросс-соединение (умножение на третью таблицу цифр даст диапазон до 1000 месяцев).

Далее мы рассчитываем границы подпериодов. Важнейшая оптимизация здесь кроется в вычислении даты гашения:

ВЫБОР КОГДА ДЕНЬ(КОНЕЦПЕРИОДА(М.МесяцПериода, МЕСЯЦ)) < &ДеньГашения ТОГДА КОНЕЦПЕРИОДА(М.МесяцПериода, МЕСЯЦ) ИНАЧЕ ДОБАВИТЬКДАТЕ(НАЧАЛОПЕРИОДА(М.МесяцПериода, МЕСЯЦ), ДЕНЬ, &ДеньГашения - 1) КОНЕЦ

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

Решение 3: Оптимальное проектирование в отчетах СКД

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

Для реализации этой возможности выполните следующие шаги:

  1. В схеме компоновки данных добавьте набор данных - запрос, где выбираются детальные записи с указанием поля даты.
  2. На вкладке «Настройки» в структуре отчета добавьте группировку по полю даты.
  3. В свойствах группировки установите тип дополнения в значение «Месяц», указав в качестве начальной и конечной дат параметры &НачалоПериода и &КонецПериода.
  4. Используйте механизм Вычисляемых полей СКД для динамического вывода подпериодов. Для этого напишите выражение, аналогичное нашему первому программному решению, используя функции встроенного языка СКД: Выбор Когда... Тогда... Иначе... Конец.

Такой подход переносит всю нагрузку по генерации отсутствующих временных интервалов на саму платформу 1С, гарантируя максимальное быстродействие отчета при работе с большими объемами данных.

Сравнение подходов и рекомендации по выбору решения

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

← На главную