Несуществующие и неоднозначные времена

Работа с датой и временем осложняется переходами на летнее и зимнее время. В определённые моменты локальное время может:

  • не существовать вообще;
  • существовать дважды.

Подобные ситуации возникают из-за изменения смещения (offset) часового пояса.

Например, при переводе часов вперёд часть локального времени пропадает:

Локальное время Состояние
01:59 существует
02:00 не существует
02:30 не существует
03:00 существует

При переводе часов назад время дублируется:

Локальное время Состояние
01:30 существует
02:00 существует
02:30 существует дважды
03:00 существует

Luxon содержит встроенные механизмы обработки подобных ситуаций.


Несуществующее время

Несуществующее время появляется при переходе на летнее время, когда часы переводятся вперёд.

Пример для часового пояса Europe/Berlin:

import { DateTime } from "luxon";

const dt = DateTime.fromObject(
  {
    year: 2025,
    month: 3,
    day: 30,
    hour: 2,
    minute: 30
  },
  {
    zone: "Europe/Berlin"
  }
);

console.log(dt.toString());

В ночь перехода на летнее время часы перескакивают с 02:00 сразу на 03:00. Время 02:30 не существует.

Luxon автоматически корректирует значение:

2025-03-30T03:30:00.000+02:00

Библиотека не создаёт ошибку, а переносит время вперёд до ближайшего допустимого момента.


Автоматическая нормализация времени

Luxon стремится создавать валидные объекты DateTime. Поэтому несуществующие значения корректируются автоматически.

Пример:

const dt = DateTime.fromISO(
  "2025-03-30T02:15",
  {
    zone: "Europe/Berlin"
  }
);

console.log(dt.toISO());

Результат:

2025-03-30T03:15:00.000+02:00

Фактически Luxon говорит:

«02:15 не существует, ближайшее допустимое время — 03:15».


Проверка смещения часового пояса

Для понимания происходящего полезно анализировать смещение (offset).

const before = DateTime.fromISO(
  "2025-03-30T01:30",
  { zone: "Europe/Berlin" }
);

const after = DateTime.fromISO(
  "2025-03-30T03:30",
  { zone: "Europe/Berlin" }
);

console.log(before.offset);
console.log(after.offset);

Результат:

60
120

Смещение изменилось с +01:00 на +02:00.


Как определить переход между временными зонами

Метод isInDST показывает, находится ли дата в летнем времени.

console.log(before.isInDST);
console.log(after.isInDST);

Результат:

false
true

Это особенно полезно при:

  • расчётах расписаний;
  • обработке международных событий;
  • построении календарей;
  • синхронизации серверов.

Неоднозначное время

Неоднозначное время появляется при переводе часов назад.

Например:

const dt = DateTime.fromObject(
  {
    year: 2025,
    month: 10,
    day: 26,
    hour: 2,
    minute: 30
  },
  {
    zone: "Europe/Berlin"
  }
);

console.log(dt.toString());

В этот день время 02:30 возникает дважды:

  1. до перевода часов;
  2. после перевода часов.

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

При переходе на зимнее время часы откатываются назад:

02:59 → 02:00

Из-за этого диапазон времени повторяется повторно:

02:00 - 02:59

Следовательно, локальное время уже не уникально.


Как Luxon обрабатывает неоднозначность

Luxon выбирает один из возможных offset автоматически.

console.log(dt.offset);

Результат может быть:

120

или:

60

в зависимости от правил временной зоны и конкретной версии данных IANA timezone database.


Сравнение одинакового локального времени с разными offset

Можно создать два разных момента времени вручную.

const first = DateTime.fromISO(
  "2025-10-26T02:30:00+02:00"
);

const second = DateTime.fromISO(
  "2025-10-26T02:30:00+01:00"
);

console.log(first.toUTC().toISO());
console.log(second.toUTC().toISO());

Результат:

2025-10-26T00:30:00.000Z
2025-10-26T01:30:00.000Z

Локальное время одинаковое, но реальные моменты времени отличаются на один час.


Проверка timestamp

Внутри Luxon каждый объект хранит абсолютный timestamp.

console.log(first.toMillis());
console.log(second.toMillis());

Результат:

1761438600000
1761442200000

Разница подтверждает, что это разные моменты времени.


Использование UTC для устранения неоднозначности

Наиболее надёжный подход — хранение времени в UTC.

const utc = DateTime.utc(
  2025,
  10,
  26,
  1,
  30
);

console.log(utc.toISO());

Результат:

2025-10-26T01:30:00.000Z

UTC не содержит переходов на летнее и зимнее время, поэтому:

  • отсутствуют дублирующиеся часы;
  • отсутствуют пропущенные часы;
  • timestamp всегда уникален.

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

