/** * 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, }; }