/**
* Wylicza ocenę wymaganą z kolejnego wpisu, aby osiągnąć docelową średnią ważoną.
*
* Wzór:
* wymagana = (docelowa × (Σw + wₙ) − Σ(oᵢ×wᵢ)) / wₙ
*
* Wynik jest przycinany (clamp) do skali 1–6. Flagi:
* - `achievable: false` gdy wynik matematyczny > 6 (cel niemożliwy)
* - gdy wynik ≤ 1, cel jest już gwarantowany nawet przy najniższej ocenie
*
* @param currentEntries - dotychczasowe oceny z wagami
* @param targetAverage - docelowa średnia ważona
* @param nextWeight - waga kolejnej (planowanej) oceny; musi być > 0
*/
export function targetGrade(
currentEntries: GradeWeightEntry[],
targetAverage: number,
nextWeight: number,
): TargetGradeResult {
if (nextWeight <= 0) {
throw new Error("Waga kolejnej oceny musi być większa od zera.");
}
if (currentEntries.some((e) => e.weight < 0)) {
throw new Error("Waga nie może być ujemna.");
}
const currentWeightSum = currentEntries.reduce((s, e) => s + e.weight, 0);
const currentWeightedSum = currentEntries.reduce(
(s, e) => s + e.grade * e.weight,
0,
);
const rawRequired =
(targetAverage * (currentWeightSum + nextWeight) - currentWeightedSum) /
nextWeight;
// Cel już osiągnięty / przekroczony przy istniejących ocenach
// (nawet ocena 1 nie obniży średniej poniżej celu).
if (currentWeightSum > 0) {
const currentAvg = weightedAverage(currentEntries);
const avgIfMin = weightedAverage([
...currentEntries,
{ grade: POLISH_GRADE_MIN, weight: nextWeight },
]);
if (avgIfMin >= targetAverage) {
return {
requiredGrade: POLISH_GRADE_MIN,
achievable: true,
note: `Cel ${targetAverage} jest już gwarantowany (bieżąca średnia: ${formatNum(currentAvg)}). Wystarczy ocena ${POLISH_GRADE_MIN}.`,
};
}
}
if (rawRequired > POLISH_GRADE_MAX) {
return {
requiredGrade: POLISH_GRADE_MAX,
achievable: false,
note: `Cel ${targetAverage} jest matematycznie niemożliwy — wymagana ocena ${formatNum(rawRequired)} przekracza maksimum skali (${POLISH_GRADE_MAX}).`,
};
}
if (rawRequired <= POLISH_GRADE_MIN) {
return {
requiredGrade: POLISH_GRADE_MIN,
achievable: true,
note: `Cel ${targetAverage} jest już gwarantowany — wystarczy ocena ${POLISH_GRADE_MIN} (wyliczone: ${formatNum(rawRequired)}).`,
};
}
const clamped = Math.min(
POLISH_GRADE_MAX,
Math.max(POLISH_GRADE_MIN, rawRequired),
);
return {
requiredGrade: clamped,
achievable: true,
note: `Aby osiągnąć średnią ${targetAverage}, potrzebujesz oceny ${formatNum(clamped)}.`,
};
}