Разница в месяцах и годах

Общая модель вычисления разницы во времени

В библиотеке date-fns вычисление разницы между датами строится вокруг календарной логики, а не абсолютного количества миллисекунд. Это означает, что месяцы и годы интерпретируются как календарные единицы, а не как фиксированное число дней.

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

  • differenceInMonths / differenceInYears — вычисление полных интервалов
  • differenceInCalendarMonths / differenceInCalendarYears — календарная разница по границам периодов

Ключевая идея:

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


Разница в месяцах

differenceInMonths

Функция вычисляет количество полных месяцев между двумя датами.

import { differenceInMonths } from "date-fns";

const result = differenceInMonths(
  new Date(2024, 5, 15),
  new Date(2024, 2, 15)
);

console.log(result); // 3

Принцип работы

Алгоритм:

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

Важный момент

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

differenceInMonths(
  new Date(2024, 2, 31),
  new Date(2024, 1, 28)
);

Результат может отличаться от интуитивного ожидания, так как учитывается календарная корректность, а не «30 дней = месяц».


differenceInCalendarMonths

Функция возвращает разницу по календарным месяцам без учёта «полноты» периода.

import { differenceInCalendarMonths } from "date-fns";

const result = differenceInCalendarMonths(
  new Date(2024, 5, 15),
  new Date(2024, 2, 31)
);

console.log(result); // 3

Отличие от differenceInMonths

Функция Логика Особенность
differenceInMonths только полные месяцы учитывает неполные периоды
differenceInCalendarMonths разница календарных месяцев игнорирует «полноту»

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

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

differenceInMonths(A, B) === -differenceInMonths(B, A)

Это правило делает поведение симметричным.


Типичные сценарии использования

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

Разница в годах

differenceInYears

Функция вычисляет количество полных лет между датами.

import { differenceInYears } from "date-fns";

const result = differenceInYears(
  new Date(2030, 0, 1),
  new Date(2025, 0, 1)
);

console.log(result); // 5

Логика вычисления

  • сравниваются годовые компоненты дат
  • учитываются только завершённые 12-месячные циклы
  • месяцы и дни влияют на «дозревание» полного года

Пример с неполным годом

differenceInYears(
  new Date(2025, 11, 31),
  new Date(2024, 0, 1)
);

Результат может быть 1 или 0 в зависимости от того, завершился ли полный годовой цикл по календарным правилам.


differenceInCalendarYears

Возвращает разницу именно по календарным годам.

import { differenceInCalendarYears } from "date-fns";

const result = differenceInCalendarYears(
  new Date(2030, 6, 1),
  new Date(2025, 11, 31)
);

console.log(result); // 5

Отличие от differenceInYears

Функция Поведение
differenceInYears считает только полные годы
differenceInCalendarYears считает смену календарных лет

Сравнение месяцев и лет в date-fns

Иерархия единиц

  • 1 год = 12 календарных месяцев
  • месяц = переменная длина (28–31 день)

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

  • арифметической моделью (миллисекунды)
  • календарной моделью (даты и границы периодов)

Ключевая разница семантики

Полные интервалы

differenceInMonths, differenceInYears

  • возвращают только завершённые периоды
  • зависят от дня месяца и даты окончания

Календарные интервалы

differenceInCalendarMonths, differenceInCalendarYears

  • ориентированы на смену календарных единиц
  • менее чувствительны к «дню внутри месяца»

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

Разные длины месяцев

Февраль делает вычисления неоднозначными:

differenceInMonths(
  new Date(2024, 2, 31),
  new Date(2024, 1, 29)
);

Високосные и невисокосные годы могут давать разные результаты.


Переход через конец года

differenceInMonths(
  new Date(2025, 0, 1),
  new Date(2024, 11, 31)
);

Несмотря на разницу в 1 день, результат может быть 0, так как полный месяц не завершён.


Обратный порядок дат

differenceInYears(A, B)
differenceInYears(B, A)

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


Практические стратегии выбора функции

Когда использовать differenceInMonths

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

Когда использовать differenceInCalendarMonths

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

Когда использовать differenceInYears

  • стаж работы
  • возраст (в полных годах)
  • долгосрочные финансовые расчёты

Когда использовать differenceInCalendarYears

  • группировка по годам (например, 2020, 2021, 2022)
  • аналитика исторических данных
  • сравнение событий по календарным годам

Взаимосвязь с другими функциями date-fns

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

  • addMonths, subMonths
  • addYears, subYears
  • startOfMonth, endOfMonth
  • startOfYear, endOfYear

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


Поведение при временных зонах и времени суток

Хотя месяцы и годы логически не зависят от времени суток, в JavaScript возможны пограничные эффекты:

  • переходы через DST
  • различия UTC и локального времени
  • смещения при создании даты через Date(year, month, day)

date-fns нормализует такие ситуации через внутреннюю работу с Date, но логика остаётся календарной.


Итоговая модель различий

  • месяцы и годы в date-fns всегда считаются как календарные сущности
  • differenceIn* — строгий учёт завершённых периодов
  • differenceInCalendar* — ориентир на смену календарных единиц
  • выбор функции определяется не математикой, а бизнес-смыслом расчёта