Календарная арифметика в JavaScript сталкивается с фундаментальной
проблемой: локальное время в разных регионах мира не является
непрерывной математической шкалой. Изменения часовых поясов, а особенно
переходы на летнее и зимнее время, создают разрывы и неоднозначности,
которые нельзя корректно обработать с помощью стандартного объекта
Date.
В момент перехода на летнее время часть локального времени «исчезает» (например, время с 02:00 до 03:00 может отсутствовать), а при возврате на зимнее — дублируется. Это приводит к двум типичным ситуациям:
Библиотека js-joda проектируется как строгая реализация ISO-8601 модели времени, где базовые сущности разделены на:
LocalDate, LocalTime,
LocalDateTime)Instant)ZoneId)ZonedDateTime)Именно комбинация ZonedDateTime + ZoneId обеспечивает
корректную работу с переходами времени.
В js-joda ключевым элементом учёта переходов является привязка локального времени к зоне:
import { ZonedDateTime, ZoneId } from '@js-joda/core';
Временная зона определяет правила смещения UTC, включая исторические изменения и DST-переходы.
const zone = ZoneId.of('Europe/Berlin');
Любая операция создания зонального времени проходит через проверку правил зоны:
const zdt = ZonedDateTime.of(2026, 3, 29, 2, 30, 0, 0, zone);
Если в указанной зоне существует переход на летнее время, то 02:30 может оказаться невалидным локальным временем.
Разрыв возникает, когда локальные часы «перепрыгивают» вперёд.
Пример: переход 02:00 → 03:00
В js-joda при создании несуществующего времени применяется политика нормализации:
ZoneOffsetTransition (внутренне)Пример поведения:
const zone = ZoneId.of('Europe/Berlin');
const zdt = ZonedDateTime.of(2026, 3, 29, 2, 30, 0, 0, zone);
Если 02:30 не существует, библиотека автоматически переводит время в ближайшее корректное значение (обычно 03:30 или 03:00 в зависимости от правил зоны).
Ключевой принцип:
js-joda никогда не хранит несуществующее локальное время в ZonedDateTime без нормализации
Перекрытие возникает при возврате на зимнее время, когда один и тот же локальный час существует дважды.
Пример:
Обе метки времени валидны, но соответствуют разным моментам UTC.
js-joda решает неоднозначность через выбор смещения:
const zdt = ZonedDateTime.of(2026, 10, 25, 2, 30, 0, 0, ZoneId.of('Europe/Berlin'));
По умолчанию выбирается первое подходящее смещение (ранний offset), если не задано иное поведение через API работы с переходами.
Для устранения неоднозначности используется преобразование в
Instant:
const instant = zdt.toInstant();
Instant всегда однозначен и не зависит от локальных
правил зоны.
Критическая ошибка при работе с DST — хранение времени в
LocalDateTime без зоны. Такое значение теряет контекст
переходов.
const ldt = LocalDateTime.of(2026, 3, 29, 2, 30);
const zdt = ldt.atZone(ZoneId.of('Europe/Berlin'));
При вызове atZone происходит анализ:
В случае разрыва результат будет автоматически нормализован.
Самый надёжный способ работы с временем в условиях DST — опора на UTC-момент:
const instant = Instant.now();
const zdt = instant.atZone(ZoneId.of('Europe/Berlin'));
Преимущества:
При обратном преобразовании:
const back = zdt.toInstant();
получается тот же исходный момент, независимо от DST.
Одна из наиболее сложных проблем — арифметика времени через переходы.
const zone = ZoneId.of('Europe/Berlin');
const start = ZonedDateTime.of(2026, 3, 28, 23, 30, 0, 0, zone);
const end = start.plusHours(3);
В момент перехода длина «часа» может быть не равна 60 минутам.
js-joda корректно учитывает:
Поэтому plusHours не просто добавляет 3600 секунд, а
пересчитывает через Instant.
OffsetDateTime хранит фиксированное смещение:
OffsetDateTime.of(2026, 3, 29, 2, 30, 0, 0, ZoneOffset.of('+01:00'));
Недостаток:
ZonedDateTime:
ZoneIdВ системах с долгоживущими событиями (расписания, календарные
системы) предпочтителен именно ZonedDateTime.
Временные зоны не статичны. Они изменяются:
js-joda использует tz-данные (аналог IANA Time Zone Database), благодаря чему:
ZoneId.of('Europe/Moscow')
может давать разные правила в зависимости от года события.
Пример:
При создании ZonedDateTime учитывается не только дата,
но и исторический контекст зоны.
Каждое создание ZonedDateTime проходит через процесс
нормализации:
InstantЭтот процесс гарантирует, что объект всегда представляет реальный момент времени.
Устойчивый подход в js-joda строится на разделении уровней:
InstantZonedDateTimeLocalDateTimeZoneIdТипичный поток:
const zone = ZoneId.of('Europe/Berlin');
const userInput = LocalDateTime.of(2026, 10, 25, 2, 30);
const zdt = userInput.atZone(zone);
const instant = zdt.toInstant();
Такой подход устраняет:
При выводе времени важно учитывать, что одно и то же значение
Instant может отображаться по-разному в зависимости от
зоны:
const instant = Instant.parse('2026-10-25T00:30:00Z');
const berlin = instant.atZone(ZoneId.of('Europe/Berlin'));
const london = instant.atZone(ZoneId.of('Europe/London'));
Разные зоны дают разные локальные времена, но одинаковый момент времени.
Наиболее распространённые проблемы:
Date без учёта зоныplusHoursLocalDateTime как абсолютного
времениjs-joda устраняет эти ошибки за счёт строгой типизации временных
сущностей и обязательного учёта ZoneId.
Каждая зона содержит набор правил:
Эти правила используются при каждом вычислении
ZonedDateTime, обеспечивая корректную работу даже для дат
десятилетней давности или будущих прогнозируемых значений.