Временные зоны и DST

Временные зоны в js-joda реализованы через строгую модель разделения абсолютного времени (Instant) и локального календарного времени (LocalDateTime). Такой подход исключает неоднозначности, характерные для встроенного Date, и позволяет корректно работать с переходами на летнее и зимнее время (DST — Daylight Saving Time).

Ключевой принцип: время хранится как момент на временной оси, а отображается через правила конкретной временной зоны


Модель временных зон

В js-joda временная зона описывается через идентификатор ZoneId. Он соответствует стандарту IANA Time Zone Database:

  • Europe/Berlin
  • Asia/Almaty
  • America/New_York

ZoneId не содержит смещения сам по себе, а представляет набор правил, которые определяют:

  • смещение относительно UTC
  • изменения смещения во времени (DST)
  • исторические корректировки
import { ZoneId } from '@js-joda/core';

const zone = ZoneId.of('Europe/Berlin');

Абсолютное время и локальное представление

Разделение между Instant и ZonedDateTime является фундаментальным:

Instant

Представляет момент на временной шкале в UTC:

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

const instant = Instant.now();

ZonedDateTime

Комбинирует:

  • LocalDateTime
  • ZoneId
  • правила временной зоны
import { ZonedDateTime, ZoneId } from '@js-joda/core';

const zdt = ZonedDateTime.now(ZoneId.of('Asia/Almaty'));

Применение временной зоны к моменту времени

Преобразование Instant в локальное время зоны:

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

const instant = Instant.parse('2026-01-01T12:00:00Z');

const zoned = ZonedDateTime.ofInstant(
  instant,
  ZoneId.of('Europe/Berlin')
);

Результат зависит от текущего смещения зоны, включая DST, если он активен.


Смещения (Offset) и их роль

Каждая временная зона в конкретный момент времени предоставляет ZoneOffset:

  • фиксированное смещение от UTC
  • изменяется при переходе DST
const offset = zoned.offset();
console.log(offset.toString()); // например +01:00 или +02:00

DST (летнее и зимнее время)

DST представляет собой временное изменение стандартного смещения зоны.

Основные эффекты DST:

  • весной время «перескакивает вперёд»
  • осенью возникает повтор временного интервала
  • локальное время становится неоднозначным или несуществующим

Неоднозначные и пропущенные локальные времена

Пропущенное время (Gap)

При переходе вперёд часть локального времени не существует:

Пример: 02:00 → 03:00 (час «пропадает»)

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

const ldt = LocalDateTime.of(2026, 3, 29, 2, 30);
const zone = ZoneId.of('Europe/Berlin');

const zdt = ZonedDateTime.of(ldt, zone);

В таких случаях js-joda применяет правила смещения зоны и корректирует время.


Неоднозначное время (Overlap)

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

Пример: 02:30 может существовать дважды с разными смещениями.

const ldt = LocalDateTime.of(2026, 10, 25, 2, 30);
const zone = ZoneId.of('Europe/Berlin');

const zdt = ZonedDateTime.of(ldt, zone);

Стратегии разрешения конфликтов

При DST-коллизиях применяются стратегии:

  • смещение вперёд (forward adjustment)
  • использование раннего/позднего смещения
  • перевод в ближайшее валидное время

В js-joda поведение определяется внутренними правилами ZoneRules.


ZoneRules: ядро DST-логики

Каждая зона содержит набор правил:

  • исторические изменения смещений
  • расписание переходов DST
  • вычисление offset для любого момента
const rules = ZoneId.of('Europe/Berlin').rules();

ZoneRules позволяют:

  • проверять, является ли момент в DST
  • получать смещение для конкретной даты
  • определять переходы

Проверка DST-режима

const isDst = zoned.offset().totalSeconds() !== ZoneOffset.ofHours(1).totalSeconds();

Более корректный способ:

const rules = ZoneId.of('Europe/Berlin').rules();
const isDst = rules.isDaylightSavings(zoned.toInstant());

Перевод между зонами

Перенос времени между зонами выполняется через withZoneSameInstant:

const berlin = ZonedDateTime.now(ZoneId.of('Europe/Berlin'));

const tokyo = berlin.withZoneSameInstant(
  ZoneId.of('Asia/Tokyo')
);

Смысл операции:

  • сохраняется момент времени
  • меняется локальное представление

Сравнение withZoneSameInstant и withZoneSameLocal

withZoneSameInstant

Сохраняет абсолютный момент времени

withZoneSameLocal

Сохраняет локальное время, изменяя момент

const shifted = berlin.withZoneSameLocal(ZoneId.of('Asia/Tokyo'));

Это приводит к изменению Instant.


Работа с историческими временными зонами

IANA база включает изменения правил:

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

Пример: одна и та же зона может иметь разные offset в разные эпохи.

const past = ZonedDateTime.of(
  1980,
  6,
  1,
  12,
  0,
  0,
  0,
  ZoneId.of('Europe/Berlin')
);

Пограничные случаи DST

Переход весной

  • локальное время сжимается
  • некоторые значения не существуют

Переход осенью

  • локальное время дублируется
  • требуется уточнение offset

LocalDateTime и отсутствие зоны

LocalDateTime не содержит информации о временной зоне:

const ldt = LocalDateTime.of(2026, 5, 25, 10, 0);

Он не может однозначно быть преобразован в Instant без ZoneId.


Конвертация LocalDateTime в ZonedDateTime

const zdt = ldt.atZone(ZoneId.of('Asia/Almaty'));

При этом учитываются DST и правила зоны.


Конвертация ZonedDateTime в Instant

const instant = zdt.toInstant();

Результат всегда однозначен и не зависит от зоны.


Практическая модель вычислений времени

Внутренние операции js-joda следуют цепочке:

  1. LocalDateTime
  2. ZoneId
  3. ZoneRules
  4. ZoneOffset
  5. Instant

Эта модель гарантирует отсутствие неоднозначностей при арифметике времени.


Сдвиги времени при арифметике

При добавлении часов учитываются правила DST:

const shifted = zdt.plusHours(5);

Если операция пересекает DST-переход:

  • смещение может измениться
  • локальная длительность часа может отличаться от 60 минут

Сравнение временных значений в разных зонах

Сравнение всегда происходит через Instant:

const a = ZonedDateTime.now(ZoneId.of('Europe/Berlin'));
const b = ZonedDateTime.now(ZoneId.of('Asia/Tokyo'));

const result = a.toInstant().isBefore(b.toInstant());

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

Нормализация применяется при создании ZonedDateTime из LocalDateTime:

  • проверка валидности локального времени
  • применение правил зоны
  • выбор корректного offset

Значение временных зон в архитектуре времени

Использование ZoneId позволяет:

  • избежать ошибок с DST
  • корректно хранить события в распределённых системах
  • обеспечивать стабильность временных меток
  • отделять хранение времени от отображения

DST как часть доменной модели времени

DST рассматривается не как форматирование, а как часть правил зоны:

  • не требует ручной обработки
  • полностью управляется ZoneRules
  • учитывается во всех операциях ZonedDateTime