Создание собственных adjuster'ов

В библиотеке js-joda adjuster — это объект или функция, изменяющая дату или время по определённому правилу. Концепция пришла из Java Time API и позволяет инкапсулировать повторяемую логику модификации временных объектов.

Adjuster применяется через метод with():

const upd ated = date.with(adjuster)

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

Примеры типичных задач:

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

Интерфейс TemporalAdjuster

В основе механизма лежит интерфейс TemporalAdjuster.

Adjuster обязан реализовать метод:

adjustInto(temporal)

Этот метод принимает временной объект и возвращает новый модифицированный объект.

Простейшая структура custom adjuster:

const customAdjuster = {
    adjustInto(temporal) {
        return temporal.plusDays(1)
    }
}

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

const { LocalDate } = require('@js-joda/core')

const date = LocalDate.parse('2025-03-10')

const nextDay = date.with(customAdjuster)

console.log(nextDay.toString())

Результат:

2025-03-11

Неизменяемость temporal-объектов

Все объекты в js-joda immutable.

Это означает:

  • исходный объект никогда не изменяется;
  • adjuster всегда возвращает новый экземпляр;
  • цепочки преобразований безопасны;
  • отсутствуют побочные эффекты.

Пример:

const date = LocalDate.parse('2025-01-01')

const adjuster = {
    adjustInto(temporal) {
        return temporal.plusWeeks(2)
    }
}

const updated = date.with(adjuster)

console.log(date.toString())
console.log(updated.toString())

Вывод:

2025-01-01
2025-01-15

Создание adjuster через объект

Наиболее распространённый способ — обычный объект с методом adjustInto.

Пример: переход к следующему понедельнику

const {
    LocalDate,
    DayOfWeek
} = require('@js-joda/core')

const nextMondayAdjuster = {
    adjustInto(temporal) {
        let current = temporal

        while (current.dayOfWeek() !== DayOfWeek.MONDAY) {
            current = current.plusDays(1)
        }

        return current
    }
}

const date = LocalDate.parse('2025-05-14')

const result = date.with(nextMondayAdjuster)

console.log(result.toString())

Результат:

2025-05-19

Использование фабрики TemporalAdjusters

Библиотека содержит готовые adjuster’ы.

Модуль:

const { TemporalAdjusters } = require('@js-joda/core')

Пример:

const result = date.with(
    TemporalAdjusters.firstDayOfMonth()
)

Наиболее полезные встроенные adjuster’ы:

Adjuster Назначение
firstDayOfMonth() Первый день месяца
lastDayOfMonth() Последний день месяца
firstDayOfYear() Первый день года
lastDayOfYear() Последний день года
next() Следующий указанный день недели
nextOrSame() Следующий или текущий день недели
previous() Предыдущий день недели
previousOrSame() Предыдущий или текущий день недели

Создание функционального adjuster

Adjuster можно создавать через функцию.

Пример: начало рабочего дня

const workdayStartAdjuster = {
    adjustInto(temporal) {
        return temporal
            .withHour(9)
            .withMinute(0)
            .withSecond(0)
            .withNano(0)
    }
}

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

const {
    LocalDateTime
} = require('@js-joda/core')

const dateTime = LocalDateTime.parse(
    '2025-03-10T15:45:22'
)

const updated = dateTime.with(workdayStartAdjuster)

console.log(updated.toString())

Результат:

2025-03-10T09:00

Adjuster для рабочих дней

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

Пример: следующий рабочий день

const {
    DayOfWeek
} = require('@js-joda/core')

const nextBusinessDayAdjuster = {
    adjustInto(temporal) {
        let current = temporal.plusDays(1)

        while (
            current.dayOfWeek() === DayOfWeek.SATURDAY ||
            current.dayOfWeek() === DayOfWeek.SUNDAY
        ) {
            current = current.plusDays(1)
        }

        return current
    }
}

Проверка:

const friday = LocalDate.parse('2025-05-16')

const result = friday.with(nextBusinessDayAdjuster)

console.log(result.toString())

Результат:

2025-05-19

Adjuster с поддержкой праздничных дней

Реальные бизнес-системы обычно учитывают праздники.

Пример

const holidays = [
    '2025-01-01',
    '2025-01-07',
    '2025-05-09'
]

const holidayAwareAdjuster = {
    adjustInto(temporal) {
        let current = temporal.plusDays(1)

        while (true) {
            const isWeekend =
                current.dayOfWeek() === DayOfWeek.SATURDAY ||
                current.dayOfWeek() === DayOfWeek.SUNDAY

            const isHoliday =
                holidays.includes(current.toString())

            if (!isWeekend && !isHoliday) {
                return current
            }

            current = current.plusDays(1)
        }
    }
}

