Расчет праздничных дней

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

Выделяются несколько устойчивых моделей:

  • фиксированные даты (например, 1 января)
  • плавающие даты (например, первый понедельник месяца)
  • вычисляемые религиозные даты (например, Пасха)
  • региональные наборы праздников

Day.js в этом контексте выступает как слой нормализации и сравнения дат, а не как источник календарной логики.


Представление праздничных дат в структуре данных

Базовый способ хранения праздников — массив объектов, где каждая запись содержит минимум два поля: дату и описание. В Day.js удобнее всего работать с ISO-строками или объектами dayjs.

import dayjs from 'dayjs';

const holidays = [
  { date: dayjs('2026-01-01'), name: 'Новый год' },
  { date: dayjs('2026-05-09'), name: 'День Победы' },
  { date: dayjs('2026-03-08'), name: 'Международный женский день' }
];

Для сравнения дат важно учитывать, что объекты Day.js неизменяемы, а сравнение выполняется через методы:

const isHoliday = (date) => {
  return holidays.some(h =>
    h.date.isSame(date, 'day')
  );
};

Ключевой момент заключается в уровне точности сравнения. Указание 'day' позволяет игнорировать время суток и сравнивать только календарные дни.


Нормализация дат перед вычислениями

При работе с праздничными днями все входные значения приводятся к единому формату:

const normalize = (date) => dayjs(date).startOf('day');

Это предотвращает ошибки, связанные с часовыми поясами и различиями во времени:

const target = normalize('2026-05-09T18:45:00');

const found = holidays.some(h =>
  h.date.isSame(target, 'day')
);

Day.js оперирует локальным временем по умолчанию, поэтому критически важно фиксировать границу суток через startOf('day').


Фильтрация праздничных дней в диапазоне

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

const getHolidaysInRange = (start, end) => {
  const from = dayjs(start);
  const to = dayjs(end);

  return holidays.filter(h =>
    (h.date.isAfter(from) || h.date.isSame(from, 'day')) &&
    (h.date.isBefore(to) || h.date.isSame(to, 'day'))
  );
};

Для упрощения часто применяется вспомогательная библиотека dayjs/plugin/isBetween:

import isBetween from 'dayjs/plugin/isBetween';
dayjs.extend(isBetween);

const getHolidaysInRange = (start, end) => {
  return holidays.filter(h =>
    h.date.isBetween(start, end, 'day', '[]')
  );
};

Диапазон '[]' включает границы интервала, что критично при календарных расчетах.


Исключение праздничных дней при вычислении рабочих периодов

Одна из наиболее распространённых задач — исключение праздничных дней из последовательности дат. Это применяется при расчёте сроков исполнения задач и бизнес-планировании.

const isWorkingDay = (date) => {
  const d = dayjs(date);
  const weekday = d.day(); // 0 - воскресенье, 6 - суббота

  const weekend = (weekday === 0 || weekday === 6);

  const holiday = holidays.some(h =>
    h.date.isSame(d, 'day')
  );

  return !weekend && !holiday;
};

На основе этой функции можно фильтровать диапазон дат:

const getWorkingDays = (start, end) => {
  let current = dayjs(start);
  const last = dayjs(end);

  const result = [];

  while (current.isBefore(last) || current.isSame(last, 'day')) {
    if (isWorkingDay(current)) {
      result.push(current);
    }
    current = current.add(1, 'day');
  }

  return result;
};

Использование plugin business days

Для задач, связанных с деловыми календарями, часто применяется расширение dayjs-business-days.

import businessDays from 'dayjs-business-days';

dayjs.extend(businessDays);

Конфигурация праздничных дней:

dayjs.updateLocale('en', {
  holidays: [
    '2026-01-01',
    '2026-05-09',
    '2026-03-08'
  ],
  holidayFormat: 'YYYY-MM-DD'
});

Проверка рабочего дня:

dayjs('2026-05-09').isBusinessDay();

Добавление рабочих дней с автоматическим пропуском выходных и праздников:

dayjs('2026-01-01').businessDaysAdd(10);

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


Вычисление подвижных праздников

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

Алгоритм вычисления (упрощённая версия, григорианский календарь):

const getEaster = (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(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 dayjs(`${year}-${month}-${day}`);
};

На основе Пасхи строятся связанные праздники:

const easter = getEaster(2026);

const holidaysDynamic = [
  easter,
  easter.add(1, 'day'),
  easter.add(49, 'day')
];

Комбинирование фиксированных и динамических календарей

В реальных системах праздничный календарь формируется объединением нескольких источников:

const fixedHolidays = [
  dayjs('2026-01-01'),
  dayjs('2026-05-09')
];

const dynamicHolidays = [
  getEaster(2026),
  getEaster(2026).add(1, 'day')
];

const allHolidays = [...fixedHolidays, ...dynamicHolidays];

Для ускорения проверок список часто преобразуется в структуру Set с нормализованными строками:

const holidaySet = new Set(
  allHolidays.map(d => d.format('YYYY-MM-DD'))
);

const isHolidayFast = (date) =>
  holidaySet.has(dayjs(date).format('YYYY-MM-DD'));

Такой подход снижает сложность проверки с O(n) до O(1).


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

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

const calendars = {
  kz: {
    holidays: [
      '01-01',
      '03-08',
      '05-09'
    ]
  },
  ru: {
    holidays: [
      '01-01',
      '01-07',
      '05-09'
    ]
  }
};

Проверка с учётом года:

const isLocalizedHoliday = (date, locale) => {
  const d = dayjs(date);
  const md = d.format('MM-DD');

  return calendars[locale].holidays.includes(md);
};

Оптимизация вычислений в больших диапазонах

При обработке календарей за несколько лет важно избегать повторного создания объектов Day.js внутри циклов.

Оптимизации включают:

  • предварительное преобразование праздничных дат в хэш-структуры
  • использование строкового сравнения вместо объектов
  • минимизация вызовов dayjs() внутри итераций

Пример предрасчёта:

const buildCalendarIndex = (holidays) => {
  return holidays.reduce((acc, h) => {
    acc[h.format('YYYY-MM-DD')] = true;
    return acc;
  }, {});
};

const holidayIndex = buildCalendarIndex(allHolidays);

Итеративное построение календаря без выходных и праздников

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

const buildCalendar = (start, days) => {
  const result = [];
  let current = dayjs(start);

  while (result.length < days) {
    const key = current.format('YYYY-MM-DD');

    const weekday = current.day();
    const isWeekend = weekday === 0 || weekday === 6;

    if (!isWeekend && !holidayIndex[key]) {
      result.push(current);
    }

    current = current.add(1, 'day');
  }

  return result;
};

Объединение правил в единую систему вычислений

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

  • слой нормализации дат (Day.js)
  • слой правил выходных
  • слой фиксированных праздников
  • слой динамических праздников
  • слой локализации

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