Работа с отрицательными интервалами

В Date-fns многие функции работают с интервалами в строгом формате { start, end }, где подразумевается, что начало периода не позже его конца. При нарушении этого условия появляется так называемый «перевёрнутый интервал» — ситуация, когда start > end. На уровне прикладной логики это соответствует отрицательному интервалу, хотя формально в JavaScript даты не имеют знака, а знак проявляется только в разности.

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

Типичный пример некорректного интервала:

const interval = {
  start: new Date(2024, 10, 10),
  end: new Date(2024, 9, 10)
}

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


Причины появления отрицательных интервалов

Обратные интервалы возникают не как ошибка времени, а как следствие бизнес-логики:

  • пользователь вводит даты в произвольном порядке;
  • вычисление разности дат без проверки порядка;
  • инверсия диапазона при фильтрации;
  • арифметика с датами через subDays, subMonths, subYears;
  • получение границ диапазона из разных источников данных.

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


Разница между направленной и абсолютной временной разностью

В Date-fns функции вычисления разности ведут себя по-разному в зависимости от направления аргументов.

import { differenceInDays } from 'date-fns'

a - b

Если a < b, результат становится отрицательным:

differenceInDays(new Date(2024, 9, 1), new Date(2024, 9, 10))
// → -9

Такое поведение отражает направленность временной оси. Отрицательное значение здесь — не ошибка, а сигнал о порядке дат.

Однако существуют функции, которые возвращают абсолютные значения, теряя направление:

  • intervalToDuration
  • некоторые формы форматирования длительности

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


Интервалы {start, end} и требования к порядку

Многие функции Date-fns, работающие с диапазонами, ожидают корректный интервал:

import { eachDayOfInterval } from 'date-fns'
eachDayOfInterval({
  start: new Date(2024, 9, 1),
  end: new Date(2024, 9, 10)
})

При обратном порядке:

eachDayOfInterval({
  start: new Date(2024, 9, 10),
  end: new Date(2024, 9, 1)
})

поведение становится некорректным или приводит к ошибке валидации диапазона.

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

import { isBefore } from 'date-fns'

function normalizeInterval(interval) {
  if (isBefore(interval.start, interval.end)) {
    return interval
  }
  return {
    start: interval.end,
    end: interval.start
  }
}

Сравнение дат как основа обработки направления

Функции isBefore, isAfter, compareAsc, compareDesc используются для определения направления интервала.

import { isBefore, isAfter } from 'date-fns'

Логика определения отрицательного интервала сводится к простому условию:

  • если start позже end, интервал «перевёрнут»
isBefore(start, end) === false

Такая проверка лежит в основе большинства нормализующих алгоритмов.


Арифметика дат и накопление отрицательных смещений

При работе с функциями добавления и вычитания времени формируется цепочка смещений:

import { addDays, subDays } from 'date-fns'
const a = new Date(2024, 9, 10)
const b = subDays(a, 5)

Если затем перепутать порядок:

differenceInDays(b, a)

получается отрицательная разность.

Здесь важно различать:

  • отрицательный интервал как структуру данных;
  • отрицательную разность как результат вычисления.

Первый — ошибка модели представления, второй — корректное математическое значение.


Потеря знака в intervalToDuration

Функция intervalToDuration возвращает объект длительности, разбивая интервал на компоненты:

import { intervalToDuration } from 'date-fns'
intervalToDuration({
  start: new Date(2024, 0, 1),
  end: new Date(2024, 0, 10)
})

Результат:

{
  years: 0,
  months: 0,
  days: 9,
  ...
}

Однако при обратном порядке:

intervalToDuration({
  start: new Date(2024, 0, 10),
  end: new Date(2024, 0, 1)
})

результат остаётся тем же по абсолютной величине. Направление полностью теряется.

Это критическое свойство: длительность перестаёт быть ориентированной.


Модель хранения направления интервала

Чтобы избежать потери информации, используется расширенная структура:

const interval = {
  start: new Date(2024, 9, 10),
  end: new Date(2024, 9, 1),
  direction: 'backward'
}

или альтернативный подход — всегда нормализованный интервал + отдельный флаг:

const normalized = normalizeInterval(interval)

const direction =
  interval.start.getTime() <= interval.end.getTime()
    ? 'forward'
    : 'backward'

Так сохраняется двойственная природа данных:

  • канонический интервал для вычислений;
  • направление для бизнес-логики.

Ошибки обработки отрицательных интервалов

На практике часто возникают следующие проблемы:

1. Непредсказуемые результаты обхода интервала

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

2. Ложные отрицательные длительности

При использовании differenceIn* создаётся ощущение, что интервал «отрицательный», хотя на самом деле отрицательна только разность порядка аргументов.

3. Неконсистентность UI-логики

Отображение диапазонов дат может меняться местами, если не применять нормализацию перед форматированием.


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

Базовый подход заключается в единой функции нормализации:

import { min, max } from 'date-fns'

function canonicalInterval(a, b) {
  return {
    start: min([a, b]),
    end: max([a, b])
  }
}

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


Смешанные сценарии с частично пересекающимися диапазонами

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

  • пересечение интервалов в обратном порядке;
  • вычитание одного диапазона из другого;
  • сортировка интервалов по времени начала.

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


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

Корректная работа с Date-fns в условиях отрицательных интервалов опирается на три принципа:

  • разделение «данных» и «направления»;
  • нормализация всех входных диапазонов;
  • использование функций разности только как направленных операторов, а не как описания структуры интервала.

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