Работа с началом и концом периодов

Базовые принципы определения границ периода

В библиотеке js-joda работа с временными границами строится вокруг неизменяемых типов данных и явного преобразования дат и времени. Любая операция, связанная с началом или концом периода, не изменяет исходный объект, а возвращает новый экземпляр.

Граница периода определяется через:

  • календарные компоненты (LocalDate, YearMonth, Year)
  • временные компоненты (LocalTime, LocalDateTime, ZonedDateTime)
  • преобразования через методы и TemporalAdjusters

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


Начало и конец дня

Начало дня

Для получения начала дня используется метод:

const start = localDate.atStartOfDay();

Результатом становится LocalDateTime со значением 00:00.

Пример:

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

const date = LocalDate.of(2026, 5, 25);
const startOfDay = date.atStartOfDay();

startOfDay будет:

2026-05-25T00:00

При необходимости привязки к часовому поясу применяется преобразование:

const zoned = startOfDay.atZone(ZoneId.of('Europe/Moscow'));

Конец дня

В js-joda отсутствует прямой метод atEndOfDay, поскольку концепция конца дня требует явного определения точности.

На практике используется переход к следующему дню с вычитанием минимального шага времени:

import { LocalTime } from '@js-joda/core';

const endOfDay = localDate
  .plusDays(1)
  .atStartOfDay()
  .minusNanos(1);

Итоговое значение:

23:59:59.999999999

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


Работа с началом и концом месяца

Начало месяца

Для перехода к первому дню месяца используется TemporalAdjusters:

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

const firstDay = localDate.with(TemporalAdjusters.firstDayOfMonth());

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


Конец месяца

Для определения последнего дня месяца применяется аналогичный механизм:

const lastDay = localDate.with(TemporalAdjusters.lastDayOfMonth());

При необходимости получения конца дня месяца:

const endOfMonth = localDate
  .with(TemporalAdjusters.lastDayOfMonth())
  .atTime(23, 59, 59, 999999999);

Начало и конец года

Начало года

const firstDayOfYear = localDate.with(TemporalAdjusters.firstDayOfYear());

Результат:

YYYY-01-01

Для получения начала временной метки:

const startOfYear = firstDayOfYear.atStartOfDay();

Конец года

const lastDayOfYear = localDate.with(TemporalAdjusters.lastDayOfYear());

Преобразование в конец дня:

const endOfYear = lastDayOfYear
  .atTime(23, 59, 59, 999999999);

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

В js-joda неделя не имеет фиксированной глобальной структуры, поэтому используется локализованный подход через DayOfWeek.

Начало недели

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

const startOfWeek = localDate.with(
  TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)
);

Результат всегда приводит дату к ближайшему понедельнику (или текущему, если он уже понедельник).


Конец недели

const endOfWeek = localDate.with(
  TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY)
);

Для получения конечной временной метки:

const endOfWeekDateTime = endOfWeek.atTime(23, 59, 59, 999999999);

Границы через Instant и ZonedDateTime

При работе с абсолютным временем используются Instant и ZonedDateTime.

Начало дня в часовом поясе

const zonedStart = ZonedDateTime.of(
  localDate,
  LocalTime.MIN,
  ZoneId.of('UTC')
);

Эквивалент:

YYYY-MM-DDT00:00Z

Конец дня с учётом зоны

const zonedEnd = ZonedDateTime.of(
  localDate,
  LocalTime.MAX,
  ZoneId.of('UTC')
);

LocalTime.MAX представляет максимально возможное время в сутках:

23:59:59.999999999

Использование LocalTime.MIN и LocalTime.MAX

В js-joda присутствуют специальные константы:

  • LocalTime.MIN → начало суток
  • LocalTime.MAX → конец суток

Применение:

const start = LocalDateTime.of(localDate, LocalTime.MIN);
const end = LocalDateTime.of(localDate, LocalTime.MAX);

Особенность LocalTime.MAX заключается в включении максимально возможной точности временной шкалы.


Интервалы на основе границ периода

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

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

const start = localDate.atStartOfDay();
const end = localDate
  .plusDays(1)
  .atStartOfDay();

const interval = { start, end };

Такой интервал является полуоткрытым:

[start, end)

Это стандартная модель для временных диапазонов.


Интервал месяца

const startOfMonth = localDate
  .with(TemporalAdjusters.firstDayOfMonth())
  .atStartOfDay();

const endOfMonth = localDate
  .with(TemporalAdjusters.lastDayOfMonth())
  .plusDays(1)
  .atStartOfDay();

Использование полуоткрытого интервала упрощает сравнение и исключает ошибки включения границ.


Нормализация временных границ

При вычислении границ важно учитывать нормализацию типов:

  • LocalDate → календарная дата без времени
  • LocalDateTime → дата и время без зоны
  • ZonedDateTime → абсолютное время с зоной

Переход между ними всегда явный:

localDate.atStartOfDay()              // LocalDate → LocalDateTime
localDateTime.atZone(zoneId)          // LocalDateTime → ZonedDateTime
zonedDateTime.toInstant()             // ZonedDateTime → Instant

Работа с переходами времени (DST)

При использовании ZonedDateTime границы периода могут изменяться из-за переходов летнего и зимнего времени.

Пример:

const start = ZonedDateTime.of(
  localDate,
  LocalTime.MIN,
  ZoneId.of('Europe/Berlin')
);

В дни перехода времени:

  • сутки могут содержать 23 или 25 часов
  • LocalTime.MAX не всегда соответствует реальному последнему моменту в календарном смысле

Поэтому для вычислений интервалов предпочтение отдается Instant.


Сравнение границ периодов

Для проверки попадания времени в диапазон используются сравнения Instant:

const isInside =
  instant.compareTo(start.toInstant()) >= 0 &&
  instant.compareTo(end.toInstant()) < 0;

Полуоткрытый интервал обеспечивает однозначность:

  • нижняя граница включена
  • верхняя граница исключена

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

Сдвиг периода

const nextDayStart = localDate
  .plusDays(1)
  .atStartOfDay();
const previousMonthStart = localDate
  .minusMonths(1)
  .with(TemporalAdjusters.firstDayOfMonth())
  .atStartOfDay();

Усечение времени до границы

const truncated = localDateTime
  .withHour(0)
  .withMinute(0)
  .withSecond(0)
  .withNano(0);

Аналог начала дня без преобразования типа.


Практическая модель хранения границ

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

{
  start: Instant,
  end: Instant
}

Либо:

{
  start: ZonedDateTime,
  end: ZonedDateTime
}

Выбор зависит от необходимости учитывать часовые пояса.


Особенности точности

js-joda оперирует наносекундной точностью:

  • 1 секунда = 1_000_000_000 наносекунд
  • границы конца периода часто выражаются как 999_999_999 наносекунд

Пример:

const preciseEnd = localDate.atTime(23, 59, 59, 999999999);

Обобщённая модель вычисления границ

Алгоритмически вычисление начала и конца периода сводится к:

  1. Определению базовой календарной единицы
  2. Переходу к начальной точке через TemporalAdjusters или MIN
  3. Получению следующей единицы
  4. Вычитанию минимального временного шага или использованию полуоткрытого интервала

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