75 lines
2.4 KiB
TypeScript
75 lines
2.4 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[];
|
|
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 (!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 };
|
|
}
|