Open Finapp
Разработка

Дата-утилиты

Вычисление диапазонов, форматирование периодов и распределение транзакций по интервалам.

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

Ключевые файлы

ФайлНазначение
app/components/date/utils.tsБазовые утилиты: toDuration, getStartOf, getEndOf, вычисление интервалов
app/components/date/types.tsТипы: Period, Range, StatDateParams, IntervalsInRangeProps
app/components/date/statDateParams.tsВычисление диапазона из параметров, парсинг query-параметров
app/components/date/useStatDate.tsComposable: реактивное состояние дат с персистенцией в localStorage
app/components/stat/date/useGetDateRange.tsЧеловекочитаемые подписи ("Сегодня", "Последние 3 месяца", "Мар - Июн")
app/components/stat/intervals.tsРаспределение транзакций по интервалам, средние значения

Система периодов

Все операции с датами используют union-тип Period:

const periods = ['day', 'week', 'month', 'year'] as const
type Period = typeof periods[number]

Три базовых хелпера преобразуют Period в операции date-fns:

toDuration(period, value)   // Period -> Duration ({ days: 3 }, { months: 1 })
getStartOf(date, period)    // Period -> начало периода (startOfMonth, startOfWeek, ...)
getEndOf(date, period)      // Period -> конец периода (endOfMonth, endOfWeek, ...)

Все три используют exhaustive switch - добавление нового варианта периода вызовет ошибку компиляции, а не молчаливый fallback.

Типобезопасный Duration

toDuration заменяет динамические ключи свойств типизированным возвратом Duration:

// Раньше: теряет типобезопасность, TS видит Record<string, number>
sub(date, { [`${period}s`]: value })

// Сейчас: возвращает типизированный Duration
sub(date, toDuration(period, value))

Вычисление диапазона дат

computeDateRange() преобразует StatDateParams в конкретный { start, end }:

Приоритет:
1. customDate (выбор в календаре)      -> возвращается как есть
2. isShowMaxRange                      -> maxRange.start до конца текущего периода
3. isShowMaxRange + isSkipEmpty        -> maxRange как есть
4. вычисленный                         -> от Date.now() по rangeBy/rangeDuration/rangeOffset

Диапазон - это Vue computed, использующий Date.now() как базу. Поскольку Date.now() не является реактивной зависимостью, диапазон обновляется только при изменении params или maxRange (не автоматически в полночь). Обновление страницы или любое изменение параметров запускает перевычисление.

Генерация интервалов

getIntervalsInRange() разбивает диапазон дат на подинтервалы:

range: 1 мар - 31 мар, intervalsBy: 'week'
-> [1-2 мар, 3-9 мар, 10-16 мар, 17-23 мар, 24-30 мар, 31 мар]

Крайние интервалы обрезаются по границам диапазона.

Производительность

Интервалы собираются через push() с последующим reverse() вместо unshift() в цикле. unshift сдвигает все элементы при каждом вызове (O(n^2) на весь цикл), тогда как push + reverse работает за O(n).

Распределение транзакций

bucketTrnsByIntervals() распределяет транзакции по интервалам бинарным поиском:

// O(N log M), где N = транзакции, M = интервалы
for each transaction:
  бинарный поиск по интервалам
  добавление в нужный бакет

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

Форматирование дат

useGetDateRange() создает человекочитаемые подписи. Каждая функция форматирования возвращает полную строку (без разделения на "начало"/"конец" с последующей конкатенацией):

duration=1, текущий период   -> "Сегодня", "Этот месяц", "Этот год"
duration=1, предыдущий       -> "Вчера", "Прошлый месяц", "Прошлый год"
duration=1, этот год         -> "15 марта"
duration>1, текущий период   -> "Последние 3 дня", "Последние 6 месяцев"
остальные                    -> "Мар - Июн", "10-15 мар 2024"

Сравнение периодов использует isSamePeriod(date1, date2, period) - один хелпер вместо повторяющихся switch-блоков.

Парсинг query-параметров

parseStatDateQueryParams() мержит URL query-параметры в StatDateParams через Zod-валидацию. Все поля проверяются через !== undefined (не falsy), чтобы rangeOffset=0 и intervalSelected=0 применялись корректно.

Вычисление средних

computeAverageTotal() считает средние за день/неделю/месяц для диапазона дат. Возвращает undefined для однодневных диапазонов или нулевых сумм (ранний выход до вычисления интервалов). Среднее за день всегда присутствует при возврате результата (диапазон >= 2 дней гарантирован), поэтому проверка на пустой объект не нужна.

Далее