В практике начинающих программистов 1С часто встречаются задачи, связанные с генерацией календарных данных. Одной из таких задач является создание структуры данных (например, многомерного массива), содержащей все дни текущего года, разбитые по месяцам. На первый взгляд задача кажется тривиальной, но при ее реализации можно допустить логические ошибки, приводящие к зависанию программы.
В этой статье мы подробно разберем, как правильно работать с циклами при переборе дат, почему возникает бесконечный цикл при неправильном условии, и рассмотрим профессиональные подходы к написанию компактного и быстрого кода.
Необходимо создать двумерный массив (массив массивов). Первое измерение должно содержать 12 элементов, соответствующих месяцам года. Каждый из этих элементов должен представлять собой массив, содержащий все даты конкретного месяца.
Рассмотрим решение, которое часто интуитивно пишут новички. В нем для каждого месяца создается отдельный блок кода, и используется цикл Пока для заполнения дней.
Вот фрагмент кода, который приводит к полному зависанию программы (бесконечному циклу):
ДеньЯнваря = ПервоеЯнваря;
Пока ДеньЯнваря <= КонецМесяца(ДеньЯнваря) Цикл
МассивЯнварь.Добавить(ДеньЯнваря);
ДеньЯнваря = ДеньЯнваря + 86400; // Добавляем одни сутки
КонецЦикла;
Давайте проанализируем, что здесь происходит не так. Проблема кроется в условии цикла: ДеньЯнваря <= КонецМесяца(ДеньЯнваря).
ДеньЯнваря равен '01.01.2023'. Функция КонецМесяца('01.01.2023') возвращает '31.01.2023'. Условие истинно.ДеньЯнваря становится равна '01.02.2023'.КонецМесяца('01.02.2023'). А конец февраля — это '28.02.2023'. Условие снова истинно!Таким образом, переменная даты "убегает" вперед, но и граничное условие (конец месяца этой даты) тоже сдвигается вперед. Условие никогда не станет ложным, и цикл будет работать вечно, перебирая все месяцы и годы, пока не переполнится память или процесс не будет принудительно завершен.
Чтобы исправить ошибку в логике цикла Пока, необходимо вычислить граничное значение (конец месяца) до начала цикла и сохранить его в отдельную переменную. В этом случае граница будет статичной.
Правильный вариант кода с использованием цикла Пока выглядит так:
ДеньЯнваря = ПервоеЯнваря;
// Вычисляем конец месяца один раз перед циклом
КонецЯнваря = КонецМесяца(ДеньЯнваря);
Пока ДеньЯнваря <= КонецЯнваря Цикл
МассивЯнварь.Добавить(ДеньЯнваря);
ДеньЯнваря = ДеньЯнваря + 86400;
КонецЦикла;
В этом случае, когда ДеньЯнваря станет равен '01.02.2023', условие '01.02.2023' <= '31.01.2023' станет ложным, и цикл корректно завершится.
Писать 12 одинаковых блоков кода для каждого месяца — это плохая практика. Это нарушает принцип DRY (Don't Repeat Yourself — не повторяйся). Если вам понадобится изменить логику, придется править код в 12 местах.
Давайте рассмотрим более элегантное решение с использованием вложенных циклов. Мы будем использовать функцию Дата() для конструирования дат и функцию День() для определения количества дней в месяце.
Алгоритм следующий:
&НаКлиенте
Процедура СформироватьКалендарь()
НомерГода = Год(ТекущаяДата());
МассивМесяцев = Новый Массив;
// Перебираем месяцы с 1 по 12
Для НомерМесяца = 1 По 12 Цикл
МассивДней = Новый Массив;
// Получаем дату первого числа месяца, чтобы узнать конец месяца
ПервоеЧислоМесяца = Дата(НомерГода, НомерМесяца, 1);
// Определяем, сколько дней в этом месяце (28, 29, 30 или 31)
// Функция День() от КонецМесяца вернет число - последний день
ПоследнийДеньМесяца = День(КонецМесяца(ПервоеЧислоМесяца));
// Вложенный цикл по дням
Для НомерДня = 1 По ПоследнийДеньМесяца Цикл
// Создаем конкретную дату по году, месяцу и дню
КонкретнаяДата = Дата(НомерГода, НомерМесяца, НомерДня);
МассивДней.Добавить(КонкретнаяДата);
КонецЦикла;
// Добавляем заполненный массив дней текущего месяца в общий массив
МассивМесяцев.Добавить(МассивДней);
КонецЦикла;
// Вывод результата для проверки
Для Каждого Месяц ИЗ МассивМесяцев Цикл
Для Каждого День ИЗ Месяц Цикл
Сообщить(День);
КонецЦикла;
КонецЦикла;
КонецПроцедуры
Этот код занимает гораздо меньше места, легче читается и его проще поддерживать. Использование цикла Для также защищает нас от ошибки бесконечного цикла, так как границы (от 1 до ПоследнийДеньМесяца) определены четко.
Существует еще один интересный способ решить эту задачу, используя математику и свойства массивов. Мы можем пройтись одним сквозным циклом по всем дням года и раскладывать их в нужные "корзины" (вложенные массивы), вычисляя индекс массива на лету.
Так как индексы в массивах 1С начинаются с 0, то месяц Январь (1) должен попасть в индекс 0, Февраль (2) — в индекс 1 и так далее. Формула индекса проста: Месяц(ТекущаяДата) - 1.
&НаКлиенте
Процедура СформироватьКалендарьОптимизировано()
// Создаем структуру массива массивов
// Можно создать циклом, но для наглядности покажем инициализацию через JSON
// Это создаст массив, содержащий 12 пустых массивов
МассивМесяцев = Новый Массив;
Для Сч = 1 По 12 Цикл
МассивМесяцев.Добавить(Новый Массив);
КонецЦикла;
НомерГода = Год(ТекущаяДата());
// Начинаем с 1 января
ТекущийДень = Дата(НомерГода, 1, 1);
// Цикл работает, пока мы находимся в пределах текущего года
Пока Год(ТекущийДень) = НомерГода Цикл
// Вычисляем индекс месяца (0..11)
ИндексМесяца = Месяц(ТекущийДень) - 1;
// Добавляем дату в соответствующий подмассив
МассивМесяцев[ИндексМесяца].Добавить(ТекущийДень);
// Переходим к следующему дню
ТекущийДень = ТекущийДень + 86400;
КонецЦикла;
КонецПроцедуры
Этот метод очень эффективен: мы используем всего один цикл Пока, но корректно распределяем данные благодаря функции Месяц().
При работе с датами и циклами в 1С всегда помните о следующих правилах:
Пока выражение, результат которого меняется вместе с переменной итератора, если это не предусмотрено логикой (как в примере с ошибкой КонецМесяца).Дата(Год, Месяц, День), День(), Месяц(), КонецМесяца(). Они позволяют строить гибкие и надежные алгоритмы.