Переходы на летнее время

Переход на летнее время (DST, Daylight Saving Time) — одна из самых сложных тем при работе с датами и временем. Во многих странах часы переводятся вперёд или назад в определённый день года. Из-за этого возникают:

  • отсутствующие часы;
  • повторяющиеся интервалы времени;
  • неоднозначные даты;
  • ошибки при арифметике времени;
  • проблемы сериализации и хранения данных.

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


Почему DST создаёт проблемы

Во время перехода на летнее время локальное время изменяется неравномерно.

Например:

  • ночью часы могут перепрыгнуть с 01:59 сразу на 03:00;
  • либо после 02:59 снова наступает 02:00.

Из-за этого некоторые локальные значения времени:

  • никогда не существуют;
  • существуют дважды.

Пример несуществующего времени

В зоне 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:30 отсутствует, потому что часы были переведены вперёд.

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


Поддержка временных зон в Luxon

Все DST-операции в Luxon основаны на IANA Time Zone Database.

Примеры зон:

  • Europe/Berlin
  • America/New_York
  • Asia/Almaty
  • UTC

Создание даты с указанием зоны:

const dt = DateTime.now().setZone("America/New_York");

Без указания зоны используется системная временная зона.


Проверка смещения UTC

Во время DST смещение относительно UTC изменяется.

Получение offset:

const dt = DateTime.now().setZone("Europe/London");

console.log(dt.offset);

Результат:

0

или:

60

Значение указывается в минутах.


Определение летнего времени

Свойство isInDST показывает, действует ли DST для текущего объекта.

const dt = DateTime.now().setZone("Europe/Paris");

console.log(dt.isInDST);

Возможные значения:

true
false

Разница между локальным временем и UTC

UTC не содержит DST.

Поэтому хранение времени в UTC считается наиболее безопасным подходом.

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

const local = DateTime.local();

UTC

const utc = DateTime.utc();

Конвертация

const dt = DateTime.local()
  .setZone("UTC");

console.log(dt.toISO());

Проблема отсутствующего часа

Весной часы переводятся вперёд.

Например:

01:59 → 03:00

Интервал между 02:00 и 02:59 отсутствует.

Пример

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

console.log(dt.toISO());

Luxon нормализует время автоматически.


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

Некоторые комбинации даты и времени могут быть некорректными.

Проверка:

if (!dt.isValid) {
  console.log(dt.invalidReason);
}

Возможные причины:

  • unsupported zone;
  • invalid input;
  • unparsable;
  • invalid unit values.

Повторяющийся час

Осенью часы переводятся назад.

Пример:

02:59 → 02:00

Время между 02:00 и 02:59 возникает дважды.

Это создаёт неоднозначность.


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

Рассмотрим:

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

console.log(dt.toISO());

Такое время может относиться:

  • либо к летнему времени;
  • либо к стандартному времени.

Luxon выбирает один из вариантов согласно внутренним правилам зоны.


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

Наиболее надёжный способ — указывать UTC offset явно.

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

console.log(dt.toISO());

или:

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

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


Арифметика времени и DST

DST особенно опасен при сложении часов.

Ошибочный подход

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

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

console.log(result.toISO());

Из-за перехода итог может отличаться от ожидаемого локального времени.


Разница между plus({ hours: 24 }) и plus({ days: 1 })

Это принципиально разные операции.

Прибавление часов

dt.plus({ hours: 24 });

Добавляет фиксированное количество времени.

Прибавление дней

dt.plus({ days: 1 });

Сохраняет календарную дату.


Поведение при DST

Пример:

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

console.log(
  dt.plus({ hours: 24 }).toISO()
);

console.log(
  dt.plus({ days: 1 }).toISO()
);

Результаты могут различаться на один час.


Когда использовать часы, а когда дни

hours

Подходит для:

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

days

Подходит для:

  • календарей;
  • расписаний;
  • событий;
  • бизнес-логики.

Работа с интервалами

Luxon предоставляет класс Interval.

import { Interval } from "luxon";

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

const end = start.plus({ days: 1 });

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

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

Продолжительность суток может быть:

  • 23 часа;
  • 24 часа;
  • 25 часов.

Почему сутки не всегда равны 24 часам

DST изменяет длину локальных суток.

Весной

23 часа

Осенью

25 часов

Поэтому нельзя предполагать, что:

1 day === 24 hours

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

Наиболее безопасная стратегия:

  1. хранить время в UTC;
  2. конвертировать в локальную зону только при отображении.

Пример

const stored = DateTime.utc();

const local = stored.setZone(
  "America/New_York"
);

