Разбиение интервалов

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

Интервал создаётся через Interval.fromDateTimes(start, end) или через вспомогательные методы:

import { DateTime, Interval } from "luxon";

const start = DateTime.fromISO("2026-01-01T00:00:00");
const end = DateTime.fromISO("2026-01-10T00:00:00");

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

Разбиение интервала представляет собой преобразование одного непрерывного диапазона в набор меньших интервалов. Формально это операция:

  • вход: один Interval
  • выход: массив Interval
  • условие: объединение результатов эквивалентно исходному интервалу (без потерь или с контролируемым остатком)

В Luxon для этого используются методы:

  • splitAt(...)
  • splitBy(...)

Каждый метод ориентирован на разные типы разбиения: по точкам разреза или по равным шагам.


Разбиение по фиксированным точкам: splitAt

Метод splitAt разделяет интервал в заданных точках времени. Эти точки должны находиться внутри интервала или на его границах.

const interval = Interval.fromDateTimes(
  DateTime.fromISO("2026-01-01T00:00:00"),
  DateTime.fromISO("2026-01-10T00:00:00")
);

const parts = interval.splitAt(
  DateTime.fromISO("2026-01-03T00:00:00"),
  DateTime.fromISO("2026-01-07T00:00:00")
);

Результатом будет массив из трёх интервалов:

  1. 1–3 января
  2. 3–7 января
  3. 7–10 января

Особенности поведения splitAt

  • точки разбиения автоматически сортируются
  • точки вне интервала игнорируются
  • дубликаты точек не влияют на результат
  • границы интервала включаются в разбиение логически, но не дублируются

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

const result = interval.splitAt(
  DateTime.fromISO("2025-12-01T00:00:00") // вне интервала
);

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


Разбиение по равным шагам: splitBy

Метод splitBy делит интервал на равные части по заданной длительности (Duration).

import { Duration } from "luxon";

const interval = Interval.fromDateTimes(
  DateTime.fromISO("2026-01-01T00:00:00"),
  DateTime.fromISO("2026-01-02T00:00:00")
);

const chunks = interval.splitBy(
  Duration.fromObject({ hours: 6 })
);

Результат — четыре интервала по 6 часов.

Механика разбиения

Алгоритм splitBy работает последовательно:

  1. берётся старт интервала
  2. добавляется шаг длительности
  3. формируется новый подинтервал
  4. процесс повторяется до достижения конца

Формально:

  • пусть t0 — начало
  • d — шаг
  • тогда границы: t0 + k*d

Неровное деление интервалов

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

const interval = Interval.fromDateTimes(
  DateTime.fromISO("2026-01-01T00:00:00"),
  DateTime.fromISO("2026-01-01T10:00:00")
);

const parts = interval.splitBy(
  Duration.fromObject({ hours: 3 })
);

Результат:

  • 00:00–03:00
  • 03:00–06:00
  • 06:00–09:00
  • 09:00–10:00 (остаток)

Это поведение важно при расчётах биллинга, где неполные интервалы должны учитываться отдельно.


Комбинированное разбиение

В реальных сценариях часто используется сочетание splitAt и splitBy. Сначала выполняется грубое разбиение по ключевым точкам, затем каждый сегмент дробится равномерно.

Пример: разделение месяца по неделям с последующим разбиением на дни:

const month = Interval.fromDateTimes(
  DateTime.fromISO("2026-01-01"),
  DateTime.fromISO("2026-02-01")
);

const weeks = month.splitBy(Duration.fromObject({ days: 7 }));

const daily = weeks.flatMap(week =>
  week.splitBy(Duration.fromObject({ days: 1 }))
);

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


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

При разбиении интервалов, содержащих переходы часовых поясов или летнего времени, возможны неоднородные сегменты.

Luxon опирается на DateTime с привязанной зоной:

const interval = Interval.fromDateTimes(
  DateTime.fromISO("2026-03-28T00:00:00", { zone: "Europe/Berlin" }),
  DateTime.fromISO("2026-03-29T00:00:00", { zone: "Europe/Berlin" })
);

При переходе на летнее время один из шагов может быть короче или длиннее стандартного часа. Это влияет на результат splitBy(Duration.fromObject({ hours: 1 })), где количество сегментов может отличаться от ожидаемого.


Пограничные случаи

Интервал нулевой длины

Если start равен end, результат разбиения — пустой набор:

const interval = Interval.fromDateTimes(dt, dt);
interval.splitBy(Duration.fromObject({ hours: 1 })); // []

Очень малые интервалы

Если шаг больше интервала, возвращается один элемент:

  • интервал 1 час
  • шаг 2 часа

Результат: [interval]


Несовпадение типов длительности

Использование Duration с неоднозначными единицами (например, месяцы) может давать переменные результаты, так как длина месяца не фиксирована:

interval.splitBy(Duration.fromObject({ months: 1 }));

В этом случае границы зависят от календарной модели DateTime.


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

При работе с большими диапазонами (например, несколько лет с часовым шагом) количество создаваемых объектов растёт линейно:

  • 1 год ≈ 8760 интервалов (при шаге 1 час)
  • память и CPU зависят от количества операций создания DateTime

Оптимизация достигается за счёт:

  • уменьшения числа промежуточных объектов
  • предварительной агрегации шагов (например, дни вместо часов)
  • отказа от лишних преобразований зон

Практические сценарии разбиения

Тарификация по времени использования

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

const session = Interval.fromDateTimes(login, logout);

const billingBlocks = session.splitBy(
  Duration.fromObject({ minutes: 15 })
);

Каждый блок затем анализируется отдельно.


Построение временных графиков

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

const range = Interval.fromDateTimes(
  DateTime.fromISO("2026-01-01"),
  DateTime.fromISO("2026-01-31")
);

const days = range.splitBy(Duration.fromObject({ days: 1 }));

Каждый день становится бакетом для статистики.


Нарезка расписаний

При планировании задач интервал разбивается на слоты:

  • 30 минут
  • 1 час
  • 2 часа
const schedule = dayInterval.splitBy(
  Duration.fromObject({ minutes: 30 })
);

Поведение при пересечениях и обрезке

Разбиение не выходит за пределы исходного интервала. Каждый подинтервал автоматически ограничивается:

  • началом исходного диапазона
  • концом исходного диапазона

Даже если шаг пересекает границу, лишняя часть отбрасывается.


Сравнение splitAt и splitBy

Характеристика splitAt splitBy
Тип разбиения по точкам по шагу
Вход DateTime Duration
Равномерность нет да
Использование события, границы сегментация, тайм-слоты

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