OffsetDateTime и его отличия от ZonedDateTime

В библиотеке js-joda работа с датой и временем построена вокруг строгого разделения понятий: локальное время, смещённое время (offset) и зональное время (time zone). Это разделение является ключевым для понимания различий между OffsetDateTime и ZonedDateTime.


OffsetDateTime: фиксированное смещение без правил часовых поясов

OffsetDateTime представляет собой дату и время, привязанные к конкретному смещению от UTC, например +03:00 или -05:00, но без учёта правил часовых поясов и переходов на летнее/зимнее время.

Основные характеристики

  • содержит LocalDateTime (дата и время без зоны)
  • содержит ZoneOffset (фиксированное смещение)
  • не использует ZoneRules
  • не учитывает DST (daylight saving time)
  • является «плоским» представлением времени относительно UTC

Таким образом, OffsetDateTime — это момент времени, выраженный через фиксированное смещение, а не через географическую зону.


ZonedDateTime: полноценная работа с часовыми поясами

ZonedDateTime включает не только дату, время и смещение, но и идентификатор временной зоны (например, Europe/Moscow, America/New_York). Эта зона определяет правила трансформации времени через исторические и текущие правила ZoneRules.

Основные характеристики

  • содержит LocalDateTime
  • содержит ZoneId
  • вычисляет ZoneOffset динамически
  • учитывает переходы DST
  • зависит от правил временной зоны, включая исторические изменения

Ключевое различие концепций

1. Смещение vs временная зона

OffsetDateTime оперирует фиксированным значением:

2025-01-10T10:00+03:00

ZonedDateTime оперирует зоной:

2025-01-10T10:00+03:00[Europe/Moscow]

В первом случае важен только offset. Во втором — зона, которая может изменять offset в зависимости от даты.


2. Поведение при переходе на летнее время

OffsetDateTime

Смещение фиксировано и не изменяется:

OffsetDateTime.of(2025, 7, 1, 12, 0, 0, 0, ZoneOffset.of("+03:00"));

Даже если в реальности регион переходит на другое смещение, объект этого не отражает.

ZonedDateTime

Использует правила зоны:

ZonedDateTime.of(2025, 7, 1, 12, 0, 0, 0, ZoneId.of("Europe/Berlin"));

Значение смещения будет вычислено автоматически и может отличаться от зимнего периода.


3. Семантика «момента времени»

Оба типа могут представлять момент времени, но делают это по-разному:

  • OffsetDateTime — фиксированное представление относительно UTC
  • ZonedDateTime — контекстное представление через правила зоны

Поведение при преобразовании в Instant

Оба типа могут быть преобразованы в Instant, но логика различается только в источнике смещения.

const instant1 = offsetDateTime.toInstant();
const instant2 = zonedDateTime.toInstant();

Instant всегда нормализует время к UTC и является точкой истины.


Различия в API js-joda

OffsetDateTime

Основные операции:

  • withOffsetSameInstant
  • withOffsetSameLocal
  • toInstant
  • plusDays, minusHours
  • toLocalDateTime
  • getOffset

Пример:

import { OffsetDateTime, ZoneOffset } from '@js-joda/core';

const odt = OffsetDateTime.parse("2025-05-01T10:15:30+03:00");

const shifted = odt.withOffsetSameInstant(ZoneOffset.of("+05:00"));

Важно: смена offset может изменить локальное отображение времени, но не момент времени.


ZonedDateTime

Основные операции:

  • withZoneSameInstant
  • withZoneSameLocal
  • getZone
  • getOffset
  • toInstant
  • plusDays, withLaterOffsetAtOverlap

Пример:

import { ZonedDateTime, ZoneId } from '@js-joda/core';

const zdt = ZonedDateTime.parse("2025-05-01T10:15:30+03:00[Europe/Moscow]");

const shifted = zdt.withZoneSameInstant(ZoneId.of("America/New_York"));

Здесь пересчёт учитывает правила зоны и DST.


Семантика операций преобразования

withOffsetSameInstant (OffsetDateTime)

Сохраняет момент времени, меняет отображение:

UTC момент фиксирован → пересчёт offset

withZoneSameInstant (ZonedDateTime)

Аналогично, но через зону:

Instant фиксирован → пересчёт по ZoneRules

Потеря информации при преобразованиях

ZonedDateTime → OffsetDateTime

При преобразовании теряется информация о зоне:

const odt = zdt.toOffsetDateTime();

Остаётся только фиксированное смещение на конкретный момент.

OffsetDateTime → ZonedDateTime

Требуется указание зоны:

const zdt = odt.toZonedDateTime(ZoneId.of("Europe/Moscow"));

При этом применяется текущее правило зоны к уже зафиксированному моменту.


Поведение при неоднозначных локальных временах

В ZonedDateTime возможны неоднозначности:

  • переходы DST (например, 02:30 может существовать дважды)
  • «пропавшие» часы

OffsetDateTime не имеет таких проблем, так как не опирается на правила зоны.


Представление и сериализация

OffsetDateTime

Строковый формат:

2025-05-01T10:15:30+03:00

Простая структура без зоны.

ZonedDateTime

Строковый формат:

2025-05-01T10:15:30+03:00[Europe/Moscow]

Содержит дополнительный контекст зоны, что важно для восстановления логики.


Влияние ZoneRules

Ключевая особенность ZonedDateTime — использование ZoneRules.

Они определяют:

  • когда начинается DST
  • когда заканчивается DST
  • исторические изменения зоны
  • смещение для конкретной даты

OffsetDateTime полностью игнорирует эти правила.


Практическое различие моделей

OffsetDateTime как модель хранения API

Используется, когда:

  • требуется фиксированное смещение
  • данные приходят от внешнего API с offset
  • нет необходимости учитывать географию

Пример сценария:

  • логирование событий с UTC offset
  • обмен данными между сервисами

ZonedDateTime как модель бизнес-времени

Используется, когда:

  • важно знать географическую зону
  • критичны DST и исторические изменения
  • расписания зависят от региона

Примеры:

  • календарные системы
  • бронирования
  • расписания транспорта
  • финансовые операции по локальному времени

Сравнение поведения при одинаковом времени

Рассмотрим дату:

2025-06-01T12:00

OffsetDateTime

OffsetDateTime.of(2025, 6, 1, 12, 0, 0, 0, ZoneOffset.of("+03:00"));

Всегда даёт фиксированный результат +03:00.

ZonedDateTime

ZonedDateTime.of(2025, 6, 1, 12, 0, 0, 0, ZoneId.of("Europe/Moscow"));

Может вернуть другое смещение, если правила зоны изменятся в будущем или исторически отличаются.


Концептуальная граница

Разделение типов можно выразить так:

  • OffsetDateTime — «время относительно UTC без географии»
  • ZonedDateTime — «время в конкретной географической зоне с правилами»

Эта граница определяет поведение всей системы работы со временем в js-joda и влияет на выбор типа при проектировании моделей данных.