115 lines
3.6 KiB
TypeScript
115 lines
3.6 KiB
TypeScript
/**
|
||
* 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 };
|
||
}
|
||
|
||
/**
|
||
* Återställ en antagandeprofil genom att ta bort ett specifikt sessions-id
|
||
* från historiken och räkna om medelvärdena deterministiskt från resterande
|
||
* observationer. Ren funktion – alla beroenden är explicita argument.
|
||
*/
|
||
export function rollbackCookingAssumptionProfile(
|
||
sessionId: string,
|
||
existing: ExistingProfile,
|
||
): {
|
||
averageEatenPortions: number | null;
|
||
averageLeftoverPortions: number | null;
|
||
observationCount: number;
|
||
lastSessionAnswers: Array<{ sessionId: string; eaten: number; leftovers: number; date: string }>;
|
||
} {
|
||
const history = (existing.lastSessionAnswers ?? []).filter((a) => a.sessionId !== sessionId);
|
||
|
||
if (history.length === 0) {
|
||
return {
|
||
averageEatenPortions: null,
|
||
averageLeftoverPortions: null,
|
||
observationCount: 0,
|
||
lastSessionAnswers: [],
|
||
};
|
||
}
|
||
|
||
// Räkna om EMA från scratch i kronologisk ordning (äldst först).
|
||
const chronological = [...history].reverse();
|
||
let avgEaten = chronological[0]!.eaten;
|
||
let avgLeftovers = chronological[0]!.leftovers;
|
||
|
||
for (let i = 1; i < chronological.length; i++) {
|
||
const alpha = 1 / (i + 1);
|
||
const a = chronological[i]!;
|
||
avgEaten = round2(avgEaten + alpha * (a.eaten - avgEaten));
|
||
avgLeftovers = round2(avgLeftovers + alpha * (a.leftovers - avgLeftovers));
|
||
}
|
||
|
||
return {
|
||
averageEatenPortions: avgEaten,
|
||
averageLeftoverPortions: avgLeftovers,
|
||
observationCount: history.length,
|
||
lastSessionAnswers: history,
|
||
};
|
||
}
|
||
|
||
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;
|
||
}
|