В работе с временными типами в js-joda часто требуется
стандартизировать получение текущего момента, поскольку прямое
использование Instant.now(), LocalDate.now() и
ZonedDateTime.now() в разных частях кода приводит к
расхождениям в тестируемости и единообразии логики. Для этого вводятся
обёртки, фиксирующие точку получения времени и инкапсулирующие источник
часов.
Базовый подход строится вокруг Clock:
import { Clock, Instant, LocalDate, ZoneId } from 'js-joda';
const utcClock = Clock.systemUTC();
const nowInstant = () => Instant.now(utcClock);
const todayUtc = () => LocalDate.now(utcClock);
const nowMoscow = () => ZonedDateTime.now(Clock.system(ZoneId.of('Europe/Moscow')));
Такая форма устраняет прямые вызовы статических методов времени и позволяет централизованно управлять временной зоной.
Расширение этой идеи приводит к созданию слоя временных функций:
const Time = {
instant: () => Instant.now(utcClock),
date: () => LocalDate.now(utcClock),
zoned: (zone) => ZonedDateTime.now(Clock.system(zone)),
};
Подобная структура снижает зависимость бизнес-логики от глобального состояния времени.
Парсинг строк в объекты js-joda требует строгого
формата. В реальных системах входные данные часто нестабильны, что
приводит к необходимости безопасных обёрток.
import { LocalDate, ZonedDateTime, DateTimeParseException } from 'js-joda';
const safeParseLocalDate = (value) => {
if (!value || typeof value !== 'string') return null;
try {
return LocalDate.parse(value);
} catch (e) {
if (e instanceof DateTimeParseException) return null;
throw e;
}
};
Такая конструкция предотвращает распространение исключений на уровень выше и позволяет унифицировать обработку некорректных данных.
Для расширенного сценария добавляется поддержка нескольких форматов:
const parseWithFormats = (value, parsers) => {
for (const parser of parsers) {
try {
return parser(value);
} catch (_) {}
}
return null;
};
const safeParseZonedDateTime = (value) =>
parseWithFormats(value, [
(v) => ZonedDateTime.parse(v),
(v) => LocalDate.parse(v).atStartOfDay(ZoneId.systemDefault()),
]);
Форматирование в js-joda реализуется через
DateTimeFormatter, но прямое использование форматтера в
бизнес-коде приводит к дублированию шаблонов.
Создаётся слой форматирования:
import { DateTimeFormatter } from 'js-joda';
const FORMATTERS = {
ISO_DATE: DateTimeFormatter.ISO_LOCAL_DATE,
ISO_DATETIME: DateTimeFormatter.ISO_LOCAL_DATE_TIME,
READABLE_DATE: DateTimeFormatter.ofPattern('dd.MM.yyyy'),
READABLE_DATETIME: DateTimeFormatter.ofPattern('dd.MM.yyyy HH:mm'),
};
const formatLocalDate = (date, type = 'READABLE_DATE') =>
date.format(FORMATTERS[type]);
const formatZonedDateTime = (dateTime, type = 'READABLE_DATETIME') =>
dateTime.format(FORMATTERS[type]);
Такой подход позволяет централизовать форматирование и исключить несогласованность представления даты в разных частях системы.
Операции добавления и вычитания времени часто используются в доменной
логике, но прямое обращение к методам plusDays,
minusMonths приводит к разрастанию кода.
Создаётся функциональный слой:
const addDays = (date, n) => date.plusDays(n);
const addMonths = (date, n) => date.plusMonths(n);
const addYears = (date, n) => date.plusYears(n);
const subtractDays = (date, n) => date.minusDays(n);
Дальнейшая абстракция объединяет операции:
const DateMath = {
add: {
days: (d, n) => d.plusDays(n),
months: (d, n) => d.plusMonths(n),
years: (d, n) => d.plusYears(n),
},
sub: {
days: (d, n) => d.minusDays(n),
months: (d, n) => d.minusMonths(n),
},
};
Такая структура делает код более декларативным и упрощает композицию операций.
Одной из частых задач является получение границ дня, месяца или
недели. В js-joda это реализуется через
TemporalAdjusters, но использование напрямую делает код
перегруженным.
Инкапсуляция:
import { TemporalAdjusters, LocalDate } from 'js-joda';
const startOfDay = (date) => date.atStartOfDay();
const endOfDay = (date) =>
date.atTime(23, 59, 59, 999999999);
const startOfMonth = (date) =>
date.with(TemporalAdjusters.firstDayOfMonth());
const endOfMonth = (date) =>
date.with(TemporalAdjusters.lastDayOfMonth());
Для недельных операций:
const startOfWeek = (date) =>
date.with(TemporalAdjusters.previousOrSame(java.time.DayOfWeek.MONDAY));
const endOfWeek = (date) =>
date.with(TemporalAdjusters.nextOrSame(java.time.DayOfWeek.SUNDAY));
Подобные обёртки фиксируют единый стандарт расчёта периодов.
В js-joda присутствует строгая модель типов:
Instant, LocalDate,
LocalDateTime, ZonedDateTime. В реальных
приложениях требуется постоянное преобразование между ними.
Создаётся слой конвертации:
import { ZoneId } from 'js-joda';
const toInstant = (zoned) => zoned.toInstant();
const toLocalDate = (instant, zone = ZoneId.UTC) =>
instant.atZone(zone).toLocalDate();
const toZonedDateTime = (instant, zone = ZoneId.systemDefault()) =>
instant.atZone(zone);
Расширенная версия учитывает локальное время:
const toLocalDateTime = (instant, zone) =>
instant.atZone(zone).toLocalDateTime();
Инкапсуляция этих операций предотвращает ошибки, связанные с неправильной интерпретацией временных зон.
Duration и Period часто используются для
выражения разницы во времени. Их прямое использование может быть
громоздким, поэтому вводятся вспомогательные функции.
import { Duration, Period } from 'js-joda';
const duration = {
seconds: (n) => Duration.ofSeconds(n),
minutes: (n) => Duration.ofMinutes(n),
hours: (n) => Duration.ofHours(n),
};
const period = {
days: (n) => Period.ofDays(n),
months: (n) => Period.ofMonths(n),
years: (n) => Period.ofYears(n),
};
Операции над длительностями:
const addDuration = (instant, dur) => instant.plus(dur);
const subtractDuration = (instant, dur) => instant.minus(dur);
Такой подход унифицирует работу с временными интервалами и снижает вероятность смешивания типов.
В реальных системах временные значения поступают из различных
источников: API, базы данных, пользовательский ввод. Нормализация
позволяет привести их к единому типу Instant или
ZonedDateTime.
const normalizeToInstant = (value) => {
if (value instanceof Instant) return value;
if (typeof value === 'string') return Instant.parse(value);
if (value instanceof Date) return Instant.fromEpochMilliseconds(value.getTime());
return null;
};
Расширение для зональных значений:
const normalizeToZoned = (value, zone = ZoneId.systemDefault()) => {
const instant = normalizeToInstant(value);
return instant ? instant.atZone(zone) : null;
};
Такие обёртки формируют слой устойчивости к неоднородным данным.
Временные операции часто связаны с доменными правилами: проверка просрочки, активных периодов, дедлайнов.
const isExpired = (deadline, now) =>
deadline.isBefore(now);
const isActivePeriod = (start, end, moment) =>
(moment.isAfter(start) || moment.equals(start)) &&
(moment.isBefore(end) || moment.equals(end));
Более абстрактный слой:
const TimeRules = {
expired: (deadline, now) => deadline.isBefore(now),
active: (start, end, t) =>
!t.isBefore(start) && !t.isAfter(end),
};
Подобные конструкции изолируют бизнес-логику от деталей реализации временных типов.
При работе с временными значениями часто встречаются
null или undefined, что требует защитного
слоя.
const orNow = (value, clock) =>
value ?? Instant.now(clock);
Для дат:
const orToday = (value, clock) =>
value ?? LocalDate.now(clock);
Комбинированная обёртка:
const ensureInstant = (value, clock) =>
value ? normalizeToInstant(value) : Instant.now(clock);
Такие функции позволяют избежать распространения null-логики по коду.
Объединение всех уровней абстракции формирует единый модуль работы со временем, в котором каждая операция представлена в унифицированной форме.
const TimeKit = {
now: Time.instant,
today: Time.date,
parse: {
date: safeParseLocalDate,
zoned: safeParseZonedDateTime,
},
format: {
date: formatLocalDate,
zoned: formatZonedDateTime,
},
math: DateMath,
period,
duration,
convert: {
toInstant,
toZoned: toZonedDateTime,
toLocalDate,
},
rules: TimeRules,
};
Такая структура фиксирует единый контракт взаимодействия с временными
данными, устраняя необходимость обращения к низкоуровневым API
js-joda в прикладном коде.