Такой подход уменьшает количество DST-ошибок.


Сериализация ISO-дат

ISO-формат желательно хранить вместе со смещением.

Хорошо

2025-10-26T02:30:00+02:00

Плохо

2025-10-26T02:30:00

Во втором случае зона неизвестна.


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

const dt = DateTime.local()
  .toUTC();

console.log(dt.toISO());

Результат:

2025-05-23T12:00:00.000Z

Суффикс Z означает UTC.


Возврат в локальную зону

const utc = DateTime.utc();

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

console.log(berlin.toString());

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

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

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

Ошибка хранения только локального времени

2025-10-26 02:30

Невозможно определить точный момент времени.


Хранение времени события

Оптимальная схема:

{
  "utc": "2025-10-26T00:30:00Z",
  "zone": "Europe/Berlin"
}

Сравнение дат во время DST

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

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

console.log(a.equals(b));

Результат:

false

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


Timestamp как универсальное решение

Unix timestamp не зависит от DST.

const ts = Date.now();

const dt = DateTime.fromMillis(ts);

Timestamp представляет абсолютный момент времени.


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

setZone

Меняет представление времени.

dt.setZone("Asia/Tokyo");

toUTC

Переводит объект в UTC.

dt.toUTC();

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

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

const dt = DateTime.local();

const changed = dt.setZone(
  "UTC",
  { keepLocalTime: true }
);

Эта операция опасна при DST, потому что может создать неоднозначное время.


Проверка перехода между датами

DST можно обнаружить через изменение offset.

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

const after = before.plus({ days: 2 });

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

Получение имени зоны

const dt = DateTime.now();

console.log(dt.zoneName);

Пример:

Europe/Berlin

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

const dt = DateTime.now()
  .setZone("America/New_York");

console.log(
  dt.toFormat(
    "yyyy-MM-dd HH:mm ZZZZ"
  )
);

Пример результата:

2025-05-23 18:20 EDT

DST и повторяющиеся cron-задачи

При запуске задач по локальному времени возможны проблемы:

  • задача может не выполниться;
  • задача может выполниться дважды.

Пример

02:30 every day

Во время весеннего перехода такого времени не существует.


Рекомендации для backend-систем

Хранить:

  • UTC timestamp;
  • timezone пользователя;
  • ISO-строки со смещением.

Избегать:

  • хранения локального времени без зоны;
  • арифметики через миллисекунды;
  • предположения, что сутки всегда 24 часа.

Рекомендации для frontend

Отображение

dt.setZone(userZone)
  .toLocaleString(
    DateTime.DATETIME_FULL
  );

Не хранить:

2025-10-26 02:30

без timezone.


Проверка различий между зонами

const ny = DateTime.now()
  .setZone("America/New_York");

const london = DateTime.now()
  .setZone("Europe/London");

console.log(ny.offset);
console.log(london.offset);

DST начинается в странах в разные даты, поэтому offset может изменяться независимо.


Работа с Duration

const duration = Duration.fromObject({
  hours: 24
});

DST влияет на результат применения duration к дате.


Пример безопасной логики календаря

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

const nextWeek = meeting.plus({
  weeks: 1
});

Календарные операции через weeks, days, months работают корректнее для расписаний.


Проверка поддержки зоны

const dt = DateTime.now()
  .setZone("Invalid/Zone");

console.log(dt.isValid);

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

Luxon содержит вспомогательный класс Info.

import { Info } from "luxon";

console.log(
  Info.hasDST("Europe/Berlin")
);

Результат:

true

Зоны без DST

Некоторые зоны никогда не используют переходы.

Например:

  • UTC
  • Asia/Dubai
  • Africa/Nairobi

Проверка:

Info.hasDST("UTC");

Влияние DST на разницу дат

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

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

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

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

23

Безопасная стратегия работы со временем

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

Используется:

  • UTC;
  • timestamp;
  • ISO со смещением.

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

Используется только для отображения интерфейса.


Основные источники DST-ошибок

Неверные предположения

1 day = 24 hours

Отсутствие timezone

2025-10-26 02:30

Хранение локального времени

без offset и zone.

Арифметика через миллисекунды

86400000

не всегда соответствует суткам.


Практические рекомендации

Для серверов

  • хранить UTC;
  • использовать ISO 8601;
  • сохранять timezone пользователя отдельно.

Для клиентских приложений

  • конвертировать время при отображении;
  • использовать setZone;
  • учитывать DST при календарных вычислениях.

Для планировщиков

  • избегать запуска в проблемные часы;
  • использовать UTC-расписания;
  • логировать offset и timezone.