Обработка граничных случаев

Работа с датой и временем редко ограничивается только созданием объекта DateTime. На практике основная сложность возникает в граничных ситуациях: переходы между часовыми поясами, летнее время, невалидные даты, неоднозначные локальные значения времени и особенности календарных вычислений.

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


Невалидные даты

Luxon не выбрасывает исключение при создании некорректной даты. Вместо этого объект получает состояние invalid.

import { DateTime } from "luxon";

const dt = DateTime.fromObject({
  year: 2025,
  month: 2,
  day: 30
});

console.log(dt.isValid); // false

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

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

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

unit out of range
you specified 30 (of type number) as a day, which is invalid

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

При работе с пользовательским вводом проверка isValid обязательна.

const dt = DateTime.fromISO(userInput);

if (!dt.isValid) {
  console.error("Некорректная дата");
} else {
  console.log(dt.toISO());
}

Игнорирование проверки может привести к распространению invalid-объектов по приложению.


Несуществующее локальное время

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

Например, в часовом поясе America/New_York время:

2025-03-09 02:30

не существует, потому что после 01:59 сразу наступает 03:00.

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

console.log(dt.toString());

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


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

При переходе с летнего времени обратно один и тот же локальный момент может существовать дважды.

Пример:

2025-11-02 01:30

в America/New_York возникает два раза:

  • до перевода часов
  • после перевода часов

Luxon использует правила IANA timezone database для определения корректного смещения.

const dt = DateTime.fromISO(
  "2025-11-02T01:30",
  {
    zone: "America/New_York"
  }
);

console.log(dt.offset);

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

Наиболее безопасный способ хранения временных значений — использование UTC.

const createdAt = DateTime.utc();

console.log(createdAt.toISO());

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

const local = createdAt.setZone("Asia/Almaty");

console.log(local.toString());

Такой подход предотвращает:

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

Потеря часового пояса при сериализации

Строка без timezone-информации создаёт неоднозначность.

Плохой вариант:

2025-05-10T15:00:00

Хороший вариант:

2025-05-10T15:00:00Z

или:

2025-05-10T15:00:00+06:00

Luxon корректно распознаёт оба формата.

DateTime.fromISO("2025-05-10T15:00:00Z");
DateTime.fromISO("2025-05-10T15:00:00+06:00");

Проблемы конца месяца

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

const dt = DateTime.fromISO("2025-01-31");

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

console.log(result.toISODate());

Результат:

2025-02-28

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


Добавление дней и месяцев — разные операции

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

const dt = DateTime.fromISO("2025-01-31");

console.log(
  dt.plus({ days: 30 }).toISODate()
);

console.log(
  dt.plus({ months: 1 }).toISODate()
);

Результат отличается:

2025-03-02
2025-02-28

Причина:

  • days — арифметическое смещение
  • months — календарное смещение

Високосные годы

Luxon учитывает високосные годы автоматически.

const dt = DateTime.fromISO("2024-02-29");

console.log(dt.isValid);

Проверка:

console.log(dt.daysInMonth);
console.log(dt.daysInYear);
console.log(dt.isInLeapYear);

Границы суток

Конец суток — частый источник ошибок.

Проблемный код:

const end = dt.endOf("day");

Результат:

23:59:59.999

При сравнении диапазонов возможны ошибки округления.

Надёжнее использовать полуоткрытые интервалы:

[start, nextDay)

Пример:

const start = dt.startOf("day");
const next = start.plus({ days: 1 });

if (value >= start && value < next) {
  // значение входит в диапазон
}

Сравнение объектов DateTime

Сравнивать объекты через === нельзя.

const a = DateTime.now();
const b = a.plus({});

console.log(a === b); // false

Корректный вариант:

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

или:

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

Проверка принадлежности диапазону

Luxon содержит класс Interval.

import { Interval } from "luxon";

const start = DateTime.now();
const end = start.plus({ hours: 2 });

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

console.log(
  interval.contains(DateTime.now())
);

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

const a = Interval.fromDateTimes(
  DateTime.fromISO("2025-01-01"),
  DateTime.fromISO("2025-01-10")
);

const b = Interval.fromDateTimes(
  DateTime.fromISO("2025-01-05"),
  DateTime.fromISO("2025-01-15")
);

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

Обработка больших временных промежутков

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

const start = DateTime.fromISO("2020-01-01");
const end = DateTime.now();

const diff = end.diff(start, [
  "years",
  "months",
  "days"
]);

console.log(diff.toObject());

