Создание массива календарных дней в 1С: разбор ошибки бесконечного цикла и методы оптимизации

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

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

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

Постановка задачи

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

Анализ ошибки: почему возникает бесконечный цикл?

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

Вот фрагмент кода, который приводит к полному зависанию программы (бесконечному циклу):


ДеньЯнваря = ПервоеЯнваря;
Пока ДеньЯнваря <= КонецМесяца(ДеньЯнваря) Цикл 
    МассивЯнварь.Добавить(ДеньЯнваря); 
    ДеньЯнваря = ДеньЯнваря + 86400; // Добавляем одни сутки
КонецЦикла;

Давайте проанализируем, что здесь происходит не так. Проблема кроется в условии цикла: ДеньЯнваря <= КонецМесяца(ДеньЯнваря).

  1. При старте ДеньЯнваря равен '01.01.2023'. Функция КонецМесяца('01.01.2023') возвращает '31.01.2023'. Условие истинно.
  2. Цикл идет, дата увеличивается. Допустим, мы дошли до '31.01.2023'. Условие '31.01.2023' <= '31.01.2023' истинно.
  3. Выполняется тело цикла, и к дате добавляется один день. Переменная ДеньЯнваря становится равна '01.02.2023'.
  4. Внимание, ошибка: Цикл снова проверяет условие. Теперь сравнивается '01.02.2023' <= КонецМесяца('01.02.2023'). А конец февраля — это '28.02.2023'. Условие снова истинно!

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

Решение 1: Фиксация границы цикла

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

Правильный вариант кода с использованием цикла Пока выглядит так:


ДеньЯнваря = ПервоеЯнваря;
// Вычисляем конец месяца один раз перед циклом
КонецЯнваря = КонецМесяца(ДеньЯнваря); 

Пока ДеньЯнваря <= КонецЯнваря Цикл 
    МассивЯнварь.Добавить(ДеньЯнваря); 
    ДеньЯнваря = ДеньЯнваря + 86400; 
КонецЦикла;

В этом случае, когда ДеньЯнваря станет равен '01.02.2023', условие '01.02.2023' <= '31.01.2023' станет ложным, и цикл корректно завершится.

Решение 2: Профессиональный подход (оптимизация кода)

Писать 12 одинаковых блоков кода для каждого месяца — это плохая практика. Это нарушает принцип DRY (Don't Repeat Yourself — не повторяйся). Если вам понадобится изменить логику, придется править код в 12 местах.

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

Алгоритм следующий:

  1. Определяем текущий год.
  2. Запускаем цикл по месяцам от 1 до 12.
  3. Внутри каждого месяца определяем количество дней.
  4. Запускаем вложенный цикл по дням и формируем итоговый массив.

&НаКлиенте
Процедура СформироватьКалендарь()
    
    НомерГода = Год(ТекущаяДата());
    МассивМесяцев = Новый Массив;
    
    // Перебираем месяцы с 1 по 12
    Для НомерМесяца = 1 По 12 Цикл 
        
        МассивДней = Новый Массив;
        
        // Получаем дату первого числа месяца, чтобы узнать конец месяца
        ПервоеЧислоМесяца = Дата(НомерГода, НомерМесяца, 1);
        
        // Определяем, сколько дней в этом месяце (28, 29, 30 или 31)
        // Функция День() от КонецМесяца вернет число - последний день
        ПоследнийДеньМесяца = День(КонецМесяца(ПервоеЧислоМесяца));
        
        // Вложенный цикл по дням
        Для НомерДня = 1 По ПоследнийДеньМесяца Цикл 
            
            // Создаем конкретную дату по году, месяцу и дню
            КонкретнаяДата = Дата(НомерГода, НомерМесяца, НомерДня);
            МассивДней.Добавить(КонкретнаяДата);
            
        КонецЦикла;
        
        // Добавляем заполненный массив дней текущего месяца в общий массив
        МассивМесяцев.Добавить(МассивДней);
        
    КонецЦикла;
    
    // Вывод результата для проверки
    Для Каждого Месяц ИЗ МассивМесяцев Цикл
        Для Каждого День ИЗ Месяц Цикл
            Сообщить(День);
        КонецЦикла;
    КонецЦикла;

КонецПроцедуры

Этот код занимает гораздо меньше места, легче читается и его проще поддерживать. Использование цикла Для также защищает нас от ошибки бесконечного цикла, так как границы (от 1 до ПоследнийДеньМесяца) определены четко.

Решение 3: "Элегантный" подход с использованием индексации

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

Так как индексы в массивах 1С начинаются с 0, то месяц Январь (1) должен попасть в индекс 0, Февраль (2) — в индекс 1 и так далее. Формула индекса проста: Месяц(ТекущаяДата) - 1.


&НаКлиенте
Процедура СформироватьКалендарьОптимизировано()

    // Создаем структуру массива массивов
    // Можно создать циклом, но для наглядности покажем инициализацию через JSON
    // Это создаст массив, содержащий 12 пустых массивов
    МассивМесяцев = Новый Массив;
    Для Сч = 1 По 12 Цикл
        МассивМесяцев.Добавить(Новый Массив);
    КонецЦикла;

    НомерГода = Год(ТекущаяДата());
    
    // Начинаем с 1 января
    ТекущийДень = Дата(НомерГода, 1, 1);
    
    // Цикл работает, пока мы находимся в пределах текущего года
    Пока Год(ТекущийДень) = НомерГода Цикл
        
        // Вычисляем индекс месяца (0..11)
        ИндексМесяца = Месяц(ТекущийДень) - 1;
        
        // Добавляем дату в соответствующий подмассив
        МассивМесяцев[ИндексМесяца].Добавить(ТекущийДень);
        
        // Переходим к следующему дню
        ТекущийДень = ТекущийДень + 86400;
        
    КонецЦикла;

КонецПроцедуры

Этот метод очень эффективен: мы используем всего один цикл Пока, но корректно распределяем данные благодаря функции Месяц().

Выводы

При работе с датами и циклами в 1С всегда помните о следующих правилах:

  1. Никогда не используйте в условии цикла Пока выражение, результат которого меняется вместе с переменной итератора, если это не предусмотрено логикой (как в примере с ошибкой КонецМесяца).
  2. Старайтесь избегать дублирования кода. Если вы видите, что копируете один и тот же блок более двух раз — используйте циклы или выносите код в отдельную процедуру.
  3. Изучайте встроенные функции работы с датами: Дата(Год, Месяц, День), День(), Месяц(), КонецМесяца(). Они позволяют строить гибкие и надежные алгоритмы.
← На главную