Обёртки для частых операций

В работе с временными типами в 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

При работе с временными значениями часто встречаются 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 в прикладном коде.