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

Летнее время (DST, Daylight Saving Time) — механизм сезонного перевода часов, при котором локальное время смещается вперёд или назад. В большинстве регионов перевод выполняется два раза в год:

  • весной часы переводятся вперёд;
  • осенью — назад.

Для программирования это создаёт ряд проблем:

  • некоторые локальные моменты времени не существуют;
  • некоторые моменты повторяются дважды;
  • продолжительность суток может составлять 23 или 25 часов;
  • арифметика дат начинает зависеть от временной зоны.

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


Временные зоны и DST в Luxon

Luxon использует:

  • объект DateTime для работы с датой и временем;
  • API Intl браузера или Node.js;
  • базу временных зон IANA.

Пример создания даты в конкретной зоне:

import { DateTime } from "luxon";

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

console.log(dt.toString());

Именно временная зона определяет:

  • действует ли летнее время;
  • какой текущий UTC-offset;
  • когда выполняются переходы.

Проверка активности летнего времени

Для проверки используется свойство isInDST.

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

console.log(dt.isInDST);

Результат:

true

или:

false

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

  • отображении локального времени;
  • расчётах расписаний;
  • логировании;
  • построении календарей.

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

Свойство offset возвращает смещение относительно UTC в минутах.

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

Зимой используется UTC+1, летом — UTC+2.


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

UTC не подвержен летнему времени.

const utc = DateTime.now().toUTC();

console.log(utc.isInDST);

Результат:

false

Работа в UTC позволяет избежать большинства проблем DST:

const createdAt = DateTime.utc();

Типичный подход:

  • хранение дат в UTC;
  • преобразование в локальную зону только при отображении.

Переход на летнее время: несуществующее время

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

Например:

02:00 → 03:00

Промежуток между 02:00 и 02:59 не существует.

Пример

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

console.log(dt.toString());

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

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

Время 02:30 не существовало, поэтому произошло смещение вперёд.


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

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

Для проверки используется:

dt.isValid

Пример:

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

Дополнительные свойства:

console.log(dt.invalidExplanation);

Это особенно важно при:

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

Повторяющееся время при переходе на зимнее время

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

03:00 → 02:00

Из-за этого промежуток времени повторяется дважды.

Например:

02:30

может относиться:

  • к летнему времени;
  • к зимнему времени.

Пример

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

console.log(dt.toString());

Luxon выбирает одно из возможных значений согласно правилам временной зоны.


Получение информации о временной зоне

Метод offsetNameShort возвращает краткое название зоны.

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

console.log(dt.offsetNameShort);

Возможный результат:

CET

или:

CEST

Полное название:

console.log(dt.offsetNameLong);

Например:

Central European Summer Time

Изменение временной зоны

Метод setZone() позволяет переводить время между зонами.

const utc = DateTime.utc();

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

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

Luxon автоматически учитывает DST для каждой зоны.


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

По умолчанию Luxon сохраняет абсолютный момент времени.

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

Для этого используется:

keepLocalTime: true

Пример:

const dt = DateTime.fromISO(
  "2025-07-10T10:00",
  {
    zone: "UTC"
  }
);

const converted = dt.setZone(
  "Europe/Berlin",
  {
    keepLocalTime: true
  }
);

console.log(converted.toString());

Это изменяет интерпретацию времени, а не сам момент времени.


Арифметика дат во время DST

DST влияет на вычисления продолжительности.

Добавление часов

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

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

console.log(result.toString());

Во время перехода на летнее время сутки могут длиться 23 часа.


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

Это один из важнейших аспектов работы с DST.

Календарное добавление

dt.plus({ days: 1 })

Luxon перемещает дату на следующий календарный день.

Добавление часов

dt.plus({ hours: 24 })

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

При DST результаты могут различаться.

Пример

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

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

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

Во время смены времени итоговые часы могут отличаться.


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

Метод diff() учитывает DST автоматически.

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

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

const diff = end.diff(start, "hours");

console.log(diff.toObject());

Результат:

{ hours: 23 }

Хотя календарно прошёл один день.


Нормализация времени через UTC

Для надёжных вычислений часто используется схема:

  1. преобразование в UTC;
  2. выполнение арифметики;
  3. возврат в локальную зону.

Пример:

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

const utc = local.toUTC();

