- Extract computeSubScores() and computeWarnings() to variety.ts - 18 unit tests covering formulas, boundaries, clamping, edge cases: - proteinDiversity: repeats × 2 penalty, clamped to 0 - ingredientOverlap: overlaps × 1.5 penalty, clamped to 0 - effortBalance: easy-hard diff × 1.5, total=0 → 10 - warnings: repeat≥2 days, overlap≥2 days, duplicates Addresses QA blockers: untested business logic in sub-score derivations. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
89 lines
2.1 KiB
TypeScript
89 lines
2.1 KiB
TypeScript
interface TagRepeat {
|
|
tagType?: string;
|
|
tagName?: string;
|
|
days?: string[];
|
|
}
|
|
|
|
interface IngredientOverlap {
|
|
ingredientName?: string;
|
|
days?: string[];
|
|
}
|
|
|
|
interface SubScoreInput {
|
|
tagRepeats: TagRepeat[];
|
|
ingredientOverlaps: IngredientOverlap[];
|
|
easy: number;
|
|
medium: number;
|
|
hard: number;
|
|
}
|
|
|
|
export interface SubScores {
|
|
proteinDiversity: number;
|
|
ingredientOverlap: number;
|
|
effortBalance: number;
|
|
}
|
|
|
|
export function computeSubScores(input: SubScoreInput): SubScores {
|
|
const { tagRepeats, ingredientOverlaps, easy, medium, hard } = input;
|
|
|
|
const proteinRepeats = tagRepeats.filter((t) => t.tagType === 'protein').length;
|
|
const ingredientOverlapCount = ingredientOverlaps.length;
|
|
const total = easy + medium + hard;
|
|
|
|
const effortBalance =
|
|
total === 0
|
|
? 10
|
|
: Math.min(10, Math.round(Math.max(0, 10 - Math.abs(easy - hard) * 1.5)));
|
|
|
|
return {
|
|
proteinDiversity: Math.max(0, Math.round(10 - proteinRepeats * 2)),
|
|
ingredientOverlap: Math.max(0, Math.round(10 - ingredientOverlapCount * 1.5)),
|
|
effortBalance
|
|
};
|
|
}
|
|
|
|
interface WarningInput {
|
|
tagRepeats: TagRepeat[];
|
|
ingredientOverlaps: IngredientOverlap[];
|
|
duplicatesInPlan: string[];
|
|
}
|
|
|
|
export interface Warning {
|
|
title: string;
|
|
explanation: string;
|
|
}
|
|
|
|
export function computeWarnings(input: WarningInput): Warning[] {
|
|
const { tagRepeats, ingredientOverlaps, duplicatesInPlan } = input;
|
|
const result: Warning[] = [];
|
|
|
|
for (const repeat of tagRepeats) {
|
|
if ((repeat.days?.length ?? 0) > 1) {
|
|
const days = (repeat.days ?? []).join(', ');
|
|
result.push({
|
|
title: `${repeat.tagName} mehrfach diese Woche`,
|
|
explanation: `${days} — erwäge einen Tausch für mehr Protein-Abwechslung.`
|
|
});
|
|
}
|
|
}
|
|
|
|
for (const overlap of ingredientOverlaps) {
|
|
if ((overlap.days?.length ?? 0) > 1) {
|
|
const days = (overlap.days ?? []).join(', ');
|
|
result.push({
|
|
title: `${overlap.ingredientName} in mehreren Gerichten`,
|
|
explanation: `${days} — sorge für Zutaten-Abwechslung.`
|
|
});
|
|
}
|
|
}
|
|
|
|
for (const name of duplicatesInPlan) {
|
|
result.push({
|
|
title: `${name} doppelt geplant`,
|
|
explanation: 'Dasselbe Rezept erscheint mehrfach — tausche eines aus.'
|
|
});
|
|
}
|
|
|
|
return result;
|
|
}
|