Учет праздников

Учет праздничных и нерабочих дней в календарных вычислениях строится на расширении базовых возможностей работы с датами, где обычная арифметика времени дополняется бизнес-логикой: выходные, государственные праздники, корпоративные выходные, переносы рабочих дней. В экосистеме date-fns это реализуется через комбинацию функций работы с интервалами, сравнением дат и проверкой условий, а также через построение собственного слоя календарных правил поверх примитивных операций.

Праздничные дни в прикладных задачах почти всегда требуют явного набора данных. Наиболее устойчивым форматом является использование строк в ISO-формате YYYY-MM-DD, поскольку он исключает неоднозначности и упрощает сравнение.

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

const holidaySet = new Set(holidays);

Ключевая особенность:

  • Set обеспечивает проверку принадлежности за O(1)
  • строки исключают проблемы временных зон при корректной нормализации даты

Для большинства бизнес-календарей этого уровня достаточно, если все даты приводятся к локальному «дню без времени».

Нормализация даты к календарному дню

Работа с date-fns требует строгого разделения между моментом времени и календарным днём. Праздники почти всегда сравниваются именно по дню.

import { format } from 'date-fns';

function toDayKey(date) {
  return format(date, 'yyyy-MM-dd');
}

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

Проверка выходных дней

date-fns предоставляет встроенную функцию isWeekend, которая определяет субботу и воскресенье как нерабочие дни.

import { isWeekend } from 'date-fns';

function isNonWorkingDay(date, holidaySet) {
  const day = toDayKey(date);

  return isWeekend(date) || holidaySet.has(day);
}

Логика объединения условий:

  • выходные дни определяются календарной функцией
  • праздники — через пользовательский набор
  • итоговое условие строится через логическое «ИЛИ»

Генерация диапазона дат

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

import { eachDayOfInterval } from 'date-fns';

function getDays(start, end) {
  return eachDayOfInterval({ start, end });
}

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

Выделение рабочих дней из интервала

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

function getWorkingDays(start, end, holidaySet) {
  const days = eachDayOfInterval({ start, end });

  return days.filter((date) => {
    const day = toDayKey(date);

    return !isWeekend(date) && !holidaySet.has(day);
  });
}

Состав правила:

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

Добавление рабочих дней к дате

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

function addWorkingDays(date, amount, holidaySet) {
  let current = new Date(date);
  let added = 0;

  while (added < amount) {
    current.setDate(current.getDate() + 1);

    if (!isWeekend(current) && !holidaySet.has(toDayKey(current))) {
      added++;
    }
  }

  return current;
}

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

Вычитание рабочих дней

Обратная операция реализуется симметрично.

function subWorkingDays(date, amount, holidaySet) {
  let current = new Date(date);
  let subtracted = 0;

  while (subtracted < amount) {
    current.setDate(current.getDate() - 1);

    if (!isWeekend(current) && !holidaySet.has(toDayKey(current))) {
      subtracted++;
    }
  }

  return current;
}

Разница в рабочих днях

date-fns предоставляет функцию differenceInBusinessDays, которая учитывает только выходные дни. Для расширенного сценария с праздниками требуется модификация логики.

import { eachDayOfInterval } from 'date-fns';

function differenceInWorkingDays(start, end, holidaySet) {
  const days = eachDayOfInterval({ start, end });

  return days.reduce((acc, date) => {
    const day = toDayKey(date);

    if (!isWeekend(date) && !holidaySet.has(day)) {
      return acc + 1;
    }

    return acc;
  }, 0);
}

Учёт локальных праздничных календарей

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

const calendars = {
  kz: new Set(['2026-01-01', '2026-03-21']),
  ru: new Set(['2026-01-01', '2026-05-09']),
  us: new Set(['2026-01-01', '2026-07-04'])
};

function getHolidaySet(region) {
  return calendars[region] || new Set();
}

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

Подвижные праздники и вычисляемые даты

Некоторые праздничные даты зависят от календарных алгоритмов (например, Пасха). В таких случаях список праздников формируется функцией.

function getDynamicHolidays(year) {
  return new Set([
    `${year}-01-01`,
    `${year}-03-08`,
    `${year}-05-01`
  ]);
}

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

Кэширование календаря

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

const cache = new Map();

function getCachedHolidays(region, year) {
  const key = `${region}-${year}`;

  if (!cache.has(key)) {
    cache.set(key, getDynamicHolidays(year));
  }

  return cache.get(key);
}

Это снижает количество пересчётов при массовых операциях с датами.

Архитектура слоя бизнес-дат

Практическая реализация обычно разделяется на несколько уровней:

  • Базовый слой date-fns: операции над датами, интервалами и форматированием
  • Календарный слой: праздники, выходные, региональные правила
  • Бизнес-слой: рабочие дни, SLA, сроки задач
  • Слой кеширования: ускорение повторных вычислений

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