TemporalAdjusters для сложных манипуляций

Модуль TemporalAdjusters представляет собой набор предопределённых и расширяемых стратегий преобразования дат и времени. Его задача — выполнять нетривиальные календарные операции, которые невозможно выразить простой арифметикой добавления дней, месяцев или лет.

В основе концепции лежит идея коррекции временной точки (Temporal Adjustment): исходная дата преобразуется в новую по заданному правилу, зависящему от календарной логики, а не от фиксированного смещения.


Архитектура TemporalAdjusters

TemporalAdjusters реализует функциональный подход: каждый корректировщик — это функция, принимающая объект типа Temporal и возвращающая новый Temporal.

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

  • корректировщики не мутируют исходные объекты
  • возвращают новый экземпляр даты/времени
  • легко комбинируются с методами with(...)

Базовая сигнатура:

TemporalAdjuster = (temporal) => Temporal

Использование:

import { LocalDate, TemporalAdjusters } from '@js-joda/core';

const date = LocalDate.parse('2026-01-15');
const adjusted = date.with(TemporalAdjusters.firstDayOfMonth());

Основные категории корректировщиков

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

Одна из самых используемых групп — вычисление границ календарных интервалов.

Первый день месяца
TemporalAdjusters.firstDayOfMonth()

Поведение:

  • устанавливает день месяца в 1
  • сохраняет год и месяц

Пример:

LocalDate.parse('2026-05-25')
  .with(TemporalAdjusters.firstDayOfMonth());
// 2026-05-01

Последний день месяца
TemporalAdjusters.lastDayOfMonth()

Логика учитывает:

  • длину месяца
  • високосные годы
  • календарные особенности февраля
LocalDate.parse('2024-02-10')
  .with(TemporalAdjusters.lastDayOfMonth());
// 2024-02-29

Первый день года
TemporalAdjusters.firstDayOfYear()

Результат всегда:

  • 1 января текущего года

Последний день года
TemporalAdjusters.lastDayOfYear()

Корректировщики дней недели

Работа с днями недели — одна из ключевых задач календарных систем.

Следующий день недели

TemporalAdjusters.next(DayOfWeek.MONDAY)

Логика:

  • ищет ближайший следующий указанный день недели
  • если текущий день уже совпадает — переходит к следующей неделе
import { DayOfWeek, LocalDate, TemporalAdjusters } from '@js-joda/core';

LocalDate.parse('2026-05-25')
  .with(TemporalAdjusters.next(DayOfWeek.FRIDAY));

Следующий или текущий день недели

TemporalAdjusters.nextOrSame(DayOfWeek.MONDAY)

Отличие:

  • если текущий день уже подходит — он возвращается без сдвига

Предыдущий день недели

TemporalAdjusters.previous(DayOfWeek.SUNDAY)

Поведение:

  • всегда движется назад
  • исключает текущий день

Предыдущий или текущий

TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)

Корректировка по условиям календаря

TemporalAdjusters позволяет выражать сложные правила бизнес-календарей.

Первый день конкретного месяца

Хотя есть firstDayOfMonth, можно комбинировать с логикой:

date.with(TemporalAdjusters.firstDayOfMonth())

Но важно, что корректировщики можно использовать в цепочках.


Работа с кварталами (через пользовательские корректировщики)

В стандартной библиотеке нет прямого firstDayOfQuarter, но он легко реализуется:

const firstDayOfQuarter = (temporal) => {
  const month = temporal.monthValue();
  const startMonth = month <= 3 ? 1 : month <= 6 ? 4 : month <= 9 ? 7 : 10;

  return temporal.withMonth(startMonth).withDayOfMonth(1);
};

Использование:

LocalDate.parse('2026-08-15')
  .with(firstDayOfQuarter);
// 2026-07-01

Пользовательские TemporalAdjusters

Ключевая сила механизма — возможность создавать собственные корректировщики.

Общий шаблон

const customAdjuster = (temporal) => {
  return temporal.plusDays(10);
};

Использование:

date.with(customAdjuster);

Примеры сложной логики

Следующая рабочая пятница

const nextWorkingFriday = (temporal) => {
  let result = temporal.with(TemporalAdjusters.nextOrSame(DayOfWeek.FRIDAY));

  if (result.dayOfWeek().equals(DayOfWeek.SATURDAY)) {
    result = result.with(TemporalAdjusters.next(DayOfWeek.FRIDAY));
  }

  return result;
};

Последний рабочий день месяца

const lastWorkingDayOfMonth = (temporal) => {
  let result = temporal.with(TemporalAdjusters.lastDayOfMonth());

  const dayOfWeek = result.dayOfWeek();

  if (dayOfWeek.equals(DayOfWeek.SATURDAY)) {
    result = result.minusDays(1);
  }

  if (dayOfWeek.equals(DayOfWeek.SUNDAY)) {
    result = result.minusDays(2);
  }

  return result;
};

Комбинирование TemporalAdjusters

Корректировщики можно применять последовательно, формируя цепочки преобразований.

date
  .with(TemporalAdjusters.firstDayOfMonth())
  .with(TemporalAdjusters.next(DayOfWeek.MONDAY));

Логика:

  1. переход к первому дню месяца
  2. поиск следующего понедельника

Взаимодействие с LocalDate и другими типами

TemporalAdjusters применяется ко всем типам, реализующим интерфейс Temporal, включая:

  • LocalDate
  • LocalDateTime
  • ZonedDateTime

Особенность:

  • при работе с ZonedDateTime сохраняется временная зона
  • при работе с LocalDateTime сохраняются компоненты времени

Пример:

import { ZonedDateTime, TemporalAdjusters, DayOfWeek } from '@js-joda/core';

const zdt = ZonedDateTime.now();

const adjusted = zdt.with(TemporalAdjusters.next(DayOfWeek.MONDAY));

Поведение с переходами календаря

TemporalAdjusters учитывает:

  • длину месяцев
  • високосные годы
  • корректность переходов между месяцами
  • сохранение валидности даты

Пример критического случая:

LocalDate.parse('2024-01-31')
  .with(TemporalAdjusters.firstDayOfMonth());

Результат корректно:

2024-01-01

Производительность и неизменяемость

Модель работы TemporalAdjusters построена на принципах:

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

Это обеспечивает:

  • предсказуемость
  • потокобезопасность
  • удобство в функциональных цепочках обработки дат

Типовые сценарии применения

Финансовые расчёты

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

Планирование задач

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

Бизнес-логика

  • SLA-окна
  • дедлайны
  • автоматическое смещение дат в рамках правил компании

Создание библиотеки правил поверх TemporalAdjusters

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

Пример слоя правил:

const BusinessCalendar = {
  startOfMonth: (d) => d.with(TemporalAdjusters.firstDayOfMonth()),
  endOfMonth: (d) => d.with(TemporalAdjusters.lastDayOfMonth()),
  nextWorkingDay: (d) => d.with(TemporalAdjusters.next(DayOfWeek.MONDAY)),
};

Ограничения механизма

Несмотря на гибкость, существуют ограничения:

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

Эти ограничения компенсируются возможностью создания пользовательских корректировщиков.


Внутренний смысл модели преобразований

TemporalAdjusters реализует абстракцию:

“Дата не изменяется напрямую — она пересчитывается относительно правила”

Это отличает подход от императивных моделей:

  • нет setDate
  • нет мутаций полей
  • нет скрытых изменений состояния

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