Группировка по периодам

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

Группировка по периодам обычно сводится к двум операциям:

  1. разбиение диапазона дат на последовательность интервалов
  2. приведение каждой даты к «ключу периода» (день, неделя, месяц и т.д.)

Базовая концепция интервалов

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

  • eachDayOfInterval
  • eachWeekOfInterval
  • eachMonthOfInterval
  • eachYearOfInterval

Каждая из них принимает объект интервала:

import { eachDayOfInterval } from "date-fns";

const result = eachDayOfInterval({
  start: new Date(2024, 0, 1),
  end: new Date(2024, 0, 7),
});

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


Группировка по дням

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

import { startOfDay, format } from "date-fns";

const groupByDay = (items) => {
  const groups = {};

  items.forEach(({ date, value }) => {
    const key = format(startOfDay(date), "yyyy-MM-dd");

    if (!groups[key]) {
      groups[key] = [];
    }

    groups[key].push(value);
  });

  return groups;
};

Ключевой принцип — все временные метки приводятся к началу дня, что устраняет влияние часов, минут и секунд.


Группировка по неделям

Неделя в разных системах может начинаться с понедельника или воскресенья. date-fns учитывает это через параметры локали и функцию startOfWeek.

import { startOfWeek, format } from "date-fns";

const groupByWeek = (items) => {
  const groups = {};

  items.forEach(({ date, value }) => {
    const weekStart = startOfWeek(date, { weekStartsOn: 1 });
    const key = format(weekStart, "yyyy-'W'II");

    if (!groups[key]) {
      groups[key] = [];
    }

    groups[key].push(value);
  });

  return groups;
};

Использование ISO-нумерации недель позволяет избежать неоднозначности при переходе между годами.

Для более формализованного разбиения диапазона применяется:

import { eachWeekOfInterval } from "date-fns";

const weeks = eachWeekOfInterval({
  start: new Date(2024, 0, 1),
  end: new Date(2024, 2, 1),
});

Группировка по месяцам

Месячная агрегация часто используется в финансовой аналитике и отчетности. Основная идея — приведение даты к первому числу месяца.

import { startOfMonth, format } from "date-fns";

const groupByMonth = (items) => {
  const groups = {};

  items.forEach(({ date, value }) => {
    const key = format(startOfMonth(date), "yyyy-MM");

    if (!groups[key]) {
      groups[key] = [];
    }

    groups[key].push(value);
  });

  return groups;
};

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

import { eachMonthOfInterval } from "date-fns";

const months = eachMonthOfInterval({
  start: new Date(2023, 0, 1),
  end: new Date(2023, 11, 31),
});

Группировка по годам

Годовая агрегация применяется для долгосрочной статистики и тренд-анализа.

import { startOfYear, format } from "date-fns";

const groupByYear = (items) => {
  const groups = {};

  items.forEach(({ date, value }) => {
    const key = format(startOfYear(date), "yyyy");

    if (!groups[key]) {
      groups[key] = [];
    }

    groups[key].push(value);
  });

  return groups;
};

Для построения диапазонов лет:

import { eachYearOfInterval } from "date-fns";

const years = eachYearOfInterval({
  start: new Date(2015, 0, 1),
  end: new Date(2025, 0, 1),
});

Унифицированный механизм группировки

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

import {
  startOfDay,
  startOfWeek,
  startOfMonth,
  startOfYear,
  format,
} from "date-fns";

const strategies = {
  day: (d) => startOfDay(d),
  week: (d) => startOfWeek(d, { weekStartsOn: 1 }),
  month: (d) => startOfMonth(d),
  year: (d) => startOfYear(d),
};

const formatters = {
  day: "yyyy-MM-dd",
  week: "yyyy-'W'II",
  month: "yyyy-MM",
  year: "yyyy",
};

const groupByPeriod = (items, period) => {
  const normalize = strategies[period];
  const fmt = formatters[period];

  const groups = {};

  for (const { date, value } of items) {
    const key = format(normalize(date), fmt);

    if (!groups[key]) {
      groups[key] = [];
    }

    groups[key].push(value);
  }

  return groups;
};

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


Работа с календарными границами

При группировке важно учитывать границы переходов между периодами:

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

date-fns обеспечивает корректную работу с календарными переходами за счет использования локализованных правил и строгой работы с объектами Date.


Использование диапазонов и последовательностей

Комбинация each*OfInterval и группировочной логики позволяет строить полные временные сетки.

import { eachDayOfInterval, format } from "date-fns";

const buildTimeline = (start, end, data) => {
  const days = eachDayOfInterval({ start, end });

  const map = new Map(
    data.map((d) => [format(d.date, "yyyy-MM-dd"), d.value])
  );

  return days.map((day) => {
    const key = format(day, "yyyy-MM-dd");

    return {
      date: day,
      value: map.get(key) ?? 0,
    };
  });
};

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


Производственные особенности

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

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

Стабильность ключей группировки

Выбор формата ключа критически влияет на корректность агрегации:

  • yyyy-MM-dd — строгая дневная уникальность
  • yyyy-'W'II — ISO-недели без конфликтов с годами
  • yyyy-MM — стабильная месячная структура

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


Композиция периодов

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

const nestedGroup = (items) => {
  const groups = {};

  for (const { date, value } of items) {
    const year = format(startOfYear(date), "yyyy");
    const month = format(startOfMonth(date), "MM");
    const day = format(startOfDay(date), "dd");

    groups[year] ??= {};
    groups[year][month] ??= {};
    groups[year][month][day] ??= [];

    groups[year][month][day].push(value);
  }

  return groups;
};

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


Обобщение подходов

Группировка по периодам в date-fns строится на трех базовых операциях:

  • приведение даты к началу периода (startOf*)
  • форматирование ключа (format)
  • генерация диапазона (each*OfInterval)

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