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

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

Основной инструмент управления доступностью дат — функция disableDayFn, принимающая объект Date и возвращающая булево значение. Если возвращается true, дата становится недоступной для выбора.

const picker = new Pikaday({
  field: document.getElementById('datepicker'),
  disableDayFn: function(date) {
    return false;
  }
});

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

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

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

const startBlock = new Date('2026-06-10');
const endBlock = new Date('2026-06-20');

const picker = new Pikaday({
  field: document.getElementById('datepicker'),
  disableDayFn: function(date) {
    const time = date.getTime();
    return time >= startBlock.getTime() && time <= endBlock.getTime();
  }
});

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

Нормализация времени для корректного сравнения

При работе с диапазонами важно учитывать, что объекты Date могут содержать различное время суток. Это приводит к некорректным результатам при сравнении, особенно при работе с часовыми поясами или пользовательским вводом.

Рекомендуется приводить даты к началу дня:

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

const startBlock = normalize(new Date('2026-06-10'));
const endBlock = normalize(new Date('2026-06-20'));

const picker = new Pikaday({
  field: document.getElementById('datepicker'),
  disableDayFn: function(date) {
    const d = normalize(date);
    return d >= startBlock && d <= endBlock;
  }
});

Такая нормализация устраняет ошибки, связанные с часовыми смещениями и частичным сравнением времени.

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

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

let selectedStart = null;

const picker = new Pikaday({
  field: document.getElementById('datepicker'),

  onSelect: function(date) {
    selectedStart = date;
  },

  disableDayFn: function(date) {
    if (!selectedStart) return false;

    const d = date.getTime();
    const start = selectedStart.getTime();
    const maxRange = 7 * 24 * 60 * 60 * 1000;

    return d > start && d <= start + maxRange;
  }
});

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

Исключение пересекающихся диапазонов

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

const blockedRanges = [
  [new Date('2026-06-05'), new Date('2026-06-08')],
  [new Date('2026-06-15'), new Date('2026-06-18')],
];

const picker = new Pikaday({
  field: document.getElementById('datepicker'),

  disableDayFn: function(date) {
    const time = date.getTime();

    return blockedRanges.some(range => {
      const start = range[0].getTime();
      const end = range[1].getTime();
      return time >= start && time <= end;
    });
  }
});

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

Инверсия логики: разрешённые диапазоны

В некоторых системах проще задавать разрешённые интервалы, а не запрещённые. В этом случае логика disableDayFn инвертируется.

const allowedRanges = [
  [new Date('2026-06-01'), new Date('2026-06-10')],
  [new Date('2026-06-20'), new Date('2026-06-30')],
];

const picker = new Pikaday({
  field: document.getElementById('datepicker'),

  disableDayFn: function(date) {
    const time = date.getTime();

    const isAllowed = allowedRanges.some(range => {
      const start = range[0].getTime();
      const end = range[1].getTime();
      return time >= start && time <= end;
    });

    return !isAllowed;
  }
});

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

Блокировка будущих и прошлых диапазонов

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

const today = new Date();
today.setHours(0, 0, 0, 0);

const picker = new Pikaday({
  field: document.getElementById('datepicker'),

  disableDayFn: function(date) {
    const d = date.getTime();

    const blockStart = new Date('2026-06-10').getTime();
    const blockEnd = new Date('2026-06-20').getTime();

    const isInBlock = d >= blockStart && d <= blockEnd;
    const isPast = d < today.getTime();

    return isInBlock || isPast;
  }
});

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

Комбинирование диапазонов с одиночными датами

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

const blockedDays = [
  new Date('2026-06-03'),
  new Date('2026-06-12'),
];

const blockedRange = [
  new Date('2026-06-20'),
  new Date('2026-06-25'),
];

const picker = new Pikaday({
  field: document.getElementById('datepicker'),

  disableDayFn: function(date) {
    const time = date.getTime();

    const isSingleBlocked = blockedDays.some(d => d.getTime() === time);

    const isRangeBlocked =
      time >= blockedRange[0].getTime() &&
      time <= blockedRange[1].getTime();

    return isSingleBlocked || isRangeBlocked;
  }
});

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

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

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

Оптимизированный вариант с предварительной сортировкой:

blockedRanges.sort((a, b) => a[0] - b[0]);

function isBlocked(time) {
  for (let i = 0; i < blockedRanges.length; i++) {
    const start = blockedRanges[i][0].getTime();
    const end = blockedRanges[i][1].getTime();

    if (time < start) return false;
    if (time >= start && time <= end) return true;
  }
  return false;
}

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

Такой подход уменьшает количество сравнений при раннем выходе из цикла.

Поведение при пересечении диапазонов и границах

Особое внимание требуется к граничным условиям. Включение или исключение крайних дат (inclusive vs exclusive) должно быть строго определено. В большинстве реализаций используется включающее сравнение:

  • >= start
  • <= end

Это обеспечивает предсказуемое поведение интерфейса, особенно при визуальном отображении диапазонов.

При необходимости исключения границ логика меняется на строгую:

return time > start && time < end;

Связь с визуальным отображением

Хотя disableDayFn управляет доступностью, визуальная индикация диапазонов часто реализуется отдельно через CSS-классы или кастомизацию рендера дней. Это позволяет пользователю различать недоступные и доступные, но неактивные периоды.

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

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

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