Fas 3 steg 3b: minimala efterfrågor + hushållsantagandeprofiler per ingrediens
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Antagandeprofiler för matlagning (Fas 3 §6.2).
|
||||
* Rensa, deterministiska funktioner för att uppdatera per-hushålls-per-ingrediens-profiler
|
||||
* utifrån faktiska svar efter en cooking session.
|
||||
*/
|
||||
|
||||
export interface ProfileUpdateInput {
|
||||
householdId: string;
|
||||
canonicalIngredientId: string;
|
||||
plannedPortions: number;
|
||||
actualPortionsEaten: number;
|
||||
leftoverEstimatePortions: number;
|
||||
sessionId: string;
|
||||
date: string;
|
||||
}
|
||||
|
||||
export interface ExistingProfile {
|
||||
averageEatenPortions: number | null;
|
||||
averageLeftoverPortions: number | null;
|
||||
observationCount: number;
|
||||
lastSessionAnswers?: Array<{ sessionId: string; eaten: number; leftovers: number; date: string }> | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Beräkna nya profilvärden med exponentiellt glidande medelvärde.
|
||||
* alpha = 1 / (observationCount + 1) ger jämn viktning över tid.
|
||||
*/
|
||||
export function updateCookingAssumptionProfile(
|
||||
input: ProfileUpdateInput,
|
||||
existing: ExistingProfile,
|
||||
): {
|
||||
averageEatenPortions: number;
|
||||
averageLeftoverPortions: number;
|
||||
observationCount: number;
|
||||
lastSessionAnswers: Array<{ sessionId: string; eaten: number; leftovers: number; date: string }>;
|
||||
} {
|
||||
const eaten = clamp(input.actualPortionsEaten, 0, input.plannedPortions);
|
||||
const leftovers = clamp(input.leftoverEstimatePortions, 0, input.plannedPortions - eaten);
|
||||
|
||||
const prevEaten = existing.averageEatenPortions ?? eaten;
|
||||
const prevLeftovers = existing.averageLeftoverPortions ?? leftovers;
|
||||
const prevCount = existing.observationCount ?? 0;
|
||||
|
||||
const alpha = 1 / (prevCount + 1);
|
||||
const averageEatenPortions = round2(prevEaten + alpha * (eaten - prevEaten));
|
||||
const averageLeftoverPortions = round2(prevLeftovers + alpha * (leftovers - prevLeftovers));
|
||||
const observationCount = prevCount + 1;
|
||||
|
||||
const history = existing.lastSessionAnswers ?? [];
|
||||
const lastSessionAnswers = [
|
||||
{ sessionId: input.sessionId, eaten, leftovers, date: input.date },
|
||||
...history,
|
||||
].slice(0, 10);
|
||||
|
||||
return { averageEatenPortions, averageLeftoverPortions, observationCount, lastSessionAnswers };
|
||||
}
|
||||
|
||||
function clamp(n: number, min: number, max: number): number {
|
||||
return Math.max(min, Math.min(max, n));
|
||||
}
|
||||
|
||||
function round2(n: number): number {
|
||||
return Math.round(n * 100) / 100;
|
||||
}
|
||||
@@ -6,3 +6,4 @@ export * from "./forecast.js";
|
||||
export * from "./trust.js";
|
||||
export * from "./reconciliation.js";
|
||||
export * from "./scan-diff.js";
|
||||
export * from "./cooking-profiles.js";
|
||||
|
||||
Reference in New Issue
Block a user