Последний рабочий день месяца: почему повтор не помещается в одну галочку
Платеж нужно отправлять в последний рабочий день месяца. Пользователь выбирает повтор ежемесячно 31-го и считает задачу решенной. В апреле дата отсутствует, в мае попадает на воскресенье, а в декабре внутренний банк закрывает прием раньше. Календарь честно выполнил записанное правило, но правило оказалось не тем.
Стандарт iCalendar RFC 5545 умеет выразить последний понедельник-пятницу месяца: FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-1. Сначала создается набор будних дат месяца, затем BYSETPOS=-1 выбирает последнюю. Это рабочий день только в смысле дня недели. Праздники конкретной страны стандарт сам не вычитает.
Почему 31-е исчезает
RFC 5545 прямо говорит: экземпляры с недопустимой датой, например 30 февраля, игнорируются и не считаются. Если событие началось 31 января и приложение строит ежемесячный повтор по числу без дополнительного правила, некоторые месяцы будут пропущены. Автоматического перехода на последний день месяца стандарт не обещает.
Для последнего календарного дня используют BYMONTHDAY=-1. Для последнего буднего - набор BYDAY и BYSETPOS=-1. Это разные задачи. Интерфейс приложения может скрывать RRULE, поэтому после создания стоит экспортировать тестовое событие или пролистать год вперед.
Праздник меняет ответ
Пятница может быть официальным нерабочим днем, а суббота - рабочей после переноса в некоторых календарях. Простое MO-FR этого не знает. Нужна отдельная таблица производственных дат, которую обновляют по официальному акту, или ручные исключения на конкретный год.
Для личного напоминания достаточно заранее проверить двенадцать экземпляров и перенести исключения. Для платежной системы правило должно обращаться к официальному календарю, учитывать регион, график банка и внутреннее время отсечения. Слово рабочий без указания чей рабочий день неоднозначно.
Три надежных варианта
Первый - создать ежемесячный повтор на последний будний день и вручную исправить праздники на год вперед. Это прозрачно и подходит одному человеку. Описание события должно содержать дату последней проверки календаря.
Второй - создать отдельные двенадцать дат из утвержденного производственного календаря. Повтора нет, зато каждое событие явно подтверждено. Для критичных сроков это часто безопаснее умной формулы.
Третий - генерировать события из учетной системы, где есть версия календаря, правила переноса и журнал изменений. Тогда изменение официальной даты создает обновление, а не тихо переписывает прошлое.
Время и часовой пояс
Срок в 18:00 должен иметь TZID, если он относится к местному времени города. RFC 5545 различает UTC, локальное время с зоной и плавающее время без зоны. Плавающее 18:00 остается 18:00 на устройстве пользователя, что удобно для обеда, но опасно для московского банковского срока у сотрудника в другом городе.
Для события на весь день время не указывают вообще, используют значение DATE. Запись 00:00 способна сдвинуться на предыдущий день при переносе между зонами. Последний рабочий день с точным сроком лучше представить двумя сущностями: событие на весь день и отдельный сигнал до времени отсечения.
Исключения нельзя править копией
В iCalendar отдельный измененный экземпляр связывается с серией через UID и RECURRENCE-ID. Если пользователь удаляет встречу и создает новую, связь теряется, обновление серии может вернуть старую дату или создать дубль. В корпоративной системе полезно менять экземпляр, а не копировать его.
Проверьте сценарий переноса: ноябрьский срок сдвинули из-за праздника, затем изменили описание всей серии. Сохранилась ли особая дата? При импорте в другое приложение повторите тест. Реализации стандарта различаются по интерфейсу и поддержке сложных правил.
Контрольный список
- Уточнить: календарный, будний, официальный рабочий или банковский день.
- Выбрать BYMONTHDAY=-1 либо BYDAY плюс BYSETPOS=-1.
- Добавить официальные праздники и переносы конкретного года.
- Указать часовой пояс и время отсечения.
- Проверить все экземпляры минимум на 12 месяцев.
- Назначить владельца ежегодного обновления.
Календарная автоматизация надежна, когда формула описывает реальное правило, а исключения имеют источник. Одна галочка повторять ежемесячно удобна, но она не понимает ни праздников, ни банков, ни смысла слова рабочий.



