Форматирование с учетом timezone

Работа с датами в JavaScript неизбежно упирается в проблему часовых поясов: одинаковое значение Date может отображаться по-разному в зависимости от локали среды выполнения, настроек сервера или браузера. Библиотека date-fns предоставляет инструменты форматирования дат, но сама по себе не решает задачу корректного отображения времени в произвольных timezone. Для этого используется расширение date-fns-tz, которое добавляет операции преобразования и форматирования с учётом временных зон.


Встроенный объект Date хранит момент времени в виде количества миллисекунд от Unix epoch (UTC). Однако:

  • вывод Date.toString() зависит от локальной timezone среды;
  • Date.toISOString() всегда возвращает UTC;
  • большинство методов (getHours, getDate) используют локальную timezone.

Ключевая проблема: один и тот же момент времени интерпретируется по-разному в разных регионах.

Пример:

const d = new Date("2026-01-01T12:00:00Z");

console.log(d.toString());
// зависит от локальной timezone

console.log(d.toISOString());
// всегда: 2026-01-01T12:00:00.000Z

При построении систем логирования, календарей и расписаний этого недостаточно: требуется явное управление timezone.


Форматирование дат в date-fns без timezone

date-fns предоставляет функцию format, которая работает только с локальной timezone среды выполнения.

Основные токены форматирования

import { format } from "date-fns";

const d = new Date(2026, 0, 1, 15, 30);

format(d, "yyyy-MM-dd HH:mm:ss");

Ключевые токены:

  • yyyy — год
  • MM — месяц (01–12)
  • dd — день месяца
  • HH — часы (24-часовой формат)
  • mm — минуты
  • ss — секунды

Ограничение

Функция не принимает timezone параметр, поэтому:

format(new Date("2026-01-01T12:00:00Z"), "HH:mm");

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


Почему timezone нельзя игнорировать

В распределённых системах один и тот же timestamp может интерпретироваться по-разному:

  • сервер (UTC)
  • пользователь (локальная зона)
  • база данных (часто UTC)
  • сторонние API (разные зоны)

Без явной timezone логика становится неоднозначной:

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

Подход date-fns-tz: явная работа с зонами

date-fns-tz добавляет функции:

  • formatInTimeZone
  • toZonedTime
  • fromZonedTime

Эти функции позволяют отделить:

  • момент времени (UTC timestamp)
  • представление времени (timezone)

formatInTimeZone: ключевой инструмент форматирования

Сигнатура

formatInTimeZone(date, timeZone, formatString)

Пример

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

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

const result = formatInTimeZone(
  date,
  "Asia/Almaty",
  "yyyy-MM-dd HH:mm:ss XXX"
);

console.log(result);

Разбор компонентов

  • date — исходный момент времени (UTC)
  • "Asia/Almaty" — целевая timezone
  • формат — строка вывода

Токены timezone

Особое значение имеют:

  • XXX — смещение +05:00
  • zzzz — полное название timezone
  • z — краткое обозначение (если доступно)

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

Правильная архитектура работы с датами:

1. Хранение

Всегда UTC:

const stored = new Date().toISOString();

2. Обработка

Всё вычисляется в UTC:

const timestamp = Date.now();

3. Отображение

Только через timezone:

formatInTimeZone(timestamp, "Europe/Berlin", "yyyy-MM-dd HH:mm");

Конвертация времени между timezone

toZonedTime

Преобразует момент времени в “виртуальное локальное время” указанной зоны:

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

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

const zoned = toZonedTime(utcDate, "America/New_York");

console.log(zoned);

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


fromZonedTime

Создаёт UTC момент из локального времени конкретной зоны:

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

const date = fromZonedTime(
  "2026-01-01 10:00:00",
  "Europe/Paris"
);

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


Форматирование с offset и UTC-индикацией

Часто требуется отображать не только локальное время, но и смещение.

Пример формата

formatInTimeZone(date, "Asia/Tokyo", "yyyy-MM-dd HH:mm XXX");

Результат:

2026-01-01 21:00 +09:00

Сложные форматы отображения

Полный формат с timezone идентификатором

formatInTimeZone(
  date,
  "America/Los_Angeles",
  "yyyy-MM-dd HH:mm:ss 'GMT' XXX"
);

Вывод:

2026-01-01 04:00:00 GMT -08:00

Работа с пользовательскими timezone

В системах с профилями пользователей timezone часто хранится как строка:

const user = {
  timezone: "Europe/Moscow"
};

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

formatInTimeZone(
  Date.now(),
  user.timezone,
  "HH:mm dd.MM.yyyy"
);

Типичные ошибки при работе с timezone

1. Использование локального Date без учета зоны

format(new Date(), "yyyy-MM-dd HH:mm");

Проблема: результат зависит от сервера.


2. Попытка вручную смещать часы

date.setHours(date.getHours() + 3);

Проблема: не учитывает переходы DST и сложные зоны.


3. Хранение времени в локальной зоне

Проблема приводит к несогласованности данных между регионами.


DST (Daylight Saving Time)

Timezone — это не фиксированное смещение. Например:

  • Europe/Berlin меняет offset летом и зимой
  • Asia/Almaty не использует DST

date-fns-tz учитывает DST автоматически через IANA timezone database.

Пример:

formatInTimeZone(date, "Europe/Berlin", "yyyy-MM-dd HH:mm XXX");

Результат будет отличаться в зависимости от сезона.


Сравнение подходов

Без timezone

format(date, "HH:mm");
  • зависит от среды
  • не подходит для серверных систем

С timezone

formatInTimeZone(date, "UTC", "HH:mm");
  • предсказуемый результат
  • одинаковый во всех системах

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

  • преобразование timezone не бесплатное
  • частые вызовы форматирования в циклах могут быть дорогими
  • рекомендуется кэшировать форматированные строки для UI

Комбинирование с другими функциями date-fns

date-fns часто используется вместе с:

  • addDays
  • subHours
  • differenceInMinutes
  • startOfDay

Пример:

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

const nextWeek = addDays(new Date(), 7);

formatInTimeZone(nextWeek, "Asia/Almaty", "yyyy-MM-dd");

Форматирование для API и логов

Логи в UTC

formatInTimeZone(new Date(), "UTC", "yyyy-MM-dd HH:mm:ss");

Логи в пользовательской зоне

formatInTimeZone(new Date(), user.timezone, "yyyy-MM-dd HH:mm:ss XXX");

Поддержка IANA timezone

Используются стандартные идентификаторы:

  • UTC
  • Europe/London
  • Asia/Tokyo
  • America/New_York

Эти значения являются частью IANA Time Zone Database и обеспечивают корректную обработку исторических изменений.


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

  • date-fns — базовые операции с датами
  • date-fns-tz — контроль timezone и корректное отображение
  • хранение — UTC
  • отображение — через formatInTimeZone
  • ввод — через fromZonedTime