Точные и приблизительные вычисления

В основе работы с датами в JavaScript лежит тип Date, который хранит время как количество миллисекунд, прошедших с 1 января 1970 года по UTC. Такая модель обеспечивает высокую вычислительную предсказуемость на уровне машинного времени, но накладывает ограничения на семантику календарных операций.

Ключевой момент: вся точность ограничена миллисекундами. Более мелкие единицы (микро- и наносекунды) отсутствуют, а календарные единицы (дни, месяцы, годы) не имеют фиксированной длительности.

Библиотека Date-fns строится поверх этой модели, разделяя:

  • абсолютные вычисления (точные интервалы в миллисекундах)
  • календарные вычисления (приближённые по смыслу единицы времени)

Точные вычисления: работа с абсолютной шкалой времени

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

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

Функции вида differenceIn* для малых единиц времени дают строго детерминированный результат:

  • differenceInMilliseconds
  • differenceInSeconds
  • differenceInMinutes
  • differenceInHours

Все они работают через разницу таймстампов:

const diff = dateA.getTime() - dateB.getTime();

Затем применяется целочисленное преобразование.

Пример поведения:

  • 1999 мс → 1 секунда (при differenceInSeconds, округление вниз)
  • 1000 мс → 1 секунда
  • 999 мс → 0 секунд

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


Округление как основа точных вычислений

Во всех точных функциях Date-fns используется строго определённая стратегия округления:

  • Math.trunc или эквивалент усечения к нулю
  • отсутствие «математического округления» по умолчанию

Это критически важно для предсказуемости:

differenceInSeconds(new Date(2000), new Date(0)) // 2
differenceInSeconds(new Date(1999), new Date(0)) // 1

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


Приближённые вычисления и календарная неоднозначность

Календарные единицы — дни, месяцы, годы — не имеют фиксированной длительности:

  • месяц: 28–31 день
  • год: 365 или 366 дней
  • сутки: 23–25 часов при переходе на летнее/зимнее время

Поэтому Date-fns использует календарную модель приближения, где результат зависит от правил календаря, а не от абсолютной шкалы времени.


differenceInDays и проблема «неравных суток»

Функция differenceInDays не просто делит миллисекунды на 86 400 000, а учитывает границы календарных дней.

Это приводит к особенностям:

  • переход через DST может дать «неполные сутки»
  • результат зависит от локальной временной зоны

Пример логики:

  • 1 марта 00:00 → 2 марта 00:00 ≠ всегда 86400000 мс
  • возможны отклонения на ±1 час

Таким образом, differenceInDays является приближённым календарным оператором, а не физическим измерением времени.


differenceInMonths и календарная неоднозначность

Месяцы в Date-fns обрабатываются через календарную проекцию:

  • сравниваются компоненты даты (год, месяц, день)
  • затем определяется число «пересечённых границ месяцев»

Это приводит к важному эффекту:

  • 31 января → 28 февраля = 0 или 1 месяц в зависимости от направления вычисления
  • добавление месяцев не симметрично вычитанию
differenceInMonths(new Date(2024, 2, 1), new Date(2024, 0, 31))

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


Точные и календарные операции добавления

Функции add и sub работают по календарным правилам:

  • addDays — прибавляет фиксированное количество суток (через миллисекунды)
  • addMonths — изменяет календарный месяц
  • addYears — изменяет годовой компонент

Ключевой эффект: перенос дат при переполнении месяца

add(new Date(2024, 0, 31), { months: 1 })
// результат: 29 февраля или 28 февраля (в зависимости от года)

Это не арифметика времени, а календарная нормализация.


Проблема «переполнения дня»

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

  • если целевой месяц не содержит исходного дня
  • дата «сдвигается назад» к последнему дню месяца

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


intervalToDuration и декомпозиция времени

Функция intervalToDuration возвращает структуру:

  • годы
  • месяцы
  • дни
  • часы
  • минуты
  • секунды

Она не опирается на единый масштаб времени, а последовательно «вычитает» компоненты календаря.

Алгоритм:

  1. вычисление разницы лет
  2. корректировка даты
  3. вычисление месяцев
  4. повторная нормализация
  5. переход к дням и т.д.

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


Точные интервалы vs календарные интервалы

В Date-fns существует фундаментальное разделение:

Точные (absolute):

  • миллисекунды
  • секунды
  • минуты
  • часы

Приближённые (calendar-based):

  • дни
  • месяцы
  • годы

Проблема возникает при смешивании подходов:

  • 1 месяц ≠ 30 дней
  • 1 год ≠ 365 дней
  • 1 день ≠ 86400000 мс (в DST)

Ошибки, возникающие при прямой арифметике дат

Типичные ошибки:

  1. Деление миллисекунд на фиксированные константы
  2. Предположение о равенстве всех дней
  3. Использование getTime() для календарных расчётов
  4. Игнорирование временных зон

Пример проблемного подхода:

const days = (end - start) / 86400000;

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


Корректные стратегии вычислений

1. Использование differenceIn* для целевых единиц

  • differenceInSeconds — для логики событий
  • differenceInDays — для пользовательского отображения
  • differenceInMonths — для подписок и биллинга

2. Использование интервалов вместо числовых предположений

eachDayOfInterval({ start, end })

Этот подход избегает ручного расчёта и учитывает календарные особенности.

3. Разделение задач:

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

Округление и границы интервалов

Date-fns использует строгие правила включения границ:

  • начало интервала включается
  • конец интервала зависит от функции

Это важно при:

  • подсчёте дней
  • фильтрации событий
  • построении временных рядов

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


Поведение при переходе временных зон

Хотя Date-fns не является time-zone библиотекой, поведение зависит от локальной системы:

  • локальное время влияет на startOfDay
  • DST может изменить длительность суток
  • сравнение дат может давать неожиданные результаты

Поэтому календарные вычисления всегда контекстны.


Смешивание точных и приблизительных методов

Наиболее критическая ошибка — комбинирование моделей:

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

Корректный подход:

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

Практика интерпретации результатов

Результаты Date-fns следует интерпретировать как:

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

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