Локальное представление формируется только на этапе отображения.

const local = utc.setZone("Europe/Berlin");

console.log(local.toString());

Это безопаснее, чем хранение локального времени.


Опасность арифметики локального времени

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

const dt = DateTime.fromISO(
  "2025-03-30T01:30",
  {
    zone: "Europe/Berlin"
  }
);

const result = dt.plus({ hours: 1 });

console.log(result.toString());

Результат:

2025-03-30T03:30:00.000+02:00

Вместо 02:30 получилось 03:30, потому что час 02:* отсутствует.


Разница между добавлением часов и добавлением timestamp

Luxon работает с абсолютным временем.

const plus60Minutes = dt.plus({ minutes: 60 });

console.log(plus60Minutes.toString());

Результат также будет:

03:30

Фактически произошло увеличение timestamp на 3600 секунд.


Проверка переходов через diff

const start = DateTime.fromISO(
  "2025-03-30T01:00",
  {
    zone: "Europe/Berlin"
  }
);

const end = DateTime.fromISO(
  "2025-03-30T04:00",
  {
    zone: "Europe/Berlin"
  }
);

console.log(
  end.diff(start, "hours").hours
);

Результат:

2

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


Работа с Interval

Interval корректно учитывает переходы DST.

import { Interval } from "luxon";

const interval = Interval.fromDateTimes(
  start,
  end
);

console.log(interval.length("hours"));

Результат:

2

Проверка валидности DateTime

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

const invalid = DateTime.fromObject({
  month: 15
});

console.log(invalid.isValid);

Результат:

false

Информация об ошибке:

console.log(invalid.invalidReason);
console.log(invalid.invalidExplanation);

Временные зоны и IANA

Luxon использует базу временных зон IANA.

Примеры:

"Europe/Berlin"
"Asia/Tokyo"
"America/New_York"

Нельзя полагаться на фиксированные offset:

+02:00

потому что offset может изменяться:

  • летом;
  • зимой;
  • после обновления законодательства страны.

Проверка offset для конкретной даты

const winter = DateTime.fromISO(
  "2025-01-15",
  {
    zone: "Europe/Berlin"
  }
);

const summer = DateTime.fromISO(
  "2025-07-15",
  {
    zone: "Europe/Berlin"
  }
);

console.log(winter.offset);
console.log(summer.offset);

Результат:

60
120

Получение имени временной зоны

console.log(summer.zoneName);

Результат:

Europe/Berlin

Краткие рекомендации

Хранение данных

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

  • хранить время в UTC;
  • отображать локально через setZone().

Работа с расписаниями

Для расписаний важно учитывать:

  • переходы DST;
  • неоднозначные часы;
  • пропущенные часы.

Особенно это критично для:

  • авиаперелётов;
  • видеоконференций;
  • cron-задач;
  • банковских операций.

Проверка временных интервалов

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

Следует использовать:

diff()
Interval
toMillis()

Избегание фиксированных offset

Плохой подход:

UTC+3

Правильный подход:

Europe/Moscow
Asia/Almaty
America/New_York

Практический пример обработки пользовательского времени

function createUserDateTime(
  isoString,
  zone
) {
  const dt = DateTime.fromISO(
    isoString,
    { zone }
  );

  if (!dt.isValid) {
    throw new Error(
      dt.invalidExplanation
    );
  }

  return dt;
}

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

const meeting = createUserDateTime(
  "2025-10-26T02:30",
  "Europe/Berlin"
);

console.log(meeting.toISO());

Преобразование между зонами

const ny = DateTime.fromISO(
  "2025-07-01T12:00",
  {
    zone: "America/New_York"
  }
);

const tokyo = ny.setZone(
  "Asia/Tokyo"
);

console.log(ny.toString());
console.log(tokyo.toString());

Luxon сохраняет один и тот же абсолютный момент времени, изменяя только локальное представление.


Разница между setZone и toUTC

toUTC()

const utc = ny.toUTC();

Переводит время в UTC.


setZone()

const paris = ny.setZone(
  "Europe/Paris"
);

Изменяет отображаемую временную зону.


Локальное время и абсолютное время

В Luxon необходимо различать два понятия:

Локальное время

2025-10-26 02:30

Может быть неоднозначным.


Абсолютное время

timestamp
UTC
Unix time

Всегда уникально.


Почему DST создаёт сложность

Проблема заключается в том, что локальное время — это отображение, а не физическое время.

При изменении правил часового пояса:

  • одни часы исчезают;
  • другие повторяются;
  • смещение меняется динамически.

Luxon скрывает большую часть сложности, однако понимание природы DST остаётся критически важным для корректной работы с датами и временем.