Табло над входом в гараж я собрал за один вечер: DS3231, матрица из дешёвых светодиодов, план простой — в Новый год бегущая волна, 23 февраля и 8 марта короткое приветствие, в остальные дни просто часы. Первая версия была написана так, как пишут в первый раз: лестница из условий, по ветке на каждый праздник. К апрелю лестница стала семиэтажной, и в ней обнаружилась опечатка, которую я не видел две недели: день Победы был вбит как 3 мая. Табло честно поздравило меня третьего мая, а девятого промолчало, и я узнал об этом от соседа по гаражу, а не от своего кода.
Вторая версия занимает вдвое меньше строк и расширяема без правки логики: все даты живут в таблице-массиве, а код делает одно и то же — проходит по таблице и спрашивает «сегодня — это оно?». Добавить день рождения кота — значит дописать одну строку таблицы, а не шестую ветку условий. В этом уроке собираем такую таблицу, закрываем январский диапазон одним условием и добиваем полезной мелочью: сколько дней осталось до ближайшего праздника.
Таблица дат вместо лестницы if
Нерабочие праздничные дни российского производственного календаря перечислены в статье 112 Трудового кодекса, и их удобно пересчитать пальцами: восемь дней январских каникул с 1 по 8 января плюс шесть отдельных дат — 23 февраля, 8 марта, 1 мая, 9 мая, 12 июня, 4 ноября. Итого четырнадцать дней в году. Хранить их дешевле всего парой параллельных массивов: в одном месяцы, во втором дни, в третьем — названия для печати. Восемь уникальных дат по два байта каждая — шестнадцать байт ОЗУ, меньше, чем одна строка на экране. Январь отдельно: 2, 3, 4, 5, 6 и 8 января — это не список, а сплошной диапазон, и проверять его честнее одним условием вида «месяц первый и день от двух до восьми», чем восемью строками, где легко промахнуться.
Проверка «сегодня праздник?» — один проход цикла: достаём из объекта DateTime месяц и день, сравниваем с каждой парой таблицы, при совпадении запоминаем индекс. По индексу достаём название. Поля объекта я разбирал в уроке про DateTime — здесь понадобятся только month() и day(). День недели тоже пригодится: метод dayOfTheWeek() возвращает 0…6, причём ноль — понедельник, и это готовый индекс в массив из семи названий «понедельник…воскресенье». В симуляторе я это поведение проверил явно: 8 марта 2025 года вернуло пятёрку — суббота.
Таблица живёт в ОЗУ вместе с кодом, и на Uno её цена смешная: шестнадцать байт на даты и десятки — на строки имён. Если проект упирается в потолок двух килобайт, даты уезжают во флеш-память через PROGMEM и читаются оттуда по требованию — приём разобран в уроке про PROGMEM. Второй резерв экономии — сами строки имён: «Den Pobedy» без падежей и кириллицы короче вдвое, и это не только про память: LCD-экран без перепрошивки знакогенератора кириллицу всё равно не покажет, так что латиница в таких проектах — осознанный выбор, а не костыль.
#include <Wire.h>#include <RTClib.h> RTC_DS3231 rtc; // ст. 112 ТК РФ: восемь дат + диапазон января проверяется отдельноint hol_m[8] = {1, 1, 2, 3, 5, 5, 6, 11};int hol_d[8] = {1, 7, 23, 8, 1, 9, 12, 4};String hol_name[8] = {"Novyj god", "Rozhdestvo", "23 fevralya", "8 marta", "1 maya", "9 maya", "12 iyunya", "4 noyabrya"}; void setup() { Serial.begin(9600); rtc.begin(); if (rtc.lostPower()) { rtc.adjust(DateTime(F(__DATE__), F(__TIME__))); }} void loop() { DateTime now = rtc.now(); int m = now.month(); int d = now.day(); int hit = -1; for (int i = 0; i < 8; i++) { if (hol_m[i] == m && hol_d[i] == d) hit = i; } if (hit >= 0) { Serial.print("Segodnya prazdnik: "); Serial.println(hol_name[hit]); } else if (m == 1 && d >= 2 && d <= 8) { Serial.println("Novogodnie kanikuly"); } else { Serial.println("Obychnyj den"); } delay(3600000UL); // раз в час; в бою лучше миллис-планировщик}Две детали скетча, на которых спотыкаются. Первая — delay() на 3 600 000 мс здесь стоит только ради простоты: час тишины в loop значит, что кнопку или датчик код в это время не видит, и для настоящего устройства проверку даты встраивают в миллис-планировщик — как в уроке про задачи на millis. Вторая — строка lostPower() в setup: календарь бессмыслен, если часы после отключения питания показывают нули, поэтому сначала проверяем, не потеряло ли время ход, и при необходимости заводим его заново. Саму механику adjust и его ловушки я разбирал в уроке про первый скетч с RTClib — там же про то, почему adjust при каждой загрузке перезаписывает честно тикающие часы.
Куда такой календарь встраивается: табло и светильники, реле полива, которое молчит по праздничкам, зуммер-напоминалка на проходной, входной автомат подъезда. А если по дате нужно не только мигать, но и срабатывать в конкретный час — формат почти тот же, что у часов с будильником на LCD: совпадение по индексу праздника плюс проверка часа и минуты. Каждое срабатывание по праздничной логике при этом стоит протоколировать со штампом времени — этим занимается следующий урок раздела, логгер со штампами.
Обратный отсчёт: сколько дней до праздника
Отсчёт «до Нового года осталось N дней» строится на второй таблице — накопительном числе дней от начала года: январь 31, февраль 59, март 90, апрель 120 и так далее до 365. День года = значение для прошлого месяца плюс число дня. Поправка на високосный год — классическое правило: год делится на 4, кроме кратных 100, если только они не кратны 400. Значит, 2024 и 2028 високосные, а 2100 — нет, и для устройства, которое переживёт два века, поправка уже написана правильно: один лишний день добавляем только если месяц старше февраля. Дальше арифметика: разность номеров дней в году, и если она ушла в минус — прибавляем длину года, потому что ближайший такой праздник уже в следующем году. 31 декабря отсчёт честно показывает «до Нового года 1 день». Если счётчик живёт на дисплее, пересчитывайте его раз в час: значения меняются раз в сутки, и бегать циклом по восьми датам каждый loop — трата тактов, пусть и смешная.
Граница применимости этой таблицы — фиксированные даты. Переходящие праздники в неё не положишь никак: дата Пасхи гуляет по весне год от года, и её считают по отдельной формуле, а не перечислением. Вторая мелочь: у межгодового отсчёта есть редкий кейс — разность через 31 декабря, где прошёл невисокосный год, а наступил високосный, даст погрешность в один день. Для гаражного табло это неважно; если важно — сравнивайте не номера дней, а unixtime двух моментов.
| Способ хранения дат | Память | Где ломается |
|---|---|---|
| Лестница if на каждый праздник | ноль байт данных, зато 14 веток кода | дописал дату в одну ветку и забыл вторую: в мае сработало, в июне нет |
| Параллельные массивы месяц/день/имя | 8 дат × 2 байта плюс имена | съехавший порядок строк: имя перестало соответствовать дате |
| Двумерный массив int[8][2] | те же 16 байт, пара связана индексами | на плате работает; в симуляторе двойной индекс отдаёт ноль — проверено |
| Таблица в PROGMEM (плата) | ОЗУ свободно, таблица во флеше | без чтения из pgmspace вместо дат выходят нули |
Симулятор: подвиньте «сегодня»
В симуляторе «сегодня» подставлено строкой rtc.adjust с фиксированной датой — это демо-приём, а не рекомендация для платы: там дата приходит из батареечных часов сама, и про её сохранность после отключений я писал в уроке про millis и RTC. Поменяйте месяц и день в adjust, запустите заново — вердикт и отсчёт пересчитаются. Честная оговорка про движок: время после запуска стоит на месте, как стоп-кадр, секунды не тикают — для проверки календарной логики этого достаточно, потому что проверка смотрит только на месяц и день. Цикл по таблице движок считает по-настоящему, поэтому вердикт здесь — не имитация, а тот же код, что на плате. Функции pad2 и doy в симуляторе ровно те же, что и в скетче для платы выше, — календарная часть переезжает в железо без правок. И проверьте глазами високосную ветку: поставьте в adjust год 2028 и день 28 февраля — мартовские праздники сдвинутся на день позже, чем в 2025-м, и это видно прямо в отсчёте.
- Поставьте в adjust 23 февраля — вердикт должен назвать праздник, а отсчёт показать ноль дней
- Поставьте 30 декабря: до Нового года останется два дня, граница года пройдена прибавкой длины года
- Смените год на 2028 и поставьте 28 февраля: поправка високосного сдвинет март на день позже
- Добавьте в таблицу свою дату — строку в три массива — и убедитесь, что вердикт её подхватил
- Годовой календарь — это массивы пар месяц-день: восемь дат и шестнадцать байт против лестницы if на четырнадцать веток
- Январские каникулы — диапазон 1–8 января: одно условие вместо восьми строк таблицы
- Отсчёт до праздника строится на накопительной таблице дней и високосной поправке: делится на 4, кроме кратных 100, если не кратны 400
- dayOfTheWeek() возвращает 0…6, где ноль — понедельник: готовый индекс массива названий
- Таблицу после правки проверяй пробегом всех дат с печатью — опечатку в дате не видно, пока праздник не наступил не вовремя
- 1. Почему январские каникулы не перечисляют восемью строками таблицы?
- 2. Что вернёт dayOfTheWeek() для субботы?
- 3. Какая поправка делает накопительную таблицу дней верной для 2028 года?
- 4. Как быстрее всего найти опечатку вроде «3 мая» вместо «9 мая» в таблице?
- 5. Почему Пасху не положишь в таблицу фиксированных дат?
Частые вопросы по теме урока
- Почему январские каникулы не перечисляют восемью строками таблицы?
- Дни с 1 по 8 января идут подряд, значит это диапазон — сравнение месяца и границ дня дешевле и надёжнее восьми отдельных строк, в каждой из которых можно опечататься. Это сплошной диапазон 1–8 января, его закрывает одно условие
- Что вернёт dayOfTheWeek() для субботы?
- Нумерация идёт с понедельника: ноль — понедельник, значит суббота — пятёрка. Это готовый индекс массива из семи названий дней. 5
- Какая поправка делает накопительную таблицу дней верной для 2028 года?
- Високосный 2028 делится на 4 и не кратен 100: после февраля таблица сдвигается на единицу. Само правило: год кратен 4, кроме кратных 100, если только не кратен 400. Один день добавляется к любому месяцу после февраля в високосный год
- Как быстрее всего найти опечатку вроде «3 мая» вместо «9 мая» в таблице?
- Компилятор видит только синтаксис. Временный цикл в setup печатает каждую строку таблицы — шестнадцать байт глазами за десять секунд, съехавшая строка заметна сразу. Пробегом всех строк таблицы с печатью пар и названий
- Почему Пасху не положишь в таблицу фиксированных дат?
- Таблица хранит даты, одинаковые каждый год. Пасха гуляет по весне, для неё нужна отдельная формула расчёта, а не строка таблицы. Дата переходящая и считается по формуле каждый год заново