Как разложить период по дням в 1С и не потерять последний день?

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

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

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

Анализ ошибки в цикле обхода дат

Рассмотрим исходный программный код, который часто приводит к ошибке. Разработчик пытается обойти период с помощью цикла Пока, увеличивая текущую дату на одни сутки (86 400 секунд):


ТЗ = Новый ТаблицаЗначений;
ТЗ.Колонки.Добавить("День");
ТЗ.Колонки.Добавить("Месяц");

// Переберем все даты за период 
ТекДат = НачалоДня(Объект.Дата1);

Пока Не ТекДат = НачалоДня(Объект.Дата2) Цикл 
    
    // Добавим текущую дату в таблицу периодов
    НовСтр = ТЗ.Добавить();
    НовСтр.День = ТекДат;
    НовСтр.Месяц = Месяц(ТекДат);
    
    ТекДат = ТекДат + 86400; // Добавим 1 День
    
КонецЦикла;

Выясним причину, почему при значениях Объект.Дата1 = 01.12.2017 и Объект.Дата2 = 31.12.2017 в итоговой таблице значений отсутствует дата 31.12.2017.

Проанализируем условие выполнения цикла: Пока Не ТекДат = НачалоДня(Объект.Дата2). Когда переменная ТекДат последовательно увеличивается и становится равной 31.12.2017, платформа проверяет условие цикла. На этой итерации выражение ТекДат = НачалоДня(Объект.Дата2) возвращает значение Истина (так как обе даты равны 31.12.2017 00:00:00). Соответственно, конструкция Не ТекДат = НачалоДня(Объект.Дата2) превращается в Ложь.

Поскольку условие цикла больше не выполняется, цикл немедленно завершает свою работу. Код внутри цикла для даты 31.12.2017 просто не успевает выполниться, и эта строка не добавляется в таблицу значений ТЗ.

Правильный вариант цикла

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


ТЗ = Новый ТаблицаЗначений;
ТЗ.Колонки.Добавить("День");
ТЗ.Колонки.Добавить("Месяц");

ДатаНачала = НачалоДня(Объект.Дата1);
ДатаОкончания = НачалоДня(Объект.Дата2);

ТекДат = ДатаНачала;

Пока ТекДат <= ДатаОкончания Цикл 
    
    НовСтр = ТЗ.Добавить();
    НовСтр.День = ТекДат;
    НовСтр.Месяц = Месяц(ТекДат);
    
    // Безопасное добавление суток с защитой от сдвига времени
    ТекДат = НачалоДня(ТекДат + 86400); 
    
КонецЦикла;

Обратите внимание на важный нюанс: прибавление ровно 86 400 секунд (24 часа) в редких случаях может привести к логической ошибке (например, при переходе на летнее/зимнее время в некоторых операционных системах или часовых поясах, когда сутки могут длиться 23 или 25 часов). Чтобы исключить вероятность сдвига времени на 23:00 предыдущего дня или 01:00 следующего, мы оборачиваем операцию прибавления секунд в функцию НачалоДня().

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

Альтернатива 1. Использование запроса с конструкцией «ПЕРИОДАМИ»

Самый быстрый и производительный способ получить список всех дат периода — это использование механизма компоновки данных или обычного запроса с дополнением периодов. Этот расчет происходит непосредственно на уровне СУБД, что работает в разы быстрее любых циклов.

Посмотрим на пример реализации такого запроса:


Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |	НАЧАЛОПЕРИОДА(&ДатаНачала, ДЕНЬ) КАК Период
    |ИТОГИ ПО
    |	Период ПЕРИОДАМИ(ДЕНЬ, &ДатаНачала, &ДатаОкончания)";

Запрос.УстановитьПараметр("ДатаНачала", Объект.Дата1);
Запрос.УстановитьПараметр("ДатаОкончания", Объект.Дата2);

// Третий параметр "ВСЕ" является ключевым — он указывает системе 
// дополнить выборку абсолютно всеми датами внутри указанного периода
Выборка = Запрос.Выполнить().Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам, "Период", "ВСЕ");

Пока Выборка.Следующий() Цикл
    НовСтр = ТЗ.Добавить();
    НовСтр.День = Выборка.Период;
    НовСтр.Месяц = Месяц(Выборка.Период);
КонецЦикла;

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

Альтернатива 2. Генерация дат в запросе через декартово произведение

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

Рассмотрим структуру такого запроса:


Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ 0 КАК Цифра ПОМЕСТИТЬ ВТ_Цифры ОБЪЕДИНИТЬ ВЫБРАТЬ 1 ОБЪЕДИНИТЬ ВЫБРАТЬ 2 
    |ОБЪЕДИНИТЬ ВЫБРАТЬ 3 ОБЪЕДИНИТЬ ВЫБРАТЬ 4 ОБЪЕДИНИТЬ ВЫБРАТЬ 5 
    |ОБЪЕДИНИТЬ ВЫБРАТЬ 6 ОБЪЕДИНИТЬ ВЫБРАТЬ 7 ОБЪЕДИНИТЬ ВЫБРАТЬ 8 ОБЪЕДИНИТЬ ВЫБРАТЬ 9;
    |
    |ВЫБРАТЬ
    |	ДОБАВИТЬКДАТЕ(&ДатаНачала, ДЕНЬ, t100.Цифра * 100 + t10.Цифра * 10 + t1.Цифра) КАК Период
    |ИЗ
    |	ВТ_Цифры КАК t1
    |	ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_Цифры КАК t10 ПО ИСТИНА
    |	ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_Цифры КАК t100 ПО ИСТИНА
    |ГДЕ
    |	ДОБАВИТЬКДАТЕ(&ДатаНачала, ДЕНЬ, t100.Цифра * 100 + t10.Цифра * 10 + t1.Цифра) <= &ДатаОкончания";

Запрос.УстановитьПараметр("ДатаНачала", НачалоДня(Объект.Дата1));
Запрос.УстановитьПараметр("ДатаОкончания", НачалоДня(Объект.Дата2));

РезультатЗапроса = Запрос.Выполнить().Выгрузить();

Данный подход позволяет сгенерировать последовательность из 1000 дней (около 3 лет) полностью на стороне СУБД за доли миллисекунды, избегая применения циклов во встроенном языке 1С.

Альтернатива 3. Использование производственного календаря

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

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


Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |	Календарь.Дата КАК Период,
    |	Календарь.ВидДня КАК ВидДня
    |ИЗ
    |	РегистрСведений.ДанныеПроизводственногоКалендаря КАК Календарь
    |ГДЕ
    |	Календарь.Дата МЕЖДУ &ДатаНачала И &ДатаОкончания
    |	И Календарь.ПроизводственныйКалендарь = &Календарь";

Запрос.УстановитьПараметр("ДатаНачала", Объект.Дата1);
Запрос.УстановитьПараметр("ДатаОкончания", Объект.Дата2);
Запрос.УстановитьПараметр("Календарь", Справочники.ПроизводственныеКалендари.НайтиПоНаименованию("РФ"));

Результат = Запрос.Выполнить().Выгрузить();

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

Альтернатива 4. Функции БСП (Библиотеки Стандартных Подсистем)

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

ЗарплатаКадрыОбщиеНаборыДанных.СоздатьВТПериоды()

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

← На главную