53 lines
1.8 KiB
TypeScript
53 lines
1.8 KiB
TypeScript
/**
|
|
* Pantry Forecast (spec §40): "Ni brukar använda mjölk var sjätte dag."
|
|
* Enkel deterministisk konsumtionstakt ur transaktionshistorik.
|
|
* Prognoser märks ALLTID som prognoser i UI.
|
|
*/
|
|
|
|
export interface ConsumptionEvent {
|
|
/** ISO-datum */
|
|
date: string;
|
|
quantity: number;
|
|
}
|
|
|
|
export interface ForecastResult {
|
|
/** Genomsnittlig förbrukning per dag (i postens enhet). */
|
|
dailyRate: number;
|
|
/** Dagar tills nuvarande saldo beräknas ta slut. */
|
|
daysUntilEmpty: number | null;
|
|
/** Föreslaget inköpsdatum (2 dagars marginal). */
|
|
suggestedRestockDate: string | null;
|
|
confidence: "low" | "medium" | "high";
|
|
}
|
|
|
|
export function forecastDepletion(
|
|
currentBalance: number,
|
|
consumption: ConsumptionEvent[],
|
|
today: Date = new Date(),
|
|
): ForecastResult {
|
|
if (consumption.length < 2) {
|
|
return { dailyRate: 0, daysUntilEmpty: null, suggestedRestockDate: null, confidence: "low" };
|
|
}
|
|
const sorted = [...consumption].sort((a, b) => a.date.localeCompare(b.date));
|
|
const first = Date.parse(sorted[0]!.date);
|
|
const last = Date.parse(sorted[sorted.length - 1]!.date);
|
|
const spanDays = Math.max(1, (last - first) / 86_400_000);
|
|
const total = sorted.reduce((sum, e) => sum + Math.abs(e.quantity), 0);
|
|
const dailyRate = total / spanDays;
|
|
|
|
if (dailyRate <= 0) {
|
|
return { dailyRate: 0, daysUntilEmpty: null, suggestedRestockDate: null, confidence: "low" };
|
|
}
|
|
|
|
const daysUntilEmpty = currentBalance / dailyRate;
|
|
const restock = new Date(today.getTime() + Math.max(0, daysUntilEmpty - 2) * 86_400_000);
|
|
const confidence = consumption.length >= 6 ? "high" : consumption.length >= 4 ? "medium" : "low";
|
|
|
|
return {
|
|
dailyRate: Math.round(dailyRate * 1000) / 1000,
|
|
daysUntilEmpty: Math.round(daysUntilEmpty * 10) / 10,
|
|
suggestedRestockDate: restock.toISOString().slice(0, 10),
|
|
confidence,
|
|
};
|
|
}
|