Повторяющиеся события

В Luxon отсутствует отдельный высокоуровневый механизм cron-подобных расписаний. Повторяющиеся события формируются через комбинацию DateTime, арифметики дат и, при необходимости, Interval. Такой подход обеспечивает гибкость и предсказуемость, поскольку каждое следующее значение вычисляется явно, без скрытого состояния планировщика.

Основная идея построения повторяемости заключается в итеративном смещении даты с использованием методов plus, minus, а также нормализации через startOf и endOf.


Базовая модель генерации последовательностей дат

Повторяющаяся последовательность строится от стартовой точки:

import { DateTime } from "luxon";

const start = DateTime.local(2026, 1, 1);

Дальнейшая генерация выполняется путем последовательного добавления временных единиц:

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

Каждый вызов plus возвращает новый экземпляр DateTime, что исключает мутации и упрощает построение цепочек событий.

Ключевая особенность: все операции являются чистыми и детерминированными.


Итеративная генерация повторяющихся событий

Наиболее распространенный способ построения повторяемости — цикл с накоплением значений:

const start = DateTime.local(2026, 1, 1);

const events = [];

let current = start;

for (let i = 0; i < 10; i++) {
  events.push(current);
  current = current.plus({ days: 1 });
}

Такая модель используется для ежедневных, еженедельных и произвольных периодов.


Ежедневные и фиксированные интервалы

Ежедневные события формируются через инкремент days: 1:

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

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

current = current.plus({ days: 3 });

или

current = current.plus({ hours: 6 });

Такая схема применяется для:

  • журналирования событий
  • генерации расписаний уведомлений
  • построения временных рядов

Использование Interval для ограничения диапазона

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

import { Interval } from "luxon";

const start = DateTime.local(2026, 1, 1);
const end = DateTime.local(2026, 1, 10);

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

Хотя Interval не генерирует повторяемость напрямую, он используется как ограничитель при итерации:

const events = [];

let current = start;

while (interval.contains(current)) {
  events.push(current);
  current = current.plus({ days: 1 });
}

Повторяющиеся события по неделям

Для еженедельных событий используется шаг weeks:

let current = DateTime.local(2026, 1, 1);

const events = [];

for (let i = 0; i < 8; i++) {
  events.push(current);
  current = current.plus({ weeks: 1 });
}

Фиксация дня недели достигается через нормализацию:

const monday = current.set({ weekday: 1 });

Значение weekday позволяет принудительно установить день недели в пределах текущей недели.


Генерация событий по конкретному дню недели

Распространённый сценарий — события каждый понедельник:

let current = DateTime.local(2026, 1, 1).set({ weekday: 1 });

const events = [];

for (let i = 0; i < 12; i++) {
  events.push(current);
  current = current.plus({ weeks: 1 });
}

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


Месячные повторения

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

События фиксируются по дню месяца:

let current = DateTime.local(2026, 1, 15);

const events = [];

for (let i = 0; i < 6; i++) {
  events.push(current);
  current = current.plus({ months: 1 });
}

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


Последний день месяца

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

const lastDay = DateTime.local(2026, 2, 1).endOf("month");

Генерация серии последних дней месяцев:

let current = DateTime.local(2026, 1, 1).endOf("month");

const events = [];

for (let i = 0; i < 6; i++) {
  events.push(current);
  current = current.plus({ months: 1 }).endOf("month");
}

«N-й день недели месяца»

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

Пример: второй вторник месяца.

function nthWeekdayOfMonth(year, month, weekday, n) {
  let date = DateTime.local(year, month, 1);

  let count = 0;

  while (date.month === month) {
    if (date.weekday === weekday) {
      count++;
      if (count === n) return date;
    }
    date = date.plus({ days: 1 });
  }
}

Рабочие дни и исключение выходных

Фильтрация выходных реализуется через проверку weekday:

function isWeekend(date) {
  return date.weekday === 6 || date.weekday === 7;
}

Генерация последовательности рабочих дней:

const events = [];

let current = DateTime.local(2026, 1, 1);

while (events.length < 10) {
  if (!isWeekend(current)) {
    events.push(current);
  }
  current = current.plus({ days: 1 });
}

Повторяемость с шагом в часы и минуты

Часовые интервалы используются для задач планирования и логирования:

let current = DateTime.local(2026, 1, 1, 8, 0);

const events = [];

for (let i = 0; i < 12; i++) {
  events.push(current);
  current = current.plus({ hours: 2 });
}

Аналогично для минут:

current = current.plus({ minutes: 15 });

Работа с часовыми поясами

Повторяющиеся события должны учитывать фиксированную зону времени:

const base = DateTime.fromObject(
  { year: 2026, month: 1, day: 1, hour: 10 },
  { zone: "Europe/Berlin" }
);

При генерации важно сохранять зону:

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

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


Нормализация временных значений

Использование startOf и endOf стабилизирует границы событий:

const dayStart = DateTime.local().startOf("day");
const monthEnd = DateTime.local().endOf("month");

В повторяющихся сценариях это применяется для устранения дрейфа времени:

current = current.plus({ days: 1 }).startOf("day");

Ограничение количества повторений

При генерации последовательностей всегда задаётся явное ограничение:

const events = [];

let current = DateTime.local(2026, 1, 1);

while (events.length < 20) {
  events.push(current);
  current = current.plus({ days: 1 });
}

Альтернативный подход — использование диапазона через сравнение:

while (current < end) {
  current = current.plus({ days: 1 });
}

Сложные комбинированные расписания

Комбинации условий формируют гибкие расписания:

  • каждые 2 дня, кроме выходных
  • каждый понедельник и пятницу
  • первые 10 минут каждого часа
  • последний рабочий день месяца

Пример комбинированной логики:

let current = DateTime.local(2026, 1, 1);

const events = [];

while (events.length < 15) {
  if (current.weekday <= 5) {
    events.push(current);
  }
  current = current.plus({ days: 1 });
}

Особенности точности и иммутабельности

Каждое значение DateTime является неизменяемым объектом. Это означает:

  • отсутствуют побочные эффекты
  • безопасно использовать в функциональных цепочках
  • удобно хранить в массивах без копирования
const a = DateTime.local();
const b = a.plus({ days: 1 });

a остаётся неизменным независимо от операций над b.


Типовые ошибки при построении повторяемости

Часто возникающие проблемы:

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

Пример дрейфа:

current = current.plus({ minutes: 1440 });

Корректнее:

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