'use strict'; /** * Time-bucketing helpers for financial analytics (daily / weekly / monthly / * forward-looking forecast windows). Weeks follow the Iranian convention of * starting on Saturday, ending on Friday. */ const startOfDay = (date) => { const d = new Date(date); d.setHours(0, 0, 0, 0); return d; }; const endOfDay = (date) => { const d = new Date(date); d.setHours(23, 59, 59, 999); return d; }; const addDays = (date, days) => { const d = new Date(date); d.setDate(d.getDate() + days); return d; }; const addMonths = (date, months) => { const d = new Date(date); d.setMonth(d.getMonth() + months); return d; }; /** JS getDay(): Sun=0..Sat=6. Iranian week starts Saturday, so Sat -> offset 0, Sun -> 1, ... Fri -> 6. */ const getWeekStart = (date) => { const d = startOfDay(date); const offset = (d.getDay() + 1) % 7; return addDays(d, -offset); }; /** Builds `days` daily buckets ending on (and including) `endDate`, oldest first. */ const buildDailyBuckets = (days, endDate = new Date()) => { const end = startOfDay(endDate); const buckets = []; for (let i = days - 1; i >= 0; i--) { const dayStart = addDays(end, -i); buckets.push({ start: dayStart, end: endOfDay(dayStart), key: dayStart.toISOString().slice(0, 10) }); } return buckets; }; /** Builds `weeks` weekly buckets (Sat-Fri) ending on the week containing `endDate`, oldest first. */ const buildWeeklyBuckets = (weeks, endDate = new Date()) => { const currentWeekStart = getWeekStart(endDate); const buckets = []; for (let i = weeks - 1; i >= 0; i--) { const weekStart = addDays(currentWeekStart, -7 * i); buckets.push({ start: weekStart, end: endOfDay(addDays(weekStart, 6)), key: weekStart.toISOString().slice(0, 10) }); } return buckets; }; /** Builds `weeks` forward-looking weekly buckets (Sat-Fri) starting from the week containing `startDate`. */ const buildForecastWeeklyBuckets = (weeks, startDate = new Date()) => { const currentWeekStart = getWeekStart(startDate); const buckets = []; for (let i = 0; i < weeks; i++) { const weekStart = addDays(currentWeekStart, 7 * i); buckets.push({ start: weekStart, end: endOfDay(addDays(weekStart, 6)), key: weekStart.toISOString().slice(0, 10) }); } return buckets; }; /** Builds `months` monthly (Gregorian calendar month) buckets ending on the month containing `endDate`, oldest first. */ const buildMonthlyBuckets = (months, endDate = new Date()) => { const base = new Date(endDate.getFullYear(), endDate.getMonth(), 1, 0, 0, 0, 0); const buckets = []; for (let i = months - 1; i >= 0; i--) { const monthStart = addMonths(base, -i); const monthEnd = endOfDay(addDays(addMonths(monthStart, 1), -1)); buckets.push({ start: monthStart, end: monthEnd, year: monthStart.getFullYear(), month: monthStart.getMonth() + 1, key: `${monthStart.getFullYear()}-${String(monthStart.getMonth() + 1).padStart(2, '0')}` }); } return buckets; }; /** Linear search is fine — bucket arrays used here are always small (<=~90 entries). */ const findBucketIndex = (buckets, date) => { if (!date) return -1; const d = date instanceof Date ? date : new Date(date); if (Number.isNaN(d.getTime())) return -1; const t = d.getTime(); return buckets.findIndex((b) => t >= b.start.getTime() && t <= b.end.getTime()); }; module.exports = { startOfDay, endOfDay, addDays, addMonths, getWeekStart, buildDailyBuckets, buildWeeklyBuckets, buildForecastWeeklyBuckets, buildMonthlyBuckets, findBucketIndex };