fiddy/apps/web/lib/shared/bucket-usage.ts
2026-02-11 23:45:15 -08:00

77 lines
2.5 KiB
TypeScript

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 };
}