export type BucketUsageBucket = { tags: string[]; necessity: "NECESSARY" | "BOTH" | "UNNECESSARY"; windowDays: number; }; export type BucketUsageEntry = { amountDollars: number; occurredAt: string | Date; necessity: "NECESSARY" | "BOTH" | "UNNECESSARY"; tags: string[]; isRecurring: boolean; entryType?: "SPENDING" | "INCOME"; }; const MS_PER_DAY = 24 * 60 * 60 * 1000; function toDateOnly(value: string | Date) { if (value instanceof Date) { return new Date(Date.UTC(value.getUTCFullYear(), value.getUTCMonth(), value.getUTCDate())); } if (!value) return null; const parsed = new Date(`${value}T00:00:00Z`); if (Number.isNaN(parsed.getTime())) return null; return parsed; } function daysBetween(today: Date, occurred: Date) { return Math.floor((today.getTime() - occurred.getTime()) / MS_PER_DAY); } function hasAllTags(bucketTags: string[], entryTags: string[]) { if (!bucketTags.length) return true; if (!entryTags.length) return false; const entrySet = new Set(entryTags.map(tag => tag.toLowerCase())); return bucketTags.every(tag => entrySet.has(tag.toLowerCase())); } export function calculateBucketUsage(bucket: BucketUsageBucket, entries: BucketUsageEntry[], today: string | Date) { const windowDays = Number(bucket.windowDays || 0); if (!Number.isFinite(windowDays) || windowDays <= 0) return { totalUsage: 0, matchedCount: 0 }; const todayDate = toDateOnly(today); if (!todayDate) return { totalUsage: 0, matchedCount: 0 }; const bucketNecessity = bucket.necessity; let totalUsage = 0; let matchedCount = 0; entries.forEach(entry => { if (entry.entryType && entry.entryType !== "SPENDING") return; if (entry.isRecurring) return; if (!hasAllTags(bucket.tags || [], entry.tags || [])) return; const occurred = toDateOnly(entry.occurredAt); if (!occurred) return; const diffDays = daysBetween(todayDate, occurred); if (diffDays < 0 || diffDays > windowDays - 1) return; const entryNecessity = entry.necessity; if (bucketNecessity !== "BOTH") { if (entryNecessity === "BOTH") { totalUsage += entry.amountDollars / 2; matchedCount += 1; return; } if (entryNecessity !== bucketNecessity) return; } totalUsage += entry.amountDollars; matchedCount += 1; }); return { totalUsage, matchedCount }; }