При разработке алгоритмов планирования, составления периодических графиков или расчетов в среде 1С программисты часто сталкиваются с необходимостью сопоставления порядковых чисел месяца с днями недели. Типичным примером такой задачи является расчет графика приема лекарств, спортивного питания (протеина) или выполнения периодических задач с разной дозировкой или трудоемкостью в будние и выходные дни.
Неопытные разработчики часто пытаются решить эту задачу «в лоб» через вложенные циклы и ручной перебор дней недели. Это приводит к логическим тупикам: циклы либо зацикливаются, либо выводят статические, неизменяющиеся данные. В этой статье мы подробно разберем, почему возникает эта ошибка, проанализируем неверный подход и рассмотрим два эффективных способа решения задачи: математический (через оператор остатка от деления) и профессиональный (с использованием стандартного типа данных Дата).
Для начала определим переменные и реквизиты, с которыми нам предстоит работать:
Также у нас есть вспомогательное соответствие ДеньНачала, которое сопоставляет числовые коды дней недели с их текстовыми представлениями (1 — "Понедельник", 2 — "Вторник" и так далее).
Давайте проанализируем проблемный участок кода, который пытался использовать автор темы:
Для Каждого эл из ДеньНачала Цикл
Если эл.Ключ = ДеньНачалаМесяца Тогда
Пока ДеньПервогоПриёма < КоличествоДнейВМесяце Цикл
Если эл.Ключ = 6 или эл.Ключ = 7 Тогда
Сообщить(""+ДеньПервогоПриёма+" Число "+"("+эл.Значение+"): "+КоличествоПротеинаВВыходные);
ДеньПервогоПриёма = ДеньПервогоПриёма + ЧастотаПриёма;
ДеньНачалаМесяца = ДеньНачалаМесяца + ЧастотаПриёма;
Иначе
...
КонецЕсли;
КонецЦикла;
КонецЕсли;
КонецЦикла;
В этой конструкции кроются сразу две критические архитектурные ошибки:
Для Каждого эл из ДеньНачала последовательно обходит элементы соответствия. Как только условие Если эл.Ключ = ДеньНачалаМесяца выполняется, управление передается внутреннему циклу Пока. Этот цикл выполняется до тех пор, пока ДеньПервогоПриёма не превысит количество дней в месяце. При этом переменная эл остается неизменной на протяжении всей работы внутреннего цикла! Программа просто не может переключить день недели, и в результате на экран выводится одно и то же значение (например, "Понедельник") для всех дат.ДеньНачалаМесяца = ДеньНачалаМесяца + ЧастотаПриёма быстро увеличивает значение переменной выше 7. В соответствии дней недели ключей больше 7 не существует, поэтому при любых последующих проверках или итерациях сопоставление ломается, и код выдает ошибочные результаты либо уходит в бесконечный цикл.Чтобы решить задачу элегантно и без лишних вложенных циклов, мы применим математический подход. День недели для абсолютно любого числа месяца можно вычислить по формуле, зная лишь день недели первого числа этого месяца. Для этого в языке 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;
ВсегоПротеина = ВсегоПротеина + ТекущийПротеин;
// Шаг цикла: увеличиваем текущий день на установленную частоту приема
ТекущийДень = ТекущийДень + ЧастотаПриема;
КонецЦикла;
// Выводим сводную информацию по результатам расчета
Сообщить("========================================");
Сообщить("Расчет завершен успешно!");
Сообщить("Всего дней приема протеина: " + ВсегоДнейПриема);
Сообщить("Общее количество принятого протеина: " + ВсегоПротеина + "гр.");
Математический расчет — это отличное упражнение для развития алгоритмического мышления. Однако в реальной практике разработки в среде 1С профессионалы предпочитают избегать хранения дат в виде простых чисел (например, хранить отдельно число месяца и отдельно день недели текстом).
Работа с "чистыми" числами не учитывает множество нюансов: високосные годы, разное количество дней в месяцах (28, 29, 30, 31 день) и переход графика на следующий месяц. Платформа 1С предоставляет мощный встроенный тип данных Дата и функции для работы с ним, которые делают код лаконичным, надежным и масштабируемым.
Рассмотрим, как решить ту же задачу, используя объектные возможности платформы:
// Предположим, у нас есть ДатаНачалаРасчета (тип Дата, например, 01.11.2023)
// И ДатаПервогоПриема (тип Дата, например, 10.11.2023)
// ЧастотаПриема по-прежнему выражена в днях
ДатаОкончанияМесяца = КонецМесяца(ДатаНачалаРасчета);
ТекущаяДата = ДатаПервогоПриема;
ВсегоДнейПриема = 0;
ВсегоПротеина = 0;
Пока ТекущаяДата <= ДатаОкончанияМесяца Цикл
// Получаем день недели с помощью встроенной функции платформы 1С
// Функция возвращает число от 1 (понедельник) до 7 (воскресенье)
ЧисловойДеньНедели = ДеньНедели(ТекущаяДата);
ИмяДняНедели = Формат(ТекущаяДата, "ДФ=дддд"); // Получаем полное название дня недели локализованно
Если ЧисловойДеньНедели = 6 Или ЧисловойДеньНедели = 7 Тогда
ТекущийПротеин = КоличествоПротеинаВВыходные;
Иначе
ТекущийПротеин = КоличествоПротеинаВБудни;
КонецЕсли;
// Выводим информацию по текущему дню
Сообщить("" + Формат(ТекущаяДата, "ДФ=д") + " Число (" + ИмяДняНедели + "): " + ТекущийПротеин + "гр.");
// Накапливаем итоги
ВсегоДнейПриема = ВсегоДнейПриема + 1;
ВсегоПротеина = ВсегоПротеина + ТекущийПротеин;
// Прибавляем интервал времени в секундах (в сутках 86400 секунд)
ТекущаяДата = ТекущаяДата + (ЧастотаПриема * 86400);
КонецЦикла;
Сообщить("========================================");
Сообщить("Всего дней приема: " + ВсегоДнейПриема);
Сообщить("Общий объем потребления: " + ВсегоПротеина + "гр.");
Проанализируем преимущества объектного подхода, реализованного во втором решении:
ДеньНедели() гарантирует точный результат для любого года и века.Формат() с маской "ДФ=дддд" мы мгновенно получаем красивое текстовое представление дня недели на языке пользователя, без необходимости вручную заполнять соответствия ДеньНачала.Подводя итоги, дадим несколько ключевых советов для написания чистых циклов в 1С:
%) — это избавит вас от необходимости писать лишние вложенные циклы.Дата. Работа со стандартными датами убережет вашу программу от логических сбоев при переходе между месяцами и в високосные периоды.