Универсальный factory-adjuster

Полезный подход — создание фабрики adjuster’ов.

Пример: смещение на N рабочих дней

function businessDaysLater(days) {
    return {
        adjustInto(temporal) {
            let current = temporal
            let added = 0

            while (added < days) {
                current = current.plusDays(1)

                const isWeekend =
                    current.dayOfWeek() === DayOfWeek.SATURDAY ||
                    current.dayOfWeek() === DayOfWeek.SUNDAY

                if (!isWeekend) {
                    added++
                }
            }

            return current
        }
    }
}

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

const result = date.with(
    businessDaysLater(5)
)

Комбинирование adjuster’ов

Adjuster’ы удобно объединять в цепочки.

Пример

const result = date
    .with(TemporalAdjusters.firstDayOfMonth())
    .with(nextBusinessDayAdjuster)

Сценарий:

  1. переход к первому дню месяца;
  2. затем поиск ближайшего рабочего дня.

Adjuster для конца квартала

Реализация

const endOfQuarterAdjuster = {
    adjustInto(temporal) {
        const month = temporal.monthValue()

        let targetMonth

        if (month <= 3) {
            targetMonth = 3
        } else if (month <= 6) {
            targetMonth = 6
        } else if (month <= 9) {
            targetMonth = 9
        } else {
            targetMonth = 12
        }

        return temporal
            .withMonth(targetMonth)
            .with(TemporalAdjusters.lastDayOfMonth())
    }
}

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

const date = LocalDate.parse('2025-05-10')

const result = date.with(endOfQuarterAdjuster)

console.log(result.toString())

Результат:

2025-06-30

Adjuster для начала квартала

const startOfQuarterAdjuster = {
    adjustInto(temporal) {
        const month = temporal.monthValue()

        let targetMonth

        if (month <= 3) {
            targetMonth = 1
        } else if (month <= 6) {
            targetMonth = 4
        } else if (month <= 9) {
            targetMonth = 7
        } else {
            targetMonth = 10
        }

        return temporal
            .withMonth(targetMonth)
            .withDayOfMonth(1)
    }
}

Работа с LocalDateTime

Adjuster может изменять не только даты, но и время.

Пример: округление до начала часа

const roundToHourAdjuster = {
    adjustInto(temporal) {
        return temporal
            .withMinute(0)
            .withSecond(0)
            .withNano(0)
    }
}

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

const dateTime = LocalDateTime.parse(
    '2025-04-11T10:37:48'
)

const result = dateTime.with(roundToHourAdjuster)

console.log(result.toString())

Результат:

2025-04-11T10:00

Параметризованные adjuster’ы

Adjuster может принимать настройки.

Пример: установка произвольного времени

function timeAdjuster(hour, minute) {
    return {
        adjustInto(temporal) {
            return temporal
                .withHour(hour)
                .withMinute(minute)
                .withSecond(0)
                .withNano(0)
        }
    }
}

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

const result = dateTime.with(
    timeAdjuster(14, 30)
)

Adjuster для ближайшего рабочего понедельника

const nearestBusinessMonday = {
    adjustInto(temporal) {
        let current = temporal

        while (current.dayOfWeek() !== DayOfWeek.MONDAY) {
            current = current.plusDays(1)
        }

        while (
            current.dayOfWeek() === DayOfWeek.SATURDAY ||
            current.dayOfWeek() === DayOfWeek.SUNDAY
        ) {
            current = current.plusDays(1)
        }

        return current
    }
}

Проверка типов temporal-объектов

Иногда adjuster рассчитан только на конкретный тип.

Пример

const strictDateAdjuster = {
    adjustInto(temporal) {
        if (!temporal.plusDays) {
            throw new Error(
                'Adjuster supports only date objects'
            )
        }

        return temporal.plusDays(1)
    }
}

Использование TemporalQueries вместе с adjuster

Adjuster часто комбинируют с query-механизмом.

Пример

const date = LocalDate.now()

const adjusted = date.with(
    TemporalAdjusters.lastDayOfMonth()
)

const dayOfWeek = adjusted.dayOfWeek()

console.log(dayOfWeek.toString())

Создание composable-adjuster

Подход composable позволяет строить сложную бизнес-логику.

Пример

function composeAdjusters(...adjusters) {
    return {
        adjustInto(temporal) {
            return adjusters.reduce(
                (current, adjuster) => current.with(adjuster),
                temporal
            )
        }
    }
}

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

