При разработке алгоритмов планирования, построения графиков платежей, поставок или тренировок начинающие программисты часто сталкиваются с логическими ошибками в циклах. Типичные симптомы таких ошибок — программа зависает (уходит в бесконечный цикл), либо во всех выводимых строках дублируется одно и то же значение (например, один и тот же день недели).
В этой статье мы подробно разберем учебную, но крайне показательную задачу по расчету графика приема протеина на месяц. Мы детально проанализируем, почему возникают ошибки зацикливания, как неправильное проектирование вложенных циклов ломает логику программы, и как решить эту задачу изящно и оптимально с помощью базовой математики.
Перед нами стоит задача: если нужный человек найден в информационной базе, необходимо рассчитать для него график приема протеина на расчетный месяц и вывести результаты пользователю через окно сообщений. Нам известны следующие параметры:
Истина) или следует сделать первый шаг и сместить прием на величину частоты (Ложь).На выходе программа должна напечатать детальный график по дням приема, общее количество дней приема и суммарный вес принятого протеина за месяц.
Рассмотрим две критические ошибки, которые часто допускают разработчики при попытке сопоставить дату месяца и день недели в цикле.
Ошибка №1: Застревание на одном дне недели. Изначально автор использовал два вложенных цикла. Внешний цикл обходил коллекцию дней недели (Для каждого эл из ДеньНачала), а внутренний цикл (Пока ДеньПервогоПриёма < КоличествоДнейВМесяце) занимался расчетом дней приема. Программа находила первый день начала месяца, заходила во внутренний цикл Пока и выполняла его до конца месяца. Однако, поскольку этот цикл крутился внутри одной и той же итерации внешнего цикла, переменная дня недели эл.Значение оставалась неизменной. В результате программа выводила график, в котором все числа месяца соответствовали одному дню недели, например, только понедельнику.
Ошибка №2: Бесконечный цикл при перемене мест. Пытаясь решить первую проблему, автор поменял циклы местами и добавил условие сопоставления внутри. Логика сломалась в строке инкремента индекса дня недели: ДеньНачалаМесяца = ДеньНачалаМесяца + ЧастотаПриёма;. Так как переменная ДеньНачалаМесяца постоянно увеличивалась на величину шага, она быстро превысила число 7. Условие Если эл.Ключ = ДеньНачалаМесяца больше никогда не выполнялось, а значит, строка с увеличением даты приема ДеньПервогоПриёма = ДеньПервогоПриёма + ЧастотаПриёма перестала вызываться. Значение переменной ДеньПервогоПриёма «застыло» на месте, условие выхода из цикла Пока никогда не достигалось, и программа уходила в бесконечный цикл, намертво блокируя работу системы.
Для сопоставления числа месяца с днем недели нам абсолютно не нужны вложенные циклы и обход коллекции дней недели при каждой итерации. Это избыточная операция, снижающая производительность алгоритма. Поскольку неделя строго циклична и состоит из 7 дней, мы можем рассчитать день недели для любого числа месяца с помощью обычной математической формулы остатка от деления.
Давайте выясним, как работает эта формула:
НомерДняНедели = (ДеньНачалаМесяца + ТекущийДень - 2) % 7 + 1;
Разберем ее по шагам на конкретном примере. Представим, что месяц начинается в четверг (ДеньНачалаМесяца = 4). Нам нужно определить день недели для 6-го числа этого месяца (ТекущийДень = 6):
4 + 6 = 10.10 - 2 = 8.%. Получаем 8 % 7 = 1.1 + 1 = 2 (вторник).Проверим вручную: если 1-е число — четверг, то 2-е — пятница, 3-е — суббота, 4-е — воскресенье, 5-е — понедельник, 6-е — вторник. Формула работает абсолютно верно и мгновенно!
Ниже представлен готовый к использованию, оптимизированный и безопасный код процедуры для кнопки формы «Вывести график». В коде предусмотрена валидация входных данных, предотвращающая возможные зависания при нулевой частоте приема.
&НаКлиенте
Процедура ВывестиГрафик(Команда)
// 1. Проверим заполнение ключевого реквизита поиска
Если Не ЗначениеЗаполнено(ПоискПоФИО) Тогда
ПоказатьПредупреждение(, "Пожалуйста, заполните поле поиска по ФИО!");
Возврат;
КонецЕсли;
// 2. Поиск человека в табличной части (эмулируем поиск)
СтрокаЧеловека = ТаблицаЛюдей.Найти(ПоискПоФИО, "ФИО");
Если СтрокаЧеловека = Неопределено Тогда
Сообщить("Данный человек не найден в таблице!");
Возврат;
КонецЕсли;
// 3. Заполним соответствие наименований дней недели
ДниНедели = Новый Соответствие;
ДниНедели.Вставить(1, "понедельник");
ДниНедели.Вставить(2, "вторник");
ДниНедели.Вставить(3, "среда");
ДниНедели.Вставить(4, "четверг");
ДниНедели.Вставить(5, "пятница");
ДниНедели.Вставить(6, "суббота");
ДниНедели.Вставить(7, "воскресенье");
// 4. Загрузим параметры расчета
Частота = СтрокаЧеловека.ЧастотаПриема;
// Защита от бесконечного цикла: частота должна быть строго больше нуля
Если Частота <= 0 Тогда
ПоказатьПредупреждение(, "Частота приема должна быть больше 0 дней!");
Возврат;
КонецЕсли;
// Определяем стартовую дату приема с учетом флага "Прием с первого дня"
ТекущийДень = СтрокаЧеловека.ДеньПервогоПриема;
Если Не СтрокаЧеловека.ПриемСПервогоДня Тогда
ТекущийДень = ТекущийДень + Частота;
КонецЕсли;
ОбщийВесПротеина = 0;
ВсегоДнейПриема = 0;
Сообщить("--- График приема протеина для: " + СтрокаЧеловека.ФИО + " ---");
// 5. Основной цикл расчета по дням месяца (в один проход)
Пока ТекущийДень <= КоличествоДнейВМесяце Цикл
// Математический расчет дня недели без использования вложенных циклов
НомерДняНедели = (ДеньНачалаМесяца + ТекущийДень - 2) % 7 + 1;
ИмяДняНедели = ДниНедели.Получить(НомерДняНедели);
// Определение дозировки в зависимости от типа дня (будний или выходной)
Если НомерДняНедели = 6 Или НомерДняНедели = 7 Тогда
ПротеинСегодня = КоличествоПротеинаВВыходные;
Иначе
ПротеинСегодня = КоличествоПротеинаВБудни;
КонецЕсли;
// Выводим информацию по текущему дню приема
Сообщить(Строка(ТекущийДень) + " число (" + ИмяДняНедели + "): " + Строка(ПротеинСегодня) + "гр.");
// Накапливаем статистику
ОбщийВесПротеина = ОбщийВесПротеина + ПротеинСегодня;
ВсегоДнейПриема = ВсегоДнейПриема + 1;
// Делаем шаг цикла вперед на указанную частоту приема
ТекущийДень = ТекущийДень + Частота;
КонецЦикла;
// 6. Выводим суммарные итоги расчета
Сообщить("--------------------------------------------------");
Сообщить("1. Общее количество принятого протеина: " + ОбщийВесПротеина + " гр.");
Сообщить("2. Количество приемов в днях: " + ВсегоДнейПриема);
КонецПроцедуры
Проанализируем преимущества использования математического расчета вместо вложенных циклов:
O(N*M) до линейной O(N), где N — количество дней приема в месяце. Это гарантирует мгновенное выполнение кода даже при расчете графиков на несколько лет вперед.Частота на значение меньше или равное нулю, программа аппаратно защищена от зависаний интерфейса и перегрузки процессора.ДеньНедели(Дата).