Обратный отсчет

Основой обратного отсчёта в Luxon служит работа с двумя сущностями: моментом времени (DateTime) и промежутком (Duration). Вместо ручных вычислений разницы через Date и миллисекунды библиотека предоставляет высокоуровневые операции, учитывающие часовые пояса, локали и корректное поведение при переходе через границы суток, месяцев и лет.

Любой таймер обратного отсчёта сводится к вычислению разницы между текущим моментом и целевой датой:

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

Luxon предоставляет для этого класс DateTime:

import { DateTime } from "luxon";

const target = DateTime.fromISO("2026-12-31T23:59:59");
const now = DateTime.now();

const diff = target.diff(now);

diff возвращает объект Duration, содержащий разницу между моментами времени в нескольких единицах измерения одновременно.

Получение обратного отсчёта в удобных единицах

Объект Duration можно привести к нужным единицам через нормализацию:

const diff = target.diff(DateTime.now(), ["days", "hours", "minutes", "seconds"]);

Результат уже структурирован: дни, часы, минуты и секунды доступны напрямую:

diff.days;
diff.hours;
diff.minutes;
diff.seconds;

Однако важно учитывать, что Duration в Luxon хранит дробные значения, поэтому для отображения обычно выполняется округление:

const seconds = Math.floor(diff.seconds);

Полноценный таймер с интервалом обновления

Обратный отсчёт в реальном времени строится на регулярном пересчёте:

import { DateTime } from "luxon";

const target = DateTime.fromISO("2026-12-31T23:59:59");

setInterval(() => {
  const now = DateTime.now();
  const diff = target.diff(now, ["days", "hours", "minutes", "seconds"]);

  const days = Math.floor(diff.days);
  const hours = Math.floor(diff.hours);
  const minutes = Math.floor(diff.minutes);
  const seconds = Math.floor(diff.seconds);

  console.log(`${days}d ${hours}h ${minutes}m ${seconds}s`);
}, 1000);

Такая реализация уже функциональна, но имеет одну особенность: интервал 1000 мс не гарантирует синхронизацию с системным временем, что приводит к накоплению дрейфа.

Устранение дрейфа времени

JavaScript-таймеры не являются точными часами. Более стабильный подход — вычислять оставшееся время каждый тик строго от текущего момента до цели, не полагаясь на инкременты:

setInterval(() => {
  const diff = target.diff(DateTime.now(), ["days", "hours", "minutes", "seconds"]);

  const totalSeconds = Math.max(0, diff.as("seconds"));
  const days = Math.floor(totalSeconds / 86400);
  const hours = Math.floor((totalSeconds % 86400) / 3600);
  const minutes = Math.floor((totalSeconds % 3600) / 60);
  const seconds = Math.floor(totalSeconds % 60);

  console.log({ days, hours, minutes, seconds });
}, 1000);

Использование diff.as("seconds") позволяет получить абсолютное количество секунд и избежать накопления ошибок округления между тиками.

Работа с отрицательными значениями

Если целевая дата уже прошла, diff становится отрицательным. Luxon сохраняет это поведение, что полезно для аналитики, но неудобно для отображения таймера.

Обычно вводится защита:

const diff = target.diff(DateTime.now(), ["days", "hours", "minutes", "seconds"]);

const total = diff.as("seconds");

if (total <= 0) {
  console.log("Время истекло");
} else {
  console.log(Math.floor(total));
}

Использование Duration для форматирования

Duration можно преобразовать в человекочитаемую форму:

const diff = target.diff(DateTime.now());
const duration = diff.shiftTo("days", "hours", "minutes", "seconds");

console.log(duration.toObject());

Метод shiftTo перераспределяет значение в удобные единицы, устраняя избыточные доли.

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

{
  days: 12,
  hours: 5,
  minutes: 40,
  seconds: 10.532
}

Это особенно важно для UI, где требуется аккуратное отображение компонентов времени.

Учёт временных зон

Luxon изначально ориентирован на работу с временными зонами через DateTime:

const target = DateTime.fromISO("2026-12-31T23:59:59", {
  zone: "Europe/Moscow"
});

Текущий момент также зависит от локальной зоны окружения:

const now = DateTime.now();

