При разработке отчетов и обработок в среде «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);
Запрос.УстановитьПараметр("ДатаОкончания", Объект.Дата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С.
Если ваша задача связана с бизнес-логикой (расчет рабочих дней, начисление зарплаты, расчет пеней и просрочек), то простого математического разложения периода по дням может быть недостаточно. Вам наверняка потребуется информация о праздниках, выходных и переносах дней.
В типовых конфигурациях 1С (таких как «1С:Бухгалтерия», «1С:ERP», «1С:ЗУП») для этого используются регистры сведений, хранящие производственные календари (например, регистр ДанныеПроизводственногоКалендаря или КалендарныеГрафики). Запрос к такому регистру выглядит максимально просто и надежно:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Календарь.Дата КАК Период,
| Календарь.ВидДня КАК ВидДня
|ИЗ
| РегистрСведений.ДанныеПроизводственногоКалендаря КАК Календарь
|ГДЕ
| Календарь.Дата МЕЖДУ &ДатаНачала И &ДатаОкончания
| И Календарь.ПроизводственныйКалендарь = &Календарь";
Запрос.УстановитьПараметр("ДатаНачала", Объект.Дата1);
Запрос.УстановитьПараметр("ДатаОкончания", Объект.Дата2);
Запрос.УстановитьПараметр("Календарь", Справочники.ПроизводственныеКалендари.НайтиПоНаименованию("РФ"));
Результат = Запрос.Выполнить().Выгрузить();
Этот способ не только решает проблему получения списка дат без написания сложных алгоритмов, но и сразу предоставляет метаданные о каждом дне (рабочий, предпраздничный, выходной).
Если вы работаете в современной конфигурации, разработанной на базе БСП, обязательно проверьте наличие готовых функций в общих модулях. Например, для конфигураций линейки ЗУП доступна эффективная функция генерации временной таблицы дат:
ЗарплатаКадрыОбщиеНаборыДанных.СоздатьВТПериоды()
Использование встроенных функций БСП значительно сокращает количество строк вашего кода, упрощает его чтение другими разработчиками и снижает вероятность возникновения ошибок при обновлении системы.