const updated = utc.plus({ hours: 24 });

const result = updated.setZone("Europe/Berlin");

console.log(result.toString());

Такой подход минимизирует ошибки при DST.


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

DST особенно опасен для:

  • cron-задач;
  • календарей;
  • уведомлений;
  • систем бронирования;
  • авиарейсов.

Потенциальная проблема

Задача должна запускаться ежедневно в 02:30.

Но:

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

Стратегии обработки DST

Хранение в UTC

Наиболее надёжный подход:

const timestamp = DateTime.utc();

Преимущества:

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

Хранение временной зоны пользователя

Для событий календаря важно хранить:

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

Пример:

{
  time: "2025-07-10T09:00",
  zone: "Europe/Berlin"
}

Это позволяет корректно учитывать будущие переходы DST.


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

Нежелательно использовать:

UTC+2

Правильнее:

Europe/Berlin

Потому что offset меняется в зависимости от сезона.


Определение следующего перехода DST

Luxon напрямую не предоставляет API для получения даты следующего перехода, однако это можно вычислить вручную.

Пример:

let dt = DateTime.now().setZone("Europe/Berlin");

const currentOffset = dt.offset;

while (dt.offset === currentOffset) {
  dt = dt.plus({ days: 1 });
}

console.log(dt.toString());

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

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

console.log(dt.isValid);

Если зона не поддерживается:

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

console.log(invalid.isValid);

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

ISO-формат может содержать offset:

2025-07-10T10:00:00+02:00

или временную зону:

2025-07-10T10:00:00 Europe/Berlin

Luxon корректно интерпретирует DST:

const dt = DateTime.fromISO(
  "2025-07-10T10:00:00+02:00"
);

console.log(dt.toString());

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

В UTC

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

console.log(dt.toISO());

Результат:

2025-07-10T08:00:00.000Z

В локальной зоне

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

console.log(local.toISO());

Результат:

2025-07-10T10:00:00.000+02:00

Поведение startOf() и endOf() во время DST

Методы:

startOf()
endOf()

учитывают особенности временной зоны.

Пример:

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

console.log(dt.startOf("day").toString());
console.log(dt.endOf("day").toString());

Продолжительность суток может отличаться от 24 часов.


Проверка равенства моментов времени

Две даты могут иметь:

  • разный offset;
  • одинаковый UTC-момент.

Пример:

const a = DateTime.fromISO(
  "2025-07-10T10:00:00+02:00"
);

const b = DateTime.fromISO(
  "2025-07-10T08:00:00Z"
);

console.log(a.toMillis() === b.toMillis());

Результат:

true

DST и интервалы времени

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

import { Interval } from "luxon";

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

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

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

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

Результат:

23

Тестирование кода с DST

При тестировании рекомендуется:

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

Пример тестовой даты

2025-03-30

или:

2025-10-26

для Европы.


Типичные ошибки при работе с летним временем

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

Проблема:

2025-07-10 10:00

без указания зоны неоднозначно.


Использование offset вместо зоны

Плохо:

UTC+2

Хорошо:

Europe/Berlin

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

Опасный подход:

timestamp + 86400000

Из-за DST сутки могут быть:

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

Игнорирование повторяющегося времени

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

Это может вызывать:

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

Практический пример: ежедневное уведомление

Неверный подход

next = current.plus({ hours: 24 });

Из-за DST время уведомления может смещаться.

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

next = current.plus({ days: 1 });

Так сохраняется календарное локальное время.


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

const meeting = {
  title: "Standup",
  localTime: "2025-07-10T09:00",
  zone: "Europe/Berlin"
};

При отображении:

const dt = DateTime.fromISO(
  meeting.localTime,
  {
    zone: meeting.zone
  }
);

console.log(dt.toString());

После перехода на зимнее время событие всё равно останется в 09:00 локального времени.


Рекомендации по работе с DST в Luxon

  • хранить серверные даты в UTC;
  • использовать IANA-зоны;
  • избегать ручной арифметики миллисекунд;
  • различать календарные дни и часы;
  • тестировать переходы DST;
  • хранить временную зону пользователя;
  • использовать plus({ days: 1 }) для календарных событий;
  • использовать UTC для вычислений;
  • проверять isValid;
  • избегать фиксированных offset.