const complexAdjuster = composeAdjusters(
    TemporalAdjusters.firstDayOfMonth(),
    nextBusinessDayAdjuster,
    timeAdjuster(9, 0)
)

Оптимизация производительности

При разработке сложных adjuster’ов важно учитывать количество создаваемых объектов.

Неэффективный вариант:

while (condition) {
    current = current.plusDays(1)
}

При больших диапазонах это создаёт множество промежуточных immutable-объектов.

Более эффективный подход:

  • минимизировать циклы;
  • избегать лишних преобразований;
  • использовать встроенные adjuster’ы;
  • кэшировать праздничные даты;
  • применять Set вместо массива для поиска.

Пример оптимизации

const holidays = new Se t([
    '2025-01-01',
    '2025-05-09'
])

Проверка:

holidays.has(date.toString())

Типичные ошибки

Изменение исходного объекта

Неправильно:

temporal.plusDays(1)
return temporal

Правильно:

return temporal.plusDays(1)

Возврат undefined

Неправильно:

adjustInto(temporal) {
    temporal.plusDays(1)
}

Метод обязан вернуть новый temporal-объект.


Бесконечные циклы

Ошибка:

while (true) {

}

Любой цикл в adjuster должен гарантированно завершаться.


Игнорирование immutable-подхода

Неправильно:

current.plusDays(1)

Правильно:

current = current.plusDays(1)

Архитектурные рекомендации

Выделение бизнес-логики

Хорошая практика — хранить adjuster’ы отдельно:

date-adjusters/
    business-day.js
    quarter.js
    holidays.js

Создание набора reusable-adjuster’ов

Крупные приложения обычно имеют библиотеку:

  • nextBusinessDay
  • endOfQuarter
  • payrollDate
  • nextTradingDay
  • settlementDate
  • startOfFiscalYear

Изоляция календарной логики

Вместо разбросанных вычислений:

date.plusDays(1)

лучше использовать:

date.with(nextBusinessDayAdjuster)

Так код становится:

  • читаемее;
  • предсказуемее;
  • тестируемее;
  • централизованнее.

Тестирование custom adjuster’ов

Проверка рабочих дней

const friday = LocalDate.parse('2025-05-16')

const result = friday.with(nextBusinessDayAdjuster)

console.assert(
    result.toString() === '2025-05-19'
)

Проверка праздничных дней

const beforeHoliday = LocalDate.parse('2025-05-08')

const result = beforeHoliday.with(
    holidayAwareAdjuster
)

console.assert(
    result.toString() === '2025-05-12'
)

Практический пример: дата выплаты зарплаты

Условие:

  • зарплата выплачивается 10 числа;
  • если дата попадает на выходной — перенос назад на рабочий день.

Реализация

const payrollAdjuster = {
    adjustInto(temporal) {
        let current = temporal.withDayOfMonth(10)

        while (
            current.dayOfWeek() === DayOfWeek.SATURDAY ||
            current.dayOfWeek() === DayOfWeek.SUNDAY
        ) {
            current = current.minusDays(1)
        }

        return current
    }
}

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

const result = LocalDate
    .parse('2025-08-01')
    .with(payrollAdjuster)

console.log(result.toString())

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

const tradingDayAdjuster = {
    adjustInto(temporal) {
        let current = temporal

        while (
            current.dayOfWeek() === DayOfWeek.SATURDAY ||
            current.dayOfWeek() === DayOfWeek.SUNDAY
        ) {
            current = current.plusDays(1)
        }

        return current
    }
}

Практический пример: начало финансового года

const fiscalYearAdjuster = {
    adjustInto(temporal) {
        return temporal
            .withMonth(4)
            .withDayOfMonth(1)
    }
}

Практический пример: конец недели

const endOfWeekAdjuster = {
    adjustInto(temporal) {
        return temporal.with(
            TemporalAdjusters.nextOrSame(
                DayOfWeek.SUNDAY
            )
        )
    }
}

Расширяемая система adjuster’ов

В крупных проектах удобно создавать registry.

Пример

const adjusters = {
    nextBusinessDay: nextBusinessDayAdjuster,
    payroll: payrollAdjuster,
    quarterEnd: endOfQuarterAdjuster
}

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

date.with(adjusters.quarterEnd)

Интеграция с доменной моделью

Adjuster’ы особенно полезны в:

  • ERP-системах;
  • банковских приложениях;
  • CRM;
  • бухгалтерии;
  • торговых платформах;
  • логистике;
  • системах бронирования.

Пример доменного вызова:

invoiceDate.with(paymentDueAdjuster)

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