Работа с UTC

UTC (Coordinated Universal Time) используется как базовая временная шкала для вычислений, хранения и передачи дат в большинстве серверных и распределённых систем. В JavaScript работа с UTC часто вызывает сложности из-за смешения локального времени, системной таймзоны и внутреннего представления Date как количества миллисекунд от эпохи Unix.

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


Встроенный объект Date хранит момент времени в виде количества миллисекунд с 1 января 1970 года UTC. Это означает:

  • внутренне значение всегда UTC-ориентировано
  • методы getHours(), getDate() и подобные возвращают локальное время
  • методы getUTCHours(), getUTCDate() работают в UTC
const d = new Date("2026-01-01T12:00:00Z");

d.getHours();     // локальное время
d.getUTCHours();  // UTC-время

Основная проблема заключается в том, что большинство операций в приложениях требуют явного контроля над таймзоной, а стандартный API JavaScript не предоставляет удобных инструментов для конвертаций.


Поведение date-fns без таймзон

date-fns работает исключительно с объектами Date и не интерпретирует их как принадлежащие конкретной таймзоне. Все функции библиотеки:

  • принимают Date или строку
  • возвращают новый Date
  • не изменяют внутреннюю временную зону

Пример:

import { format } from "date-fns";

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

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

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


Проблема локального времени при форматировании

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

import { format } from "date-fns";

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

format(date, "yyyy-MM-dd HH:mm");
// зависит от локальной таймзоны

Это означает, что одна и та же дата может отображаться по-разному на сервере и клиенте.


Явное использование UTC через методы Date

В рамках базового date-fns можно работать с UTC только через встроенные методы Date.

Получение UTC-компонентов

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

date.getUTCFullYear();  // 2026
date.getUTCMonth();     // 0
date.getUTCDate();      // 1
date.getUTCHours();     // 15

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

function formatUTC(date) {
  const yyyy = date.getUTCFullYear();
  const mm = String(date.getUTCMonth() + 1).padStart(2, "0");
  const dd = String(date.getUTCDate()).padStart(2, "0");

  return `${yyyy}-${mm}-${dd}`;
}

Такой подход используется редко, поскольку лишает преимуществ библиотеки форматирования.


Ограничения date-fns в работе с UTC

Основные ограничения:

  • отсутствие встроенных таймзон
  • отсутствие конвертации “локальное ↔︎ UTC”
  • форматирование зависит от окружения
  • отсутствие DST-логики (летнее время)

Эти ограничения приводят к необходимости использования расширения date-fns-tz.


date-fns-tz: работа с UTC и таймзонами

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

  • utcToZonedTime
  • zonedTimeToUtc
  • formatInTimeZone

Он позволяет корректно интерпретировать даты в UTC и локальных зонах.


Конвертация локального времени в UTC

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

const date = "2026-01-01 12:00:00";
const timeZone = "Europe/Almaty";

const utcDate = zonedTimeToUtc(date, timeZone);

Логика:

  • строка интерпретируется как локальное время указанной зоны
  • результат преобразуется в UTC Date

Конвертация UTC в локальную зону

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

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

const zoned = utcToZonedTime(utcDate, "Asia/Tokyo");

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


Форматирование даты в UTC-зоне

Ключевой инструмент:

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

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

const result = formatInTimeZone(
  date,
  "UTC",
  "yyyy-MM-dd HH:mm:ssXXX"
);

Особенности:

  • "UTC" используется как фиксированная зона
  • форматирование не зависит от окружения
  • гарантированная воспроизводимость

Использование UTC как стандартной модели хранения

Практика в backend-системах:

  • хранение всех дат в UTC
  • конвертация только на уровне отображения
  • отказ от локального времени в бизнес-логике

Пример нормализации:

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

function saveEvent(inputDate, timeZone) {
  return zonedTimeToUtc(inputDate, timeZone);
}

Это исключает ошибки, связанные с изменением системной таймзоны.


Работа с ISO-строками и UTC

ISO 8601 является стандартом передачи времени в UTC:

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

При использовании date-fns:

import { parseISO } from "date-fns";

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

parseISO:

  • корректно интерпретирует Z как UTC
  • не требует дополнительных преобразований

Сравнение дат в UTC

Сравнение в JavaScript всегда основано на timestamp:

const a = new Date("2026-01-01T10:00:00Z");
const b = new Date("2026-01-01T12:00:00Z");

a < b; // true

При использовании date-fns:

import { isBefore } from "date-fns";

isBefore(a, b); // true

UTC-аспект здесь скрыт, поскольку сравнение происходит по миллисекундам.


Округление и UTC-риски

Некоторые функции могут давать неожиданные результаты при локальном времени:

import { startOfDay } from "date-fns";

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

startOfDay(d);

startOfDay использует локальную зону, а не UTC. Это приводит к смещению границы суток.

Для UTC-логики используется:

import { startOfDay } from "date-fns";
import { utcToZonedTime } from "date-fns-tz";

const utc = new Date("2026-01-01T00:00:00Z");
const zoned = utcToZonedTime(utc, "UTC");

startOfDay(zoned);

Типовые архитектурные паттерны UTC

1. UTC как единый источник истины

  • база хранит UTC
  • API возвращает ISO UTC
  • клиент конвертирует в локальную зону

2. Явная таймзона на входе

  • пользователь вводит локальное время
  • сервер переводит в UTC через zonedTimeToUtc

3. Изоляция форматирования

  • вычисления: UTC
  • отображение: formatInTimeZone

Ошибки при работе с UTC

1. Использование format без учёта зоны

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

Результат зависит от сервера.

2. Смешивание локального и UTC времени

new Date().getHours(); // локально
new Date().getUTCHours(); // UTC

3. Неправильная интерпретация ISO без Z

new Date("2026-01-01T10:00:00"); // локальная зона

Рекомендованная модель работы

С точки зрения date-fns и date-fns-tz оптимальная схема выглядит следующим образом:

  • входные данные → локальное время + таймзона
  • преобразование → UTC через zonedTimeToUtc
  • хранение → UTC Date
  • вывод → formatInTimeZone

Работа с календарными границами в UTC

При расчётах периодов важно учитывать зону:

import { startOfDay, endOfDay } from "date-fns";

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

startOfDay(date);
endOfDay(date);

Для строгого UTC:

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

const start = formatInTimeZone(date, "UTC", "yyyy-MM-dd'T'00:00:00XXX");

Модель неизменяемости дат

date-fns использует неизменяемый подход:

  • функции не мутируют исходный Date
  • каждый вызов возвращает новый объект
  • UTC-логика строится поверх этого поведения

Это позволяет безопасно комбинировать цепочки:

import { addDays } from "date-fns";

const result = addDays(new Date("2026-01-01T00:00:00Z"), 5);

Итоговая структура UTC-обработки в date-fns-экосистеме

  • базовые операции: date-fns
  • таймзоны и UTC: date-fns-tz
  • хранение: UTC Date
  • отображение: formatInTimeZone
  • конвертация: zonedTimeToUtc, utcToZonedTime