В JavaScript объект Date хранит момент времени в виде
количества миллисекунд от эпохи Unix, то есть в UTC. Однако большинство
операций отображения и ввода дат выполняется в локальном часовом поясе
окружения. Это приводит к ситуации, когда одна и та же дата может
интерпретироваться по-разному в зависимости от системы, региона и
настроек среды выполнения.
При вычислении разницы между датами с использованием функций
date-fns возникает ключевая проблема: арифметика
выполняется над абсолютными моментами времени, но исходные данные часто
представлены как локальные календарные значения. Это особенно критично
при работе с:
В основе работы JavaScript Date лежит UTC-представление. Например:
const date = new Date('2026-01-01T00:00:00');
Строка ISO интерпретируется как UTC-время. Однако при создании даты без временной зоны:
const date = new Date(2026, 0, 1);
значение трактуется как локальное время окружения, после чего преобразуется в UTC внутри объекта.
Это различие критично при использовании функций
date-fns, таких как:
differenceInMillisecondsdifferenceInSecondsdifferenceInHoursdifferenceInDaysВсе они оперируют абсолютной временной шкалой, игнорируя календарные особенности часового пояса.
Функции разницы в 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 измеряет разницу в абсолютных миллисекундах, а не в календарных интервалах.
Различие между временными моделями:
Пример проблемного случая:
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,
добавляя поддержку явного управления часовыми поясами. Основные
функции:
zonedTimeToUtcutcToZonedTimeformatInTimeZoneimport { zonedTimeToUtc } from 'date-fns-tz';
const timeZone = 'Asia/Almaty';
const date = zonedTimeToUtc('2026-01-01 00:00:00', timeZone);
Данное преобразование фиксирует момент времени в 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');
Такой подход исключает зависимость от локальной среды выполнения.
Корректная стратегия вычислений строится на предварительной нормализации данных в одну временную шкалу.
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 может округлять результат в сторону
целых суток.При работе с распределенными системами используется единая стратегия:
Типовая схема:
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);
Такая нормализация устраняет смещение времени внутри суток и обеспечивает стабильность вычислений при агрегации данных.