Начало и конец квартала

В календарной модели квартал представляет собой период из трёх месяцев, используемый для финансовой отчётности, аналитики и агрегации данных. В Moment.js работа с кварталами встроена в систему манипуляции датами и опирается на методы startOf, endOf и quarter.

Квартал в Moment.js определяется относительно стандартного календаря:

  • I квартал: январь — март
  • II квартал: апрель — июнь
  • III квартал: июль — сентябрь
  • IV квартал: октябрь — декабрь

Получение номера квартала

Moment.js позволяет получить или установить квартал с помощью метода quarter().

const m = moment('2024-05-17');

m.quarter(); // 2

Значение возвращается в диапазоне от 1 до 4.

Установка квартала изменяет дату, сохраняя день в пределах допустимого диапазона месяца:

const m = moment('2024-01-10');

m.quarter(3);

m.format('YYYY-MM-DD'); // 2024-07-10

При установке квартала библиотека автоматически переводит дату в соответствующий диапазон месяцев (в данном случае — июль, как начало III квартала).


Начало квартала

Для получения первого момента квартала используется startOf('quarter').

const m = moment('2024-08-15');

const start = m.startOf('quarter');

start.format('YYYY-MM-DD HH:mm:ss'); // 2024-07-01 00:00:00

Метод изменяет исходный объект Moment, переводя его в начало квартала:

  • дата устанавливается на первый день первого месяца квартала
  • время сбрасывается в 00:00:00.000

Особенность работы заключается в мутации объекта:

const m = moment('2024-08-15');
const start = m.startOf('quarter');

m === start; // true

Если требуется сохранить исходную дату, используется клонирование:

const m = moment('2024-08-15');

const start = m.clone().startOf('quarter');

Конец квартала

Метод endOf('quarter') переводит дату в последний момент квартала.

const m = moment('2024-02-10');

const end = m.endOf('quarter');

end.format('YYYY-MM-DD HH:mm:ss'); // 2024-03-31 23:59:59

Поведение включает:

  • установку даты на последний день третьего месяца квартала
  • установку времени на 23:59:59.999

Это важно для корректных сравнений диапазонов:

const m = moment('2024-03-31');

m.isSame(moment('2024-03-31').endOf('quarter')); // true

Комбинация startOf и endOf для диапазонов

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

const m = moment('2024-11-12');

const start = m.clone().startOf('quarter');
const end = m.clone().endOf('quarter');

console.log(start.format('YYYY-MM-DD'));
console.log(end.format('YYYY-MM-DD'));

Результат:

  • 2024-10-01
  • 2024-12-31

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

const start = moment().quarter(4).startOf('quarter');
const end = moment().quarter(4).endOf('quarter');

db.query({
  createdAt: {
    $gte: start.toDate(),
    $lte: end.toDate()
  }
});

Внутренняя логика определения квартала

Moment.js вычисляет квартал на основе месяца:

quarter = Math.floor(month / 3) + 1

где month — индекс месяца от 0 до 11.

Соответствие:

  • 0–2 → 1 квартал
  • 3–5 → 2 квартал
  • 6–8 → 3 квартал
  • 9–11 → 4 квартал

Это объясняет поведение метода quarter() при установке значений: библиотека пересчитывает месяц, исходя из номера квартала и текущего месяца внутри квартала.


Особенности мутации при работе с кварталами

Методы startOf и endOf изменяют исходный объект. Это критично при цепочках вычислений:

const m = moment('2024-06-15');

const start = m.startOf('quarter');
const end = m.endOf('quarter');

console.log(m.format('YYYY-MM-DD')); // 2024-06-30

После вызова startOf('quarter') дата становится началом квартала, а последующий endOf('quarter') продолжает трансформацию уже изменённого объекта.

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

const m = moment('2024-06-15');

const start = m.clone().startOf('quarter');
const end = m.clone().endOf('quarter');

Работа с UTC и кварталами

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

const m = moment.utc('2024-10-10');

const start = m.clone().startOf('quarter');
const end = m.clone().endOf('quarter');

Разница между moment() и moment.utc() проявляется в смещениях дат при переходах через границы суток в локальных зонах, но сам квартал остаётся календарно идентичным.


Применение в аналитике

Кварталы часто используются для агрегирования метрик:

const now = moment();

const ranges = [];

for (let i = 0; i < 4; i++) {
  const start = now.clone().quarter(i + 1).startOf('quarter');
  const end = now.clone().quarter(i + 1).endOf('quarter');

  ranges.push({
    quarter: i + 1,
    start: start.toISOString(),
    end: end.toISOString()
  });
}

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


Сравнение дат внутри квартала

Проверка принадлежности даты текущему кварталу:

const date = moment('2024-05-20');

const start = moment(date).startOf('quarter');
const end = moment(date).endOf('quarter');

date.isBetween(start, end, null, '[]'); // true

Ключевой момент — включение границ диапазона через '[]', что важно для точной финансовой отчётности.


Смещение при нестандартных календарях

Moment.js не поддерживает произвольные финансовые кварталы без дополнительных решений. Если финансовый год начинается, например, с апреля, стандартная логика quarter() становится неприменимой напрямую.

В таких случаях используется ручная нормализация:

function getFiscalQuarter(date) {
  const month = date.month(); // 0-11
  return Math.floor(((month + 9) % 12) / 3) + 1;
}

Для границ квартала также требуется явное вычисление:

function startOfFiscalQuarter(date) {
  const quarter = getFiscalQuarter(date);
  const fiscalStartMonth = ((quarter - 1) * 3 + 3) % 12;

  return moment(date)
    .month(fiscalStartMonth)
    .startOf('month');
}

Форматирование границ квартала

Полученные даты часто приводятся к строковому виду для API:

const start = moment().startOf('quarter').format('YYYY-MM-DD');
const end = moment().endOf('quarter').format('YYYY-MM-DD');

Форматирование зависит от требований системы, но чаще используется ISO-формат или дата без времени.


Сравнение кварталов между датами

Определение разницы в кварталах:

const a = moment('2024-01-15');
const b = moment('2024-10-10');

b.diff(a, 'quarters'); // 3

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