Ошибки при ручной работе с offset

Смещение (offset) не равно часовому поясу.

Например:

UTC+3

может соответствовать разным регионам.

Неправильно:

DateTime.now().setZone("UTC+3");

Правильно:

DateTime.now().setZone("Europe/Moscow");

или:

DateTime.now().setZone("Asia/Almaty");

Изменение timezone без изменения времени

Метод setZone() по умолчанию сохраняет момент времени.

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

const local = dt.setZone("Asia/Tokyo");

console.log(local.toString());

Время изменится, но момент останется тем же.


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

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

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

const shifted = dt.setZone(
  "Asia/Tokyo",
  { keepLocalTime: true }
);

console.log(shifted.toString());

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


Ошибки парсинга нестандартных форматов

Некорректный шаблон приводит к invalid-значению.

const dt = DateTime.fromFormat(
  "31-12-2025",
  "yyyy/MM/dd"
);

console.log(dt.isValid);

Правильный шаблон:

DateTime.fromFormat(
  "31-12-2025",
  "dd-MM-yyyy"
);

Локализация и различия календарей

Разные локали влияют на:

  • формат дат
  • первый день недели
  • правила отображения
const dt = DateTime.now()
  .setLocale("ru");

console.log(
  dt.toLocaleString(DateTime.DATE_FULL)
);

Начало недели в разных странах

const us = DateTime.now()
  .setLocale("en-US");

const ru = DateTime.now()
  .setLocale("ru");

console.log(us.startOf("week").weekday);
console.log(ru.startOf("week").weekday);

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


Работа с Unix timestamp

Luxon использует миллисекунды.

DateTime.fromMillis(1740000000000);

Unix timestamp в секундах требует отдельного метода.

DateTime.fromSeconds(1740000000);

Ошибка единиц измерения приводит к некорректным датам.


Ограничения JavaScript Date

Luxon построен поверх Date, поэтому наследует некоторые ограничения платформы:

  • ограниченный диапазон дат
  • зависимость от системной timezone database
  • различия между средами выполнения

Поведение в Node.js и браузере

Timezone database зависит от окружения.

Одинаковый код может работать по-разному:

  • в Chrome
  • в Firefox
  • в Node.js
  • в Docker-контейнере

Особенно это заметно при работе с историческими timezone-правилами.


Проверка поддержки timezone

console.log(
  Intl.DateTimeFormat().resolvedOptions()
);

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

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

  1. Хранение дат в UTC
  2. Использование ISO 8601
  3. Отображение времени только через timezone пользователя
  4. Проверка isValid
  5. Исключение ручных вычислений timestamp
  6. Использование Interval для диапазонов
  7. Минимизация работы с локальным временем
  8. Явное указание timezone при парсинге

Тестирование граничных случаев

При тестировании необходимо проверять:

  • конец месяца
  • високосные годы
  • переходы DST
  • разные timezone
  • начало и конец суток
  • отрицательные интервалы
  • большие диапазоны дат

Пример теста:

const dt = DateTime.fromISO(
  "2024-02-29"
);

expect(dt.isValid).toBe(true);

Проверка переходов DST

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

const after = before.plus({ hours: 1 });

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

Результат покажет скачок времени через DST-границу.


Отрицательные интервалы

const start = DateTime.now();
const end = start.minus({ days: 1 });

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

console.log(interval.isValid);

Luxon считает такой интервал невалидным.


Безопасное сравнение дат

function isExpired(expiresAt) {
  const dt = DateTime.fromISO(expiresAt);

  return dt.isValid &&
    dt < DateTime.now();
}

Лучше:

function isExpired(expiresAt) {
  const dt = DateTime.fromISO(expiresAt);

  return dt.isValid &&
    dt.toMillis() < DateTime.now().toMillis();
}

Обработка null и undefined

function parseDate(value) {
  if (!value) {
    return null;
  }

  const dt = DateTime.fromISO(value);

  return dt.isValid ? dt : null;
}

Иммутабельность объектов Luxon

Все объекты Luxon неизменяемы.

const original = DateTime.now();

const updated = original.plus({
  days: 1
});

console.log(original.toISO());
console.log(updated.toISO());

Это предотвращает множество ошибок состояния.


Типичные ошибки при миграции с Date

Частая ошибка:

const dt = DateTime.now();

dt.plus({ days: 1 });

console.log(dt.toISO());

Результат не изменится, потому что требуется сохранить новый объект.

Правильно:

const updated = dt.plus({
  days: 1
});