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

В основе работы с временными зонами в js-joda лежит строгая модель разделения понятий:

  • Instant — абсолютный момент времени в UTC
  • ZoneId — идентификатор временной зоны (например, Europe/Moscow)
  • ZonedDateTime — локализованное время в конкретной зоне
  • LocalDateTime — дата и время без привязки к зоне

Ключевой принцип: любое преобразование между зонами происходит не через «изменение времени», а через пересчёт одного и того же момента времени (Instant) в другой локальный контекст.


Базовая точка: Instant как универсальный стандарт

Абсолютное время в js-joda представлено объектом Instant. Он не зависит от временных зон и всегда интерпретируется в UTC.

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

const instant = Instant.now();

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

  • локальное время → Instant
  • Instant → локальное время в другой зоне

Это устраняет ошибки, связанные с ручным пересчётом смещений.


ZonedDateTime как основная сущность работы с зонами

ZonedDateTime объединяет:

  • локальную дату и время
  • идентификатор зоны
  • правила перехода (DST — летнее/зимнее время)
import { ZonedDateTime, ZoneId } from '@js-joda/core';

const zone = ZoneId.of('Europe/Moscow');
const zdt = ZonedDateTime.now(zone);

Каждый объект ZonedDateTime всегда однозначно связан с конкретным моментом времени.


Создание времени в конкретной зоне

Часто требуется задать локальное время в определённой зоне:

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

const zone = ZoneId.of('Asia/Almaty');

const ldt = LocalDateTime.of(2026, 5, 24, 12, 0);
const zdt = ZonedDateTime.of(ldt, zone);

Важно: такой способ фиксирует локальное время, а затем интерпретирует его в рамках правил зоны.


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

Наиболее корректный способ конвертации — через абсолютный момент времени.

Принцип преобразования

  1. Берётся исходный ZonedDateTime
  2. Из него извлекается Instant
  3. Этот Instant интерпретируется в новой зоне
import { ZoneId, ZonedDateTime } from '@js-joda/core';

const zoneTokyo = ZoneId.of('Asia/Tokyo');
const zoneLondon = ZoneId.of('Europe/London');

const tokyoTime = ZonedDateTime.now(zoneTokyo);

const londonTime = tokyoTime.withZoneSameInstant(zoneLondon);

Ключевой метод: withZoneSameInstant

Метод сохраняет момент времени, изменяя только представление:

  • время в Токио → время в Лондоне
  • не меняется абсолютный момент
  • меняется только локальная интерпретация

Различие between SameInstant и SameLocal

withZoneSameInstant

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

const converted = zdt.withZoneSameInstant(ZoneId.of('UTC'));

Используется при:

  • логировании событий
  • синхронизации данных
  • API-взаимодействии

withZoneSameLocal

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

const shifted = zdt.withZoneSameLocal(ZoneId.of('Europe/Berlin'));

Используется реже, так как:

  • может изменить абсолютный момент
  • полезен при «переносе расписаний»

Конвертация через Instant (явный подход)

Иногда удобнее выполнять преобразование вручную:

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

const source = ZonedDateTime.now(ZoneId.of('Asia/Seoul'));

const instant = source.toInstant();

const target = instant.atZone(ZoneId.of('America/New_York'));

Этот способ подчёркивает фундаментальную модель:

  • ZonedDateTime → Instant → ZonedDateTime

Работа с UTC как промежуточной зоной

UTC часто используется как универсальный слой хранения:

const utcTime = zdt.withZoneSameInstant(ZoneId.UTC);

или:

const utcInstant = zdt.toInstant();

Преимущества:

  • отсутствие DST-скачков
  • стабильность хранения
  • единый формат для БД

DST (летнее время) и его влияние на конвертацию

js-joda учитывает правила переходов автоматически через ZoneRules.

При конвертации:

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

Пример неоднозначности

В момент перехода «назад» время может повторяться:

01:30 может существовать дважды

js-joda разрешает такие ситуации через правила зоны:

  • выбор первого или второго смещения
  • использование стандартных стратегий нормализации

Нормализация при создании ZonedDateTime

При создании времени из LocalDateTime возможны конфликты:

const zdt = ZonedDateTime.of(LocalDateTime.of(2026, 10, 25, 2, 30), zone);

Если такое время неоднозначно:

  • библиотека применяет правила ZoneId
  • может скорректировать offset автоматически

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

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

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

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

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

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

Перевод времени при сериализации и API

Типичный сценарий:

Хранение

const instant = zdt.toInstant().toString();

Восстановление

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

const restored = Instant.parse(instant).atZone(ZoneId.of('Europe/Paris'));

Такой подход гарантирует:

  • отсутствие потери данных
  • независимость от локальной зоны клиента

Работа с несколькими зонами одновременно

При построении расписаний часто требуется отображение одного события в разных зонах:

const base = ZonedDateTime.of(
  LocalDateTime.of(2026, 6, 1, 10, 0),
  ZoneId.of('UTC')
);

const zones = [
  ZoneId.of('Europe/London'),
  ZoneId.of('Asia/Dubai'),
  ZoneId.of('America/New_York')
];

const mapped = zones.map(z => base.withZoneSameInstant(z));

Каждый элемент массива отражает один и тот же момент времени.


Типичные ошибки при переводе зон

Ошибка 1: ручное добавление offset

// неправильный подход
const wrong = date + 3 * 60 * 60 * 1000;

Проблема:

  • игнорируются DST
  • игнорируются исторические изменения зон

Ошибка 2: использование LocalDateTime для глобальных событий

LocalDateTime не содержит зоны:

  • нельзя корректно сравнивать
  • нельзя конвертировать без контекста

Ошибка 3: повторное применение зон

zdt.withZoneSameInstant(zone1).withZoneSameInstant(zone2);

Хотя технически допустимо, часто приводит к:

  • потере ясности кода
  • усложнению трассировки времени

Согласование клиентского и серверного времени

В распределённых системах используется единый принцип:

  • клиент отправляет Instant
  • сервер хранит Instant
  • отображение происходит через ZoneId
const instant = ZonedDateTime.now(ZoneId.of('Asia/Almaty')).toInstant();

Итоговая модель преобразований

Вся работа с зонами сводится к цепочке:

LocalDateTime + ZoneId → ZonedDateTime → Instant → ZonedDateTime (новая зона)

Эта модель обеспечивает:

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