В библиотеке js-joda отсутствует встроенное понятие календаря
праздников, что соответствует философии неизменяемых, математически
чистых типов даты и времени. Праздничные дни рассматриваются как внешнее
правило, накладываемое поверх базовых типов LocalDate,
ZonedDateTime и LocalDateTime. Это приводит к
необходимости строить отдельный слой доменной модели, отвечающий за
календарные исключения.
Ключевая особенность подхода заключается в том, что праздники не являются частью даты, а представляют собой функцию принадлежности:
Такое разделение позволяет строить расширяемые и локализуемые системы календарей.
Базовый способ хранения праздничных дней — множество
LocalDate. Это обеспечивает O(1)-проверку принадлежности
при использовании структуры Set.
import { LocalDate } from '@js-joda/core';
const holidays = new Set([
LocalDate.of(2026, 1, 1),
LocalDate.of(2026, 5, 9),
LocalDate.of(2026, 12, 31),
]);
Ключевой принцип: все праздники нормализуются до
LocalDate, без учёта времени и часового пояса.
Проверка праздничного дня:
function isHoliday(date, holidaySet) {
return holidaySet.has(date);
}
Праздничный календарь почти всегда пересекается с выходными. В js-joda выходные вычисляются через день недели:
import { DayOfWeek } from '@js-joda/core';
function isWeekend(date) {
const day = date.dayOfWeek();
return day === DayOfWeek.SATURDAY || day === DayOfWeek.SUNDAY;
}
Композитное правило рабочего дня:
function isWorkingDay(date, holidays) {
return !isWeekend(date) && !holidays.has(date);
}
Такой подход делает календарь независимым от региона и политики переноса праздников.
Многие праздничные дни повторяются ежегодно (например, 1 января или 9 мая). Вместо хранения конкретных дат удобно хранить шаблоны:
const recurringHolidays = [
{ month: 1, day: 1 },
{ month: 5, day: 9 },
{ month: 12, day: 31 },
];
Преобразование шаблонов в LocalDate для конкретного
года:
import { LocalDate } from '@js-joda/core';
function buildHolidaysForYear(year, rules) {
return new Set(
rules.map(r => LocalDate.of(year, r.month, r.day))
);
}
При необходимости поддержки диапазонов лет формируется объединённое множество:
function buildHolidayCalendar(startYear, endYear, rules) {
const result = new Set();
for (let y = startYear; y <= endYear; y++) {
for (const r of rules) {
result.add(LocalDate.of(y, r.month, r.day));
}
}
return result;
}
Некоторые праздники определяются не фиксированной датой, а правилом (например, «последний понедельник мая»).
js-joda позволяет вычислять такие даты через перебор дней месяца.
Пример: последний понедельник мая
import { LocalDate, DayOfWeek } from '@js-joda/core';
function lastMondayOfMay(year) {
let date = LocalDate.of(year, 5, 31);
while (date.dayOfWeek() !== DayOfWeek.MONDAY) {
date = date.minusDays(1);
}
return date;
}
Обобщённая функция для «последнего дня недели месяца»:
function lastWeekdayOfMonth(year, month, weekday) {
let date = LocalDate.of(year, month, 1).plusMonths(1).minusDays(1);
while (date.dayOfWeek() !== weekday) {
date = date.minusDays(1);
}
return date;
}
Во многих календарных системах праздник, выпавший на выходной, переносится на ближайший рабочий день.
Правило переноса может быть различным, но типовой вариант — перенос на следующий понедельник:
function shiftIfWeekend(date) {
const day = date.dayOfWeek();
if (day === DayOfWeek.SATURDAY) {
return date.plusDays(2);
}
if (day === DayOfWeek.SUNDAY) {
return date.plusDays(1);
}
return date;
}
Построение набора «фактических» праздников:
function applyObservedRules(holidayDates) {
return new Set([...holidayDates].map(shiftIfWeekend));
}
Важно учитывать возможные коллизии, когда перенос приводит к совпадению с другим праздником; в таких случаях применяется бизнес-правило приоритета.
Одной из наиболее распространённых задач является подсчёт количества рабочих дней между двумя датами.
import { LocalDate } from '@js-joda/core';
function countWorkingDays(start, end, holidays) {
let count = 0;
let date = start;
while (!date.isAfter(end)) {
if (isWorkingDay(date, holidays)) {
count++;
}
date = date.plusDays(1);
}
return count;
}
Такой алгоритм имеет линейную сложность O(n), где n — количество дней в интервале.
Для оптимизации в больших диапазонах используется предварительная агрегация по месяцам или кэширование.
Часто требуется вычислить дату через заданное количество рабочих дней, игнорируя выходные и праздники.
function addWorkingDays(date, days, holidays) {
let result = date;
let remaining = days;
while (remaining > 0) {
result = result.plusDays(1);
if (isWorkingDay(result, holidays)) {
remaining--;
}
}
return result;
}
Отрицательные смещения обрабатываются аналогично с движением назад по календарю.
Праздничные правила всегда применяются на уровне календарной даты, а
не времени. При работе с ZonedDateTime необходимо выделять
локальную дату:
import { ZonedDateTime } from '@js-joda/core';
function isHolidayZoned(zonedDateTime, holidays) {
const localDate = zonedDateTime.toLocalDate();
return holidays.has(localDate);
}
Ключевое правило:
Для сложных систем формируется единый объект календаря:
class BusinessCalendar {
constructor(holidaySet) {
this.holidays = holidaySet;
}
isWeekend(date) {
const d = date.dayOfWeek();
return d === DayOfWeek.SATURDAY || d === DayOfWeek.SUNDAY;
}
isHoliday(date) {
return this.holidays.has(date);
}
isWorkingDay(date) {
return !this.isWeekend(date) && !this.isHoliday(date);
}
nextWorkingDay(date) {
let d = date.plusDays(1);
while (!this.isWorkingDay(d)) {
d = d.plusDays(1);
}
return d;
}
}
Такой слой изолирует бизнес-логику от низкоуровневых операций над датами.
При интенсивных вычислениях календаря применяется предварительное вычисление множества рабочих дней:
function buildWorkingDayIndex(start, end, holidays) {
const index = new Set();
let date = start;
while (!date.isAfter(end)) {
if (isWorkingDay(date, holidays)) {
index.add(date);
}
date = date.plusDays(1);
}
return index;
}
Проверка принадлежности затем становится O(1), что критично для финансовых и планировочных систем.
В реальных системах часто существует несколько уровней праздников:
Композиция выполняется через объединение множеств:
function mergeCalendars(...calendars) {
const result = new Set();
for (const cal of calendars) {
for (const date of cal) {
result.add(date);
}
}
return result;
}
Такой подход позволяет разделять источники правил без их смешивания на уровне логики вычислений.