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

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

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

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

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

Для начала определим переменные и реквизиты, с которыми нам предстоит работать:

Также у нас есть вспомогательное соответствие ДеньНачала, которое сопоставляет числовые коды дней недели с их текстовыми представлениями (1 — "Понедельник", 2 — "Вторник" и так далее).

Почему исходный код автора привел к логическому тупику?

Давайте проанализируем проблемный участок кода, который пытался использовать автор темы:


Для Каждого эл из ДеньНачала Цикл
    Если эл.Ключ = ДеньНачалаМесяца Тогда
        Пока ДеньПервогоПриёма < КоличествоДнейВМесяце Цикл
            Если эл.Ключ = 6 или эл.Ключ = 7 Тогда
                Сообщить(""+ДеньПервогоПриёма+" Число "+"("+эл.Значение+"): "+КоличествоПротеинаВВыходные);
                ДеньПервогоПриёма = ДеньПервогоПриёма + ЧастотаПриёма;
                ДеньНачалаМесяца = ДеньНачалаМесяца + ЧастотаПриёма;
            Иначе 
                ...
            КонецЕсли;
        КонецЦикла;
    КонецЕсли;
КонецЦикла;

В этой конструкции кроются сразу две критические архитектурные ошибки:

  1. Зависание переменной дня недели во внутреннем цикле. Внешний цикл Для Каждого эл из ДеньНачала последовательно обходит элементы соответствия. Как только условие Если эл.Ключ = ДеньНачалаМесяца выполняется, управление передается внутреннему циклу Пока. Этот цикл выполняется до тех пор, пока ДеньПервогоПриёма не превысит количество дней в месяце. При этом переменная эл остается неизменной на протяжении всей работы внутреннего цикла! Программа просто не может переключить день недели, и в результате на экран выводится одно и то же значение (например, "Понедельник") для всех дат.
  2. Неконтролируемый выход переменной дня недели за допустимый диапазон. Строка ДеньНачалаМесяца = ДеньНачалаМесяца + ЧастотаПриёма быстро увеличивает значение переменной выше 7. В соответствии дней недели ключей больше 7 не существует, поэтому при любых последующих проверках или итерациях сопоставление ломается, и код выдает ошибочные результаты либо уходит в бесконечный цикл.

Решение 1: Математический расчет дня недели через оператор остатка от деления

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

Формула расчета текущего дня недели выглядит следующим образом:

ДеньНедели = (ДеньНачалаМесяца + ТекущийДень - 2) % 7 + 1;

Давайте разберем, как работает эта формула на конкретном примере. Предположим, месяц начался в среду (ДеньНачалаМесяца = 3), а первый прием назначен на 10-е число (ТекущийДень = 10):

ДеньНедели = (3 + 10 - 2) % 7 + 1 = 11 % 7 + 1 = 4 + 1 = 5 (Пятница). Расчет абсолютно корректен!

Посмотрим на готовую рабочую реализацию этого алгоритма в коде 1С:


// Инициализация соответствия дней недели
ДеньНачала = Новый Соответствие;
ДеньНачала.Вставить(1, "Понедельник");
ДеньНачала.Вставить(2, "Вторник");
ДеньНачала.Вставить(3, "Среда");
ДеньНачала.Вставить(4, "Четверг");
ДеньНачала.Вставить(5, "Пятница");
ДеньНачала.Вставить(6, "Суббота");
ДеньНачала.Вставить(7, "Воскресенье");

// Инициализируем итоговые переменные для накопления статистики
ВсегоДнейПриема = 0;
ВсегоПротеина    = 0;

// Используем вспомогательную переменную для итерации по дням
ТекущийДень = ДеньПервогоПриема; 

// Запускаем единственный цикл по дням приема
Пока ТекущийДень <= КоличествоДнейВМесяце Цикл
    
    // Вычисляем день недели математически (получаем число от 1 до 7)
    ДеньНедели = (ДеньНачалаМесяца + ТекущийДень - 2) % 7 + 1;
    ИмяДняНедели = ДеньНачала.Получить(ДеньНедели);
    
    // Определяем дозировку в зависимости от дня недели
    Если ДеньНедели = 6 Или ДеньНедели = 7 Тогда
        ТекущийПротеин = КоличествоПротеинаВВыходные;
    Иначе
        ТекущийПротеин = КоличествоПротеинаВБудни;
    КонецЕсли;
    
    // Выводим информацию по текущему дню в окно сообщений
    Сообщить("" + ТекущийДень + " Число (" + ИмяДняНедели + "): " + ТекущийПротеин + "гр.");
    
    // Накапливаем итоговые показатели
    ВсегоДнейПриема = ВсегоДнейПриема + 1;
    ВсегоПротеина    = ВсегоПротеина + ТекущийПротеин;
    
    // Шаг цикла: увеличиваем текущий день на установленную частоту приема
    ТекущийДень = ТекущийДень + ЧастотаПриема;
    
КонецЦикла;

// Выводим сводную информацию по результатам расчета
Сообщить("========================================");
Сообщить("Расчет завершен успешно!");
Сообщить("Всего дней приема протеина: " + ВсегоДнейПриема);
Сообщить("Общее количество принятого протеина: " + ВсегоПротеина + "гр.");

Решение 2: Профессиональный подход с использованием встроенного типа «Дата»

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

Работа с "чистыми" числами не учитывает множество нюансов: високосные годы, разное количество дней в месяцах (28, 29, 30, 31 день) и переход графика на следующий месяц. Платформа 1С предоставляет мощный встроенный тип данных Дата и функции для работы с ним, которые делают код лаконичным, надежным и масштабируемым.

Рассмотрим, как решить ту же задачу, используя объектные возможности платформы:


// Предположим, у нас есть ДатаНачалаРасчета (тип Дата, например, 01.11.2023)
// И ДатаПервогоПриема (тип Дата, например, 10.11.2023)
// ЧастотаПриема по-прежнему выражена в днях

ДатаОкончанияМесяца = КонецМесяца(ДатаНачалаРасчета);
ТекущаяДата = ДатаПервогоПриема;

ВсегоДнейПриема = 0;
ВсегоПротеина    = 0;

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

Сообщить("========================================");
Сообщить("Всего дней приема: " + ВсегоДнейПриема);
Сообщить("Общий объем потребления: " + ВсегоПротеина + "гр.");

Преимущества использования типа «Дата» перед ручными расчетами

Проанализируем преимущества объектного подхода, реализованного во втором решении:

Резюме и рекомендации

Подводя итоги, дадим несколько ключевых советов для написания чистых циклов в 1С:

  1. Никогда не изменяйте переменную, по которой идет итерация внешнего цикла, внутри вложенного цикла, если в этом нет крайней архитектурной необходимости.
  2. Если вам нужно сопоставить периодические события с днями недели, всегда рассматривайте возможность применения математической операции остатка от деления (%) — это избавит вас от необходимости писать лишние вложенные циклы.
  3. В реальных бизнес-задачах всегда отдавайте предпочтение полноценному типу данных Дата. Работа со стандартными датами убережет вашу программу от логических сбоев при переходе между месяцами и в високосные периоды.
← На главную