/** * 报表周期导航(P4):单一 anchor 日期 + 周期类型,替代周/月/年三套独立状态。 * 全部本地时区字符串运算,避免 toISOString 的 UTC 偏移。 */ import { toDateString } from './decimal'; export type ReportPeriod = 'weekly' | 'monthly' | 'annual'; function parse(dateStr: string): Date { const [y, m, d] = dateStr.split('-').map(Number); return new Date(y, m - 1, d); } /** 平移 anchor:周 ±7 天;月 ±N(月末日钳制);年 ±N。 */ export function shiftAnchor(anchor: string, period: ReportPeriod, delta: number): string { const d = parse(anchor); if (period === 'weekly') { d.setDate(d.getDate() + 7 * delta); } else if (period === 'monthly') { const day = d.getDate(); d.setDate(1); d.setMonth(d.getMonth() + delta); // 月末钳制:目标月天数不足时退到月末 const lastDay = new Date(d.getFullYear(), d.getMonth() + 1, 0).getDate(); d.setDate(Math.min(day, lastDay)); } else { d.setFullYear(d.getFullYear() + delta); } return toDateString(d); } /** anchor 所在周期的起止日期(含两端)。周 = 周一~周日。 */ export function periodRange(anchor: string, period: ReportPeriod): { start: string; end: string } { const d = parse(anchor); if (period === 'weekly') { const day = d.getDay(); // 0=周日 const diffToMonday = day === 0 ? -6 : 1 - day; const monday = new Date(d); monday.setDate(d.getDate() + diffToMonday); const sunday = new Date(monday); sunday.setDate(monday.getDate() + 6); return { start: toDateString(monday), end: toDateString(sunday) }; } if (period === 'monthly') { const start = new Date(d.getFullYear(), d.getMonth(), 1); const end = new Date(d.getFullYear(), d.getMonth() + 1, 0); return { start: toDateString(start), end: toDateString(end) }; } return { start: `${d.getFullYear()}-01-01`, end: `${d.getFullYear()}-12-31` }; } /** ISO-8601 周数(周一开头)。 */ export function isoWeekNumber(dateStr: string): number { const date = parse(dateStr); date.setDate(date.getDate() + 3 - ((date.getDay() + 6) % 7)); const week1 = new Date(date.getFullYear(), 0, 4); return 1 + Math.round(((date.getTime() - week1.getTime()) / 86400000 - 3 + ((week1.getDay() + 6) % 7)) / 7); }