Учет часовых поясов при вычислении разницы

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

При вычислении разницы между датами с использованием функций date-fns возникает ключевая проблема: арифметика выполняется над абсолютными моментами времени, но исходные данные часто представлены как локальные календарные значения. Это особенно критично при работе с:

  • переходами через границы суток;
  • изменением летнего/зимнего времени (DST);
  • сравнениями дат без учета временной составляющей;
  • агрегацией данных по дням в разных регионах.

UTC как базовая модель вычислений

В основе работы JavaScript Date лежит UTC-представление. Например:

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

Строка ISO интерпретируется как UTC-время. Однако при создании даты без временной зоны:

const date = new Date(2026, 0, 1);

значение трактуется как локальное время окружения, после чего преобразуется в UTC внутри объекта.

Это различие критично при использовании функций date-fns, таких как:

  • differenceInMilliseconds
  • differenceInSeconds
  • differenceInHours
  • differenceInDays

Все они оперируют абсолютной временной шкалой, игнорируя календарные особенности часового пояса.

Поведение функций date-fns при вычислении разницы

Функции разницы в date-fns основаны на простом вычитании временных меток:

import { differenceInHours } from 'date-fns';

const a = new Date('2026-01-01T00:00:00Z');
const b = new Date('2026-01-02T00:00:00Z');

differenceInHours(b, a); // 24

Результат корректен, поскольку обе даты заданы в UTC.

Однако при локальных датах результат может отличаться от ожидаемого календарного значения:

const a = new Date(2026, 2, 29); // локальное время
const b = new Date(2026, 2, 30);

differenceInDays(b, a);

При переходе через DST или при различии локального смещения разница может быть не равна 1 календарному дню в привычном смысле.

Ключевая особенность:

date-fns измеряет разницу в абсолютных миллисекундах, а не в календарных интервалах.

Календарная разница против абсолютной разницы

Различие между временными моделями:

  • Абсолютная разница — вычисление на основе UTC timestamp
  • Календарная разница — логическая разница в днях, часах или месяцах с учетом локального календаря

Пример проблемного случая:

const a = new Date('2026-03-29T00:30:00+02:00');
const b = new Date('2026-03-30T00:30:00+03:00');

differenceInDays(b, a);

Несмотря на визуально одинаковое время суток, разница может быть не равна 1 дню из-за смены смещения часового пояса.

Использование date-fns-tz для корректной работы с зонами

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

  • zonedTimeToUtc
  • utcToZonedTime
  • formatInTimeZone

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

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

const timeZone = 'Asia/Almaty';
const date = zonedTimeToUtc('2026-01-01 00:00:00', timeZone);

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

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

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

const utcDate = new Date('2026-01-01T00:00:00Z');
const zoned = utcToZonedTime(utcDate, 'Asia/Almaty');

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

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

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

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

formatInTimeZone(date, 'Asia/Almaty', 'yyyy-MM-dd HH:mm:ss');

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

Вычисление разницы с учетом часового пояса

Корректная стратегия вычислений строится на предварительной нормализации данных в одну временную шкалу.

Приведение дат к UTC перед вычислением

import { differenceInHours } from 'date-fns';
import { zonedTimeToUtc } from 'date-fns-tz';

const timeZone = 'Europe/Berlin';

const a = zonedTimeToUtc('2026-03-28 12:00:00', timeZone);
const b = zonedTimeToUtc('2026-03-29 12:00:00', timeZone);

differenceInHours(b, a);

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

Сравнение календарных дней в конкретной зоне

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

import { startOfDay, differenceInCalendarDays } from 'date-fns';
import { utcToZonedTime } from 'date-fns-tz';

const zone = 'Asia/Tokyo';

const a = utcToZonedTime(new Date('2026-01-01T18:00:00Z'), zone);
const b = utcToZonedTime(new Date('2026-01-02T01:00:00Z'), zone);

differenceInCalendarDays(
  startOfDay(b),
  startOfDay(a)
);

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

Особенности перехода на летнее время

При переходе на DST сутки могут содержать 23 или 25 часов. Это напрямую влияет на результаты:

const a = new Date('2026-03-29T00:00:00+01:00');
const b = new Date('2026-03-30T00:00:00+02:00');

Фактическая разница в миллисекундах может не соответствовать 24 часам. В результате:

  • differenceInHours возвращает значение, отличное от 24;
  • differenceInDays может округлять результат в сторону целых суток.

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

При работе с распределенными системами используется единая стратегия:

  1. Все входные данные переводятся в UTC.
  2. Все вычисления выполняются в абсолютном времени.
  3. Отображение выполняется с учетом целевой временной зоны.

Типовая схема:

import { zonedTimeToUtc, formatInTimeZone } from 'date-fns-tz';
import { differenceInMinutes } from 'date-fns';

const zone = 'America/New_York';

const start = zonedTimeToUtc('2026-01-01 08:00:00', zone);
const end = zonedTimeToUtc('2026-01-01 10:30:00', zone);

differenceInMinutes(end, start); // 150

formatInTimeZone(end, zone, 'HH:mm'); // 10:30

Вычисление разницы между временными зонами

Сценарии с несколькими зонами требуют приведения всех дат к UTC перед сравнением:

const a = zonedTimeToUtc('2026-01-01 10:00:00', 'Asia/Almaty');
const b = zonedTimeToUtc('2026-01-01 10:00:00', 'Europe/London');

differenceInHours(b, a);

Несмотря на одинаковое локальное время, фактическая разница отражает географическое смещение.

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

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

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

const zone = 'Asia/Almaty';

const date = utcToZonedTime(new Date(), zone);
const normalized = startOfDay(date);

Такая нормализация устраняет смещение времени внутри суток и обеспечивает стабильность вычислений при агрегации данных.