Date Utilities
Date utilities handle period selection, range computation, interval generation, and date formatting for statistics.
Key Files
| File | Purpose |
|---|---|
app/components/date/utils.ts | Core utilities: toDuration, getStartOf, getEndOf, interval computation |
app/components/date/types.ts | Types: Period, Range, StatDateParams, IntervalsInRangeProps |
app/components/date/statDateParams.ts | Range computation from params, query param parsing |
app/components/date/useStatDate.ts | Composable: reactive date state with localStorage persistence |
app/components/stat/date/useGetDateRange.ts | Human-readable date labels ("Today", "Last 3 months", "Mar - Jun") |
app/components/stat/intervals.ts | Transaction bucketing into intervals, averages |
Period System
All date operations use the Period union type:
const periods = ['day', 'week', 'month', 'year'] as const
type Period = typeof periods[number]
Three core helpers convert Period into date-fns operations:
toDuration(period, value) // Period -> Duration ({ days: 3 }, { months: 1 })
getStartOf(date, period) // Period -> start boundary (startOfMonth, startOfWeek, ...)
getEndOf(date, period) // Period -> end boundary (endOfMonth, endOfWeek, ...)
All three use exhaustive switch - adding a new period variant causes a compile error, not silent fallback.
Type-safe Duration
toDuration replaces dynamic property keys with a proper Duration return type:
// Before: loses type safety, TS sees Record<string, number>
sub(date, { [`${period}s`]: value })
// After: returns typed Duration
sub(date, toDuration(period, value))
Date Range Computation
computeDateRange() resolves StatDateParams into a concrete { start, end } range:
Priority:
1. customDate (calendar picker) -> return as-is
2. isShowMaxRange -> maxRange.start to end-of-current-period
3. isShowMaxRange + isSkipEmpty -> maxRange as-is
4. calculated -> from Date.now() using rangeBy/rangeDuration/rangeOffset
The range is a Vue computed that uses Date.now() as the base. Since Date.now() is not reactive, the range updates only when params or maxRange change (not automatically at midnight). Page refresh or any param change triggers recomputation.
Interval Generation
getIntervalsInRange() splits a date range into sub-intervals:
range: Mar 1 - Mar 31, intervalsBy: 'week'
-> [Mar 1-2, Mar 3-9, Mar 10-16, Mar 17-23, Mar 24-30, Mar 31]
Edge intervals are clamped to the range boundaries.
Performance
Intervals are collected with push() then reverse() instead of unshift() in a loop. unshift shifts all elements on every call (O(n^2) for the loop), while push + reverse is O(n).
Transaction Bucketing
bucketTrnsByIntervals() assigns transactions to intervals using binary search:
// O(N log M) where N = transactions, M = intervals
for each transaction:
binary search intervals by date
push into matching bucket
Intervals are sorted and non-overlapping, making binary search valid. Transactions outside any interval are silently skipped.
Date Formatting
useGetDateRange() produces human-readable labels. Each format function returns the complete string directly (no start/end halves to concatenate):
duration=1, current period -> "Today", "This Month", "This Year"
duration=1, previous period -> "Yesterday", "Last Month", "Last Year"
duration=1, same year -> "15 March"
duration>1, current period -> "Last 3 days", "Last 6 months"
otherwise -> "Mar - Jun", "10-15 Mar 2024"
Period matching uses isSamePeriod(date1, date2, period) - a single helper replacing repeated switch blocks.
Query Param Parsing
parseStatDateQueryParams() merges URL query params into StatDateParams using Zod validation. All fields use !== undefined checks (not falsy) so that rangeOffset=0 and intervalSelected=0 are applied correctly.
Average Computation
computeAverageTotal() computes per-day/week/month averages for a date range. Returns undefined for single-day ranges or zero sums (early exit before computing intervals). The day average is always present when the function returns a result (range >= 2 days guaranteed), so no empty-object check is needed.
Next Steps
- Architecture - project structure and store pattern
- Technical Decisions - rationale behind key design choices