Блокировка дат по праздникам

Блокировка дат по праздничным дням в календарных интерфейсах строится вокруг механизма исключения конкретных дат из доступного диапазона выбора. В контексте Pikaday это реализуется через программное управление логикой доступности дней при отрисовке календаря. Основная цель — исключить возможность выбора дат, которые соответствуют государственным праздникам, корпоративным выходным или другим регламентированным событиям.

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


Модель представления праздничных дат

Для корректной работы механизма блокировки праздничные даты представляются в виде массива объектов Date, приведённых к единому формату:

const holidays = [
  new Date(2026, 0, 1),  // Новый год
  new Date(2026, 2, 8),  // Международный женский день
  new Date(2026, 4, 1),  // Праздник труда
  new Date(2026, 4, 9)   // День Победы
];

Ключевая проблема при работе с датами — несоответствие временных зон. Для избежания ошибок сравнения применяется нормализация до полуночи:

function normalizeDate(date) {
  return new Date(date.getFullYear(), date.getMonth(), date.getDate());
}

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


Использование disableDayFn в Pikaday

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

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

const holidaysSet = new Set(
  holidays.map(d => normalizeDate(d).getTime())
);

const picker = new Pikaday({
  field: document.getElementById('datepicker'),
  disableDayFn: function(date) {
    const normalized = normalizeDate(date).getTime();
    return holidaysSet.has(normalized);
  }
});

Использование Set вместо массива существенно повышает производительность, особенно при большом количестве праздничных дат, поскольку проверка выполняется за O(1).


Статический список праздников

Статический подход применяется в случаях, когда календарь фиксирован и не зависит от внешних данных. Он удобен для локальных приложений, корпоративных систем или административных интерфейсов.

const holidays = [
  '2026-01-01',
  '2026-03-08',
  '2026-05-01',
  '2026-05-09'
];

function parseISO(dateString) {
  const [y, m, d] = dateString.split('-').map(Number);
  return new Date(y, m - 1, d);
}

const holidaySet = new Set(
  holidays.map(d => parseISO(d).getTime())
);

Далее список подключается через disableDayFn:

const picker = new Pikaday({
  field: document.getElementById('datepicker'),
  disableDayFn: date => holidaySet.has(normalizeDate(date).getTime())
});

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


Динамическая генерация праздничных дат

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

Источники данных могут включать:

  • внешние API календарей праздников,
  • серверные генераторы дат,
  • вычисляемые правила (например, Пасха).

Пример загрузки с API:

async function loadHolidays(year, country) {
  const response = await fetch(`/api/holidays?year=${year}&country=${country}`);
  const data = await response.json();

  return data.map(item => {
    const [y, m, d] = item.date.split('-').map(Number);
    return new Date(y, m - 1, d);
  });
}

После загрузки данные интегрируются в календарь:

loadHolidays(2026, 'KZ').then(holidays => {
  const set = new Set(holidays.map(d => normalizeDate(d).getTime()));

  new Pikaday({
    field: document.getElementById('datepicker'),
    disableDayFn: date => set.has(normalizeDate(date).getTime())
  });
});

Работа с переносами выходных и плавающими праздниками

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

Для таких случаев используется генерация дат на основе алгоритмов:

function getEasterDate(year) {
  const f = Math.floor;
  const G = year % 19;
  const C = f(year / 100);
  const H = (C - f(C / 4) - f((8 * C + 13) / 25) + 19 * G + 15) % 30;
  const I = H - f(H / 28) * (1 - f(H / 28) * f(29 / (H + 1)) * f((21 - G) / 11));
  const J = (year + f(year / 4) + I + 2 - C + f(C / 4)) % 7;
  const L = I - J;
  const month = 3 + f((L + 40) / 44);
  const day = L + 28 - 31 * f(month / 4);

  return new Date(year, month - 1, day);
}

Добавление таких дат в общий список позволяет унифицировать обработку:

const easter = getEasterDate(2026);
holidays.push(easter);

Сравнение подходов хранения праздничных дат

Существует три основных модели:

1. Массив Date-объектов

  • простота реализации
  • быстрый старт
  • ограниченная масштабируемость

2. ISO-строки

  • удобство хранения в JSON
  • совместимость с API
  • необходимость парсинга

3. Timestamp-сеты

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

В связке с Pikaday наиболее эффективным считается вариант с Set<number> на основе нормализованных timestamp значений.


Обновление календаря при изменении списка праздников

При динамическом обновлении данных календарь должен пересоздавать внутреннюю логику блокировки. В Pikaday это достигается пересозданием экземпляра или обновлением функции disableDayFn.

function updatePicker(newHolidays) {
  const set = new Set(newHolidays.map(d => normalizeDate(d).getTime()));

  picker.config.disableDayFn = date => set.has(normalizeDate(date).getTime());
  picker.draw();
}

Метод draw() инициирует повторную отрисовку календаря с учётом новой логики блокировки.


Обработка крайних случаев

При блокировке праздничных дат возникают нестандартные ситуации:

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

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

Часовые пояса Нормализация дат должна исключать влияние локального времени, иначе сравнение может давать ложные совпадения.


Интеграция с бизнес-логикой интерфейса

Блокировка праздничных дат часто связана с другими ограничениями:

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

В Pikaday эти правила комбинируются в единой функции:

new Pikaday({
  field: document.getElementById('datepicker'),
  minDate: new Date(2026, 0, 1),
  maxDate: new Date(2026, 11, 31),
  disableDayFn: date => {
    const t = normalizeDate(date).getTime();
    return holidaySet.has(t) || date.getDay() === 0;
  }
});

Такой подход позволяет объединять праздничные дни и выходные в единую систему правил без дублирования логики.


Производительность при больших наборах праздников

При увеличении количества правил блокировки важно учитывать производительность рендера календаря. Основные оптимизации:

  • использование Set вместо массива для поиска
  • предварительная нормализация дат
  • минимизация вычислений внутри disableDayFn
  • кеширование результатов API

Pikaday вызывает disableDayFn многократно при построении интерфейса, поэтому любая тяжёлая логика внутри этой функции приводит к заметной деградации UX.