Парсинг дат с часовыми поясами

В JavaScript объект Date хранит момент времени в виде количества миллисекунд с начала эпохи Unix (UTC). Вся работа с локальным отображением времени происходит на уровне среды выполнения и не содержит информации о конкретном часовом поясе в самом объекте. Это создаёт ключевую проблему при парсинге строк дат с указанием временных зон: теряется контекст исходной зоны, если не использовать специализированные инструменты.

Библиотека date-fns предоставляет набор функций для работы с датами, но не включает полноценную поддержку часовых поясов. Для этого используется дополнительный пакет date-fns-tz, который расширяет базовые возможности и вводит операции преобразования между UTC и IANA-зонами.


Базовый принцип: отсутствие часового пояса в Date

При создании даты через стандартный конструктор:

const date = new Date("2026-01-01T10:00:00");

строка интерпретируется в зависимости от среды выполнения. Если указана зона Z, то она трактуется как UTC:

const date = new Date("2026-01-01T10:00:00Z");

Результат всегда хранится в UTC-форме, а отображение зависит от локальной зоны системы.


Ограничения date-fns при парсинге зон

Функция parse из date-fns работает только с локальной интерпретацией времени:

import { parse } from "date-fns";

const date = parse("2026-01-01 10:00", "yyyy-MM-dd HH:mm", new Date());

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

Ключевая проблема:

  • строка с зоной не интерпретируется как смещение
  • время приводится к локальному часовому поясу окружения
  • теряется точная привязка к исходному региону

Работа с IANA часовыми поясами через date-fns-tz

Для корректного парсинга используется пакет date-fns-tz.

Основные функции

  • zonedTimeToUtc — перевод локального времени в UTC с учётом зоны
  • toZonedTime — преобразование UTC в локальное время указанной зоны
  • formatInTimeZone — форматирование даты в заданной зоне

Парсинг локального времени как UTC с зоной

zonedTimeToUtc

Функция преобразует локальное время, заданное в конкретной зоне, в UTC:

import { zonedTimeToUtc } from "date-fns-tz";

const utcDate = zonedTimeToUtc(
  "2026-01-01 10:00:00",
  "Europe/Berlin"
);

Механизм работы:

  1. строка интерпретируется как локальное время в указанной зоне
  2. вычисляется смещение относительно UTC
  3. возвращается эквивалент в UTC

Обратное преобразование UTC в локальную зону

toZonedTime

import { toZonedTime } from "date-fns-tz";

const utcDate = new Date("2026-01-01T09:00:00Z");

const berlinTime = toZonedTime(utcDate, "Europe/Berlin");

Результат — объект Date, который при форматировании будет отображаться как время в указанной зоне.

Важно учитывать:

  • сам объект Date остаётся UTC
  • зона применяется только при интерпретации

Форматирование с учётом часового пояса

formatInTimeZone

Наиболее безопасный способ отображения времени в конкретной зоне:

import { formatInTimeZone } from "date-fns-tz";

const result = formatInTimeZone(
  new Date("2026-01-01T09:00:00Z"),
  "Europe/Berlin",
  "yyyy-MM-dd HH:mm:ss"
);

Поведение:

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

Парсинг строк с ISO и временными зонами

ISO 8601 с UTC

Корректный вариант:

const date = new Date("2026-01-01T10:00:00Z");

Такой формат однозначен и не требует дополнительных библиотек.


ISO с смещением

const date = new Date("2026-01-01T10:00:00+03:00");

Механизм:

  • смещение применяется при парсинге
  • внутренне переводится в UTC
  • исходная зона не сохраняется

Строки без смещения

const date = new Date("2026-01-01T10:00:00");

Интерпретация зависит от локальной среды выполнения, что делает результат неоднозначным при распределённых системах.


Типовая ошибка: попытка встроить зону в parse

import { parse } from "date-fns";

const date = parse(
  "2026-01-01 10:00 Europe/Berlin",
  "yyyy-MM-dd HH:mm zzz",
  new Date()
);

Результат не будет корректно учитывать временную зону, поскольку date-fns не выполняет IANA-резолвинг.


Корректная стратегия работы с часовыми поясами

Структура обработки обычно разделяется на этапы:

1. Хранение данных в UTC

Все временные метки приводятся к UTC:

const utc = zonedTimeToUtc("2026-01-01 10:00", "Asia/Tokyo");

2. Передача и хранение без потерь

UTC-значения безопасны для:

  • баз данных
  • API
  • очередей сообщений

3. Отображение в локальной зоне

const view = formatInTimeZone(
  utc,
  "Europe/Berlin",
  "dd.MM.yyyy HH:mm"
);

Работа с летним и зимним временем (DST)

IANA-зоны учитывают переходы на летнее время автоматически.

Пример:

zonedTimeToUtc("2026-07-01 10:00:00", "Europe/Berlin");
zonedTimeToUtc("2026-01-01 10:00:00", "Europe/Berlin");

Разница смещения будет вычислена автоматически:

  • летом: UTC+2
  • зимой: UTC+1

Различие между offset и IANA зонами

Часовой offset

+03:00
  • фиксированное смещение
  • не учитывает DST

IANA zone

Europe/Berlin
  • содержит правила переходов
  • зависит от исторических и региональных изменений
  • предпочтительнее для серверных приложений

Практика комбинирования date-fns и date-fns-tz

date-fns используется для:

  • вычисления интервалов
  • сравнения дат
  • форматирования без зон

date-fns-tz используется для:

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

Частые ошибки при парсинге с часовыми поясами

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

new Date("2026-01-01 10:00");

Результат зависит от окружения.


Двойное преобразование

toZonedTime(zonedTimeToUtc(...))

Приводит к искажению времени из-за повторного применения смещения.


Использование parse вместо zonedTimeToUtc

parse("2026-01-01 10:00", ...)

Не учитывает IANA-зону вообще.


Рекомендованный подход к архитектуре времени

  • хранение: UTC (Date или ISO string с Z)
  • ввод: локальное время + зона
  • преобразование: zonedTimeToUtc
  • отображение: formatInTimeZone
  • вычисления: date-fns без зональных преобразований