Рекуррентные события

Рекуррентные (повторяющиеся) события в календарных интерфейсах на базе Pikaday требуют выстраивания отдельного слоя логики поверх базового выбора даты, поскольку сама библиотека Pikaday предназначена исключительно для выбора одиночных дат и диапазонов без встроенной поддержки правил повторяемости.

Основная задача при работе с повторяющимися событиями заключается не в отображении календаря, а в формализации правил генерации дат. На практике используется несколько подходов к описанию повторяемости:

1. Простые интервалы

  • ежедневно
  • еженедельно
  • ежемесячно
  • ежегодно

Такая модель сводится к набору параметров:

  • frequency: тип повторения (daily, weekly, monthly, yearly)
  • interval: шаг повторения (каждые 2 дня, каждые 3 недели)
  • startDate: дата начала серии
  • endDate: дата окончания серии (опционально)

2. Календарные шаблоны Более гибкий вариант включает привязку к структуре календаря:

  • повторение по дням недели (каждый понедельник и четверг)
  • повторение по порядковому номеру недели месяца (второй вторник месяца)
  • повторение по конкретному дню месяца

3. Полноценные правила RRULE Расширенная модель основана на стандарте iCalendar RRULE:

  • FREQ=DAILY
  • FREQ=WEEKLY;BYDAY=MO,WE,FR
  • FREQ=MONTHLY;BYSETPOS=2;BYDAY=TU

Такой подход позволяет описывать сложные сценарии, но требует отдельного парсера и генератора дат.


Хранение рекуррентных событий

При интеграции с календарём, построенным на Pikaday, рекуррентные события обычно не сохраняются как список дат. Вместо этого сохраняется правило:

const event = {
  title: "Тренировка",
  startDate: "2026-06-01",
  recurrence: {
    frequency: "weekly",
    interval: 1,
    daysOfWeek: [1, 3, 5]
  }
};

Ключевой принцип заключается в том, что календарь не хранит каждое вхождение события, а вычисляет их динамически.


Генерация дат для отображения

Поскольку Pikaday работает с отдельными датами, необходимо заранее вычислять множество дат, попадающих в видимый диапазон календаря.

Алгоритм генерации:

  1. Определяется текущий диапазон отображения календаря (месяц или произвольный интервал).
  2. Берётся правило рекуррентности.
  3. Генерируются все даты, попадающие в диапазон.
  4. Эти даты передаются в Pikaday как отмеченные (highlighted / disabled / selected).

Пример базовой генерации:

function generateWeeklyDates(startDate, endDate, weekdays) {
  const result = [];
  const current = new Date(startDate);

  while (current <= endDate) {
    if (weekdays.includes(current.getDay())) {
      result.push(new Date(current));
    }
    current.setDate(current.getDate() + 1);
  }

  return result;
}

Интеграция с Pikaday

Pikaday предоставляет механизм кастомизации отображения дат через функции:

  • beforeShowDay
  • onDraw
  • disableDayFn

Именно через них реализуется визуализация повторяющихся событий.

Подсветка дат

const picker = new Pikaday({
  field: document.getElementById('calendar'),
  beforeShowDay: function(date) {
    const timestamp = date.toDateString();

    if (recurringDatesSet.has(timestamp)) {
      return [true, "recurring-event", "Событие"];
    }

    return [true, "", ""];
  }
});

Здесь используется предварительно рассчитанный набор дат recurringDatesSet, что позволяет обеспечить быстрый рендер без пересчёта логики при каждом обновлении UI.


Оптимизация вычислений

При большом количестве событий прямой пересчёт дат становится неэффективным. Используются следующие стратегии:

Кэширование по месяцам

  • ключ: YYYY-MM
  • значение: массив дат событий

Ленивая генерация

  • вычисление происходит только при смене месяца в календаре

Предварительная индексация

  • события группируются по дням недели и типам повторения

Сложные сценарии повторения

Повторение по N-му дню месяца

Например: «каждый 3-й четверг месяца»

Алгоритм:

  1. Перебор всех дней месяца
  2. Фильтрация по дню недели
  3. Подсчёт порядкового номера совпадений
function getNthWeekdayOfMonth(year, month, weekday, n) {
  let count = 0;
  for (let day = 1; day <= 31; day++) {
    const date = new Date(year, month, day);
    if (date.getMonth() !== month) break;

    if (date.getDay() === weekday) {
      count++;
      if (count === n) return date;
    }
  }
  return null;
}

Исключения из серии

Рекуррентные события часто требуют исключений:

  • отмена конкретной даты
  • перенос одного экземпляра
  • пропуск праздников

Структура:

recurrence: {
  frequency: "monthly",
  interval: 1,
  excludeDates: ["2026-06-15", "2026-07-15"],
  overrideDates: {
    "2026-08-15": "special-event"
  }
}

При генерации:

  1. Сначала строится базовый набор дат
  2. Исключаются excludeDates
  3. Добавляются overrideDates

Визуализация повторяющихся событий

В интерфейсе календаря важно различать:

  • первую дату серии
  • регулярные повторения
  • изменённые экземпляры

Обычно применяются разные CSS-классы:

.recurring-event {
  background-color: #e3f2fd;
}

.recurring-exception {
  background-color: #fff3e0;
}

.recurring-start {
  border: 2px solid #1e88e5;
}

Связь с пользовательским вводом

Поскольку Pikaday не содержит UI для настройки повторяемости, интерфейс обычно строится отдельно:

  • чекбоксы для выбора дней недели
  • селекторы интервалов
  • поле окончания серии

После ввода данные преобразуются в правило генерации, которое затем применяется при отрисовке календаря.


Производственные ограничения

При проектировании системы рекуррентных событий поверх Pikaday важно учитывать:

  • календарь перерисовывается при смене месяца
  • любые тяжёлые вычисления блокируют UI
  • DOM-операции должны быть минимизированы

Поэтому ключевой принцип — перенос всей логики генерации за пределы рендеринга и использование предрасчитанных структур данных.


Расширение модели на временные диапазоны

Иногда повторяется не дата, а интервал времени внутри дня:

{
  frequency: "daily",
  timeRange: {
    start: "10:00",
    end: "11:30"
  }
}

Это требует дополнительного слоя, поскольку Pikaday работает только с датами, а не с временем, и такие данные хранятся отдельно от календарной визуализации.


Объединение нескольких правил

Сложные системы допускают несколько параллельных правил:

recurrence: [
  { frequency: "weekly", daysOfWeek: [1,3,5] },
  { frequency: "monthly", dayOfMonth: 10 }
]

При обработке применяется объединение множеств дат с последующей дедупликацией.


Итоговая архитектурная модель

В связке с Pikaday рекуррентные события всегда реализуются как внешний модуль:

  • слой правил (recurrence engine)
  • слой генерации дат
  • слой кеширования
  • слой визуализации через Pikaday hooks

Такое разделение обеспечивает независимость календарного UI от бизнес-логики повторяемости и позволяет масштабировать систему без модификации самой библиотеки.