Объединение диапазонов

В работе с временными данными в приложениях часто возникает задача приведения набора интервалов к компактному виду. Такие интервалы могут описывать занятость ресурсов, бронирования, окна выполнения задач, периоды активности пользователей. При накоплении большого количества диапазонов неизбежно появляются пересечения и смежные отрезки, которые требуется объединять в единые непрерывные блоки.

В экосистеме js-joda для подобных задач используется модуль расширений с типом Interval, позволяющий моделировать диапазоны времени с высокой точностью и поддержкой строгой типизации временных точек.


Интервал в js-joda задаётся двумя границами: началом и концом. В качестве точек используются типы Instant, LocalDate или LocalDateTime в зависимости от контекста задачи.

Базовая структура интервала:

  • начало включается в диапазон
  • конец не включается (полуоткрытый интервал)

Такой подход упрощает операции сравнения и объединения, исключая неоднозначность на границах.

import { Instant } from '@js-joda/core'
import { Interval } from '@js-joda/extra'

const start = Instant.parse('2026-01-01T10:00:00Z')
const end   = Instant.parse('2026-01-01T12:00:00Z')

const interval = Interval.of(start, end)

Логика пересечения и смежности

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

Основные отношения между интервалами:

  • пересечение: диапазоны имеют общую часть
  • смежность: конец одного совпадает с началом другого
  • разрыв: между интервалами существует промежуток

Эти состояния определяют возможность объединения.

const a = Interval.of(
  Instant.parse('2026-01-01T10:00:00Z'),
  Instant.parse('2026-01-01T12:00:00Z')
)

const b = Interval.of(
  Instant.parse('2026-01-01T11:30:00Z'),
  Instant.parse('2026-01-01T13:00:00Z')
)

const overlaps = a.overlaps(b)
const abuts = a.abuts(b)
const connected = a.isConnected(b)

Метод isConnected учитывает как пересечение, так и смежность, что критично для последующего объединения.


Базовое объединение двух интервалов

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

const a = Interval.of(
  Instant.parse('2026-01-01T10:00:00Z'),
  Instant.parse('2026-01-01T12:00:00Z')
)

const b = Interval.of(
  Instant.parse('2026-01-01T11:00:00Z'),
  Instant.parse('2026-01-01T13:30:00Z')
)

const merged = a.union(b)

Полученный интервал:

  • начало: 10:00
  • конец: 13:30

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


Объединение набора интервалов

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

Шаг 1. Сортировка по началу

const sorted = intervals.sort((i1, i2) =>
  i1.start().compareTo(i2.start())
)

Шаг 2. Итеративное слияние

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

const result = []

for (const current of sorted) {
  if (result.length === 0) {
    result.push(current)
    continue
  }

  const last = result[result.length - 1]

  if (last.isConnected(current)) {
    const merged = last.union(current)
    result[result.length - 1] = merged
  } else {
    result.push(current)
  }
}

Объединение перекрывающихся и смежных диапазонов

Особенность модели Interval заключается в том, что смежные интервалы рассматриваются как потенциально объединяемые. Это позволяет устранять микропромежутки, возникающие из-за дискретности данных.

Пример набора:

  • [10:00 — 11:00]
  • [11:00 — 12:00]
  • [12:30 — 13:00]

После обработки:

  • [10:00 — 12:00]
  • [12:30 — 13:00]

Первый и второй интервалы объединяются за счёт смежности границ.


Проверка необходимости объединения

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

  • overlaps — пересечение
  • abuts — касание границ
  • isConnected — обобщённая связность
if (a.isConnected(b)) {
  const merged = a.union(b)
}

Такой подход снижает количество лишних вычислений и упрощает ветвление логики.


Нормализация сложных наборов

При большом количестве интервалов возможны цепочки объединений, когда результат одного слияния влияет на последующие проверки.

Пример:

  • A: 09:00 — 10:00
  • B: 10:00 — 11:00
  • C: 11:00 — 12:00

После последовательного применения объединения формируется один интервал:

  • 09:00 — 12:00

Алгоритмически это достигается за счёт постоянного сравнения текущего интервала с последним элементом результата.


Работа с локальными датами

При использовании LocalDate диапазоны описывают календарные периоды без учёта времени суток.

import { LocalDate } from '@js-joda/core'
import { Interval } from '@js-joda/extra'

const d1 = LocalDate.parse('2026-01-01')
const d2 = LocalDate.parse('2026-01-10')

const interval = Interval.of(d1.atStartOfDay(), d2.atStartOfDay())

При объединении таких интервалов логика остаётся идентичной, однако важно учитывать календарные особенности, включая переходы между месяцами и годами.


Граничные случаи

При объединении диапазонов необходимо учитывать:

  • полностью вложенные интервалы
  • касание в одной точке
  • отсутствие пересечения
  • цепочки пересечений через промежуточные интервалы

Вложенные интервалы не расширяют общий диапазон:

const outer = Interval.of(
  Instant.parse('2026-01-01T10:00:00Z'),
  Instant.parse('2026-01-01T15:00:00Z')
)

const inner = Interval.of(
  Instant.parse('2026-01-01T11:00:00Z'),
  Instant.parse('2026-01-01T12:00:00Z')
)

const merged = outer.union(inner)

Результат сохраняет границы outer.


Оптимизация обработки больших массивов

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

  • сортировка: O(n log n)
  • объединение: O(n)

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


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

Объединение диапазонов применяется при:

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

Во всех случаях ключевым этапом становится приведение множества перекрывающихся интервалов к минимальному набору непрерывных блоков, что упрощает последующую обработку и анализ данных.