Работа с датой и временем редко ограничивается только созданием
объекта 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.
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) {
// значение входит в диапазон
}
Сравнивать объекты через === нельзя.
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) не равно часовому поясу.
Например:
UTC+3
может соответствовать разным регионам.
Неправильно:
DateTime.now().setZone("UTC+3");
Правильно:
DateTime.now().setZone("Europe/Moscow");
или:
DateTime.now().setZone("Asia/Almaty");
Метод 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);
Результаты могут отличаться.
Luxon использует миллисекунды.
DateTime.fromMillis(1740000000000);
Unix timestamp в секундах требует отдельного метода.
DateTime.fromSeconds(1740000000);
Ошибка единиц измерения приводит к некорректным датам.
Luxon построен поверх Date, поэтому наследует некоторые
ограничения платформы:
Timezone database зависит от окружения.
Одинаковый код может работать по-разному:
Особенно это заметно при работе с историческими timezone-правилами.
console.log(
Intl.DateTimeFormat().resolvedOptions()
);
Практика промышленной разработки обычно строится на нескольких правилах:
isValidInterval для диапазоновПри тестировании необходимо проверять:
Пример теста:
const dt = DateTime.fromISO(
"2024-02-29"
);
expect(dt.isValid).toBe(true);
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();
}
function parseDate(value) {
if (!value) {
return null;
}
const dt = DateTime.fromISO(value);
return dt.isValid ? dt : null;
}
Все объекты Luxon неизменяемы.
const original = DateTime.now();
const updated = original.plus({
days: 1
});
console.log(original.toISO());
console.log(updated.toISO());
Это предотвращает множество ошибок состояния.
Частая ошибка:
const dt = DateTime.now();
dt.plus({ days: 1 });
console.log(dt.toISO());
Результат не изменится, потому что требуется сохранить новый объект.
Правильно:
const updated = dt.plus({
days: 1
});