Календари и планировщики

Работа с календарями в JavaScript требует точных операций над датами без изменения исходных объектов и без побочных эффектов. Библиотека Date-fns реализует этот подход через чистые функции, каждая из которых принимает дату и возвращает новый объект Date.

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

Смещение дат по календарю

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

  • addDays(date, amount) — прибавление дней
  • subDays(date, amount) — вычитание дней
  • addWeeks(date, amount) — смещение по неделям
  • addMonths(date, amount) — смещение по месяцам

Такие операции применяются для построения навигации календаря (следующий/предыдущий месяц), генерации диапазонов дат и расчёта временных окон.

Особенность календарных смещений заключается в учёте длины месяцев и переходов между годами. Например, прибавление одного месяца к 31 января корректно приводит к последнему дню февраля.


Границы календарных периодов

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

Начало и конец периода

  • startOfDay(date) / endOfDay(date)
  • startOfWeek(date, options) / endOfWeek(date, options)
  • startOfMonth(date) / endOfMonth(date)
  • startOfYear(date) / endOfYear(date)

Пример логики недели:

  • начало недели зависит от локали (понедельник или воскресенье)
  • конец недели — смещение на 6 дней от начала

Параметр options.weekStartsOn позволяет управлять этим поведением:

import { startOfWeek, endOfWeek } from 'date-fns'

startOfWeek(new Date(), { weekStartsOn: 1 })
endOfWeek(new Date(), { weekStartsOn: 1 })

Генерация календарной сетки

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

Ключевая функция:

  • eachDayOfInterval({ start, end }) — генерация массива дат между двумя границами

Типичный алгоритм построения сетки месяца:

  1. Получить начало и конец месяца
  2. Расширить диапазон до начала недели первого дня месяца
  3. Расширить диапазон до конца недели последнего дня месяца
  4. Сгенерировать список дней
import {
  startOfMonth,
  endOfMonth,
  startOfWeek,
  endOfWeek,
  eachDayOfInterval
} from 'date-fns'

const monthStart = startOfMonth(date)
const monthEnd = endOfMonth(date)

const gridStart = startOfWeek(monthStart)
const gridEnd = endOfWeek(monthEnd)

const days = eachDayOfInterval({
  start: gridStart,
  end: gridEnd
})

Такой подход используется в календарных компонентах, где требуется стабильная 5–6 недельная сетка отображения.


Форматирование дат в календарях

Отображение календаря требует преобразования даты в строку. Основная функция:

  • format(date, formatString, options)

Часто используемые форматы:

  • yyyy-MM-dd — машинное представление
  • dd.MM.yyyy — локальное отображение
  • EEEE — день недели
  • MMMM yyyy — месяц и год
import { format } from 'date-fns'

format(new Date(), 'dd.MM.yyyy')
format(new Date(), 'MMMM yyyy')

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


Сравнение дат в календарных интерфейсах

Для календарей важно определять равенство дат и их принадлежность к периоду.

Базовые функции сравнения

  • isSameDay(dateLeft, dateRight) — совпадение дня
  • isSameMonth(dateLeft, dateRight) — совпадение месяца
  • isSameYear(dateLeft, dateRight) — совпадение года
  • differenceInCalendarDays(dateLeft, dateRight) — разница в днях без учёта времени
import { isSameDay, differenceInCalendarDays } from 'date-fns'

isSameDay(new Date(), anotherDate)
differenceInCalendarDays(new Date(), anotherDate)

differenceInCalendarDays особенно важна для расчёта длительности событий и построения таймлайнов.


Проверка принадлежности к диапазону

Для планировщиков и календарей событий применяется проверка попадания даты в интервал:

  • isWithinInterval(date, { start, end })
import { isWithinInterval } from 'date-fns'

isWithinInterval(date, {
  start: startOfMonth(date),
  end: endOfMonth(date)
})

Эта операция используется при фильтрации событий по выбранному периоду.


Построение календарных представлений

Месячный календарь