Если таймер должен быть глобально согласованным (например, сервер + клиенты), используется UTC:

const target = DateTime.fromISO("2026-12-31T23:59:59Z");
const now = DateTime.utc();

Использование UTC исключает расхождения из-за локальных смещений.

Пересчёт с фиксированной целью и динамическим временем

В типичной архитектуре целевая дата фиксируется один раз, а текущее время постоянно обновляется:

const target = DateTime.fromISO("2026-12-31T23:59:59Z");

function getRemaining() {
  const now = DateTime.utc();
  const diff = target.diff(now, ["hours", "minutes", "seconds"]);
  return diff;
}

Такой подход делает таймер детерминированным: любое количество вызовов функции даёт согласованный результат.

Форматирование строки обратного отсчёта

Luxon не навязывает формат вывода, но предоставляет базовые инструменты для локализации:

const diff = target.diff(DateTime.now()).shiftTo("days", "hours", "minutes", "seconds");

const d = Math.floor(diff.days);
const h = Math.floor(diff.hours);
const m = Math.floor(diff.minutes);
const s = Math.floor(diff.seconds);

const formatted =
  String(d).padStart(2, "0") + ":" +
  String(h).padStart(2, "0") + ":" +
  String(m).padStart(2, "0") + ":" +
  String(s).padStart(2, "0");

Такой формат часто используется в интерфейсах обратного отсчёта.

Использование until через Interval

Хотя Luxon не имеет прямого метода “countdown”, логически его заменяет Interval:

import { Interval, DateTime } from "luxon";

const start = DateTime.now();
const end = DateTime.fromISO("2026-12-31T23:59:59");

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

Интервал можно пересчитывать динамически, но чаще он используется как фиксированная модель диапазона времени.

Оптимизация обновлений

Частое обновление DOM или консоли может быть избыточным. Обычно оптимизируют:

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

Синхронизация с границей секунды:

const delay = 1000 - DateTime.now().millisecond;

setTimeout(() => {
  setInterval(update, 1000);
}, delay);

Это снижает визуальные скачки таймера.

Поведение при смене системного времени

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

Для критически точных сценариев (например, серверные таймеры) используется серверное время как источник истины:

const serverNow = DateTime.fromMillis(serverTimestamp);
const diff = target.diff(serverNow);

Дробные секунды и точность отображения

По умолчанию Luxon сохраняет точность до миллисекунд. Это может приводить к «дрожанию» значений при отображении:

diff.seconds

Для стабильного UI обычно используется явное округление:

Math.floor(diff.seconds);

или даже полное игнорирование миллисекунд:

const diff = target.diff(DateTime.now()).set({ milliseconds: 0 });

Интеграция с реактивными UI

В реактивных системах логика таймера выносится в вычисляемое состояние:

function getCountdown(targetISO) {
  const target = DateTime.fromISO(targetISO);

  return function tick() {
    const diff = target.diff(DateTime.now(), ["days", "hours", "minutes", "seconds"]);
    return {
      days: Math.floor(diff.days),
      hours: Math.floor(diff.hours),
      minutes: Math.floor(diff.minutes),
      seconds: Math.floor(diff.seconds)
    };
  };
}

Такой подход позволяет абстрагировать таймер от конкретного механизма рендера.

Согласование разных форматов даты

Luxon поддерживает множество входных форматов:

DateTime.fromISO(...)
DateTime.fromRFC2822(...)
DateTime.fromHTTP(...)
DateTime.fromMillis(...)

Для обратного отсчёта важно, чтобы целевая дата была строго нормализована:

const target = DateTime.fromMillis(Date.parse("2026-12-31T23:59:59Z"));

Неправильный парсинг может сместить таймер на несколько часов из-за локальной зоны.

Стабильная модель таймера

Концептуально корректная модель обратного отсчёта в Luxon всегда строится на трёх принципах:

  • фиксация целевого DateTime;
  • получение текущего DateTime на каждом шаге;
  • вычисление Duration через diff без накопительных состояний.

Любые попытки хранить «остаток времени» как изменяемую переменную приводят к рассинхронизации при долгом выполнении и дрейфу, который постепенно накапливается даже при идеальных интервалах.