Учет праздничных и нерабочих дней в календарных вычислениях строится на расширении базовых возможностей работы с датами, где обычная арифметика времени дополняется бизнес-логикой: выходные, государственные праздники, корпоративные выходные, переносы рабочих дней. В экосистеме 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);
}
Это снижает количество пересчётов при массовых операциях с датами.
Практическая реализация обычно разделяется на несколько уровней:
Разделение обеспечивает независимость логики и упрощает поддержку сложных календарных систем, где стандартная арифметика времени недостаточна.