Месячное представление строится на основе сетки недель. Каждая дата снабжается дополнительными атрибутами:

  • принадлежность к текущему месяцу
  • наличие событий
  • статус (выходной, рабочий день)
import { isSameMonth } from 'date-fns'

const enrichedDays = days.map(day => ({
  date: day,
  inCurrentMonth: isSameMonth(day, monthStart)
}))

Недельное представление

Неделя строится проще:

import { startOfWeek, eachDayOfInterval, addDays } from 'date-fns'

const weekStart = startOfWeek(date)

const weekDays = eachDayOfInterval({
  start: weekStart,
  end: addDays(weekStart, 6)
})

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


Дневное расписание

Дневной планировщик строится на основе фиксированных шагов времени. Date-fns не предоставляет готового механизма интервалов по часам, но позволяет формировать их через добавление часов:

  • addHours(date, amount)
  • startOfHour(date)
import { startOfHour, addHours } from 'date-fns'

const hours = Array.from({ length: 24 }, (_, i) =>
  addHours(startOfHour(day), i)
)

Обработка событий календаря

Событие календаря обычно имеет структуру:

  • дата начала
  • дата окончания
  • метаданные (название, тип, приоритет)

Проверка пересечения событий

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

  • начало события раньше конца диапазона
  • конец события позже начала диапазона
const overlaps = (event, range) =>
  event.start <= range.end && event.end >= range.start

Date-fns применяется для нормализации границ:

import { startOfDay, endOfDay } from 'date-fns'

const range = {
  start: startOfDay(date),
  end: endOfDay(date)
}

Расчёт длительности событий

  • differenceInCalendarDays(end, start) — календарная длительность
  • differenceInDays(end, start) — более точный вариант с учётом времени
import { differenceInCalendarDays } from 'date-fns'

differenceInCalendarDays(endDate, startDate)

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


Работа с повторяющимися событиями

Date-fns не содержит встроенного механизма рецидивов, однако предоставляет базовые инструменты для их построения.

Ежедневные повторения

import { addDays } from 'date-fns'

const generateDaily = (start, count) =>
  Array.from({ length: count }, (_, i) =>
    addDays(start, i)
  )

Еженедельные повторения

import { addWeeks } from 'date-fns'

const generateWeekly = (start, count) =>
  Array.from({ length: count }, (_, i) =>
    addWeeks(start, i)
  )

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


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

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

  • начало дня для хранения
  • конец дня для диапазонов
  • округление до часа для тайм-слотов
import { startOfDay, endOfDay, startOfHour } from 'date-fns'

startOfDay(date)
endOfDay(date)
startOfHour(date)

Эта нормализация устраняет проблемы с временными зонами и дробными значениями времени при сравнении.


Построение диапазонных фильтров

Планировщики часто фильтруют события по выбранному периоду:

import { isWithinInterval } from 'date-fns'

const filterEvents = (events, range) =>
  events.filter(event =>
    isWithinInterval(event.start, range)
  )

Для более точного охвата учитываются как начало, так и конец события:

event.start <= range.end && event.end >= range.start

Календарные вычисления с локализацией

Некоторые функции учитывают локаль:

  • начало недели
  • форматирование месяцев и дней
  • названия дней недели
import { format, startOfWeek } from 'date-fns'
import { ru } from 'date-fns/locale'

format(new Date(), 'EEEE', { locale: ru })
startOfWeek(new Date(), { locale: ru })

Локализация критична для корректного отображения календарей в разных регионах.


Построение планировщиков задач

Архитектура календарного планировщика обычно включает:

  • генерацию диапазона дат
  • привязку событий к дням
  • вычисление пересечений
  • сортировку по времени

Пример базовой структуры:

const buildPlanner = (days, events) =>
  days.map(day => ({
    date: day,
    events: events.filter(event =>
      isWithinInterval(day, {
        start: startOfDay(event.start),
        end: endOfDay(event.end)
      })
    )
  }))

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