Jak liczymy

Poniżej znajdziesz proste wyjaśnienia i wzory używane w naszych kalkulatorach. Kod programu jest ukryty w sekcjach „dla programistów” — wciągany automatycznie z plików źródłowych przy każdym buildzie, więc nie może rozjechać się z tym, co naprawdę liczy strona.

Zasady przejrzystości

  • Funkcje są czyste (bez Reacta i bez side-effectów) i pokryte testami jednostkowymi.
  • Przeliczanie ocen z plusem/minusem zależy od lokalnej praktyki szkoły (WSO/PSO) — dlatego udostępniamy kilka zestawów reguł.
  • Konwersja GPA oraz progi stypendialne to przybliżenia / dane weryfikowane osobno — nie zastępują oficjalnego regulaminu uczelni.

Wzory i metody

Średnia ważona

Każdą ocenę mnożymy przez jej wagę, dodajemy te iloczyny, a wynik dzielimy przez sumę wszystkich wag.

Wzór

Średnia ważona = (ocena₁ × waga₁ + ocena₂ × waga₂ + … + ocenaₙ × wagaₙ) ÷ (waga₁ + waga₂ + … + wagaₙ)

Zobacz kod źródłowy (dla programistów)

lib/calculations/weightedAverage.tsweightedAverage()

lib/calculations/weightedAverage.ts
/**
 * Oblicza średnią ważoną ocen.
 *
 * Wzór: średnia = Σ(ocenaᵢ × wagaᵢ) / Σ(wagaᵢ)
 *
 * @param entries - lista par { ocena, waga }
 * @returns średnia ważona
 * @throws gdy lista jest pusta lub suma wag wynosi 0
 */
export function weightedAverage(entries: GradeWeightEntry[]): number {
  if (entries.length === 0) {
    throw new Error("Nie można obliczyć średniej ważonej z pustej listy.");
  }

  let weightedSum = 0;
  let totalWeight = 0;

  for (const entry of entries) {
    if (entry.weight < 0) {
      throw new Error("Waga nie może być ujemna.");
    }
    weightedSum += entry.grade * entry.weight;
    totalWeight += entry.weight;
  }

  if (totalWeight === 0) {
    throw new Error("Suma wag musi być większa od zera.");
  }

  return weightedSum / totalWeight;
}

Średnia arytmetyczna

Dodajemy wszystkie oceny i dzielimy przez ich liczbę — każda ocena liczy się tak samo.

Wzór

Średnia arytmetyczna = (ocena₁ + ocena₂ + … + ocenaₙ) ÷ n

Zobacz kod źródłowy (dla programistów)

lib/calculations/simpleAverage.tssimpleAverage()

lib/calculations/simpleAverage.ts
/**
 * Oblicza zwykłą średnią arytmetyczną.
 *
 * Wzór: średnia = Σ(ocenaᵢ) / n
 *
 * @param grades - lista ocen
 * @returns średnia arytmetyczna
 * @throws gdy lista jest pusta
 */
export function simpleAverage(grades: number[]): number {
  if (grades.length === 0) {
    throw new Error("Nie można obliczyć średniej z pustej listy ocen.");
  }

  const sum = grades.reduce((acc, grade) => acc + grade, 0);
  return sum / grades.length;
}

Jakiej oceny potrzebuję

Na podstawie dotychczasowych ocen i wag wyliczamy, jaka ocena z kolejnej pracy (o znanej wadze) da docelową średnią. Wynik ograniczamy do skali 1–6.

Wzór

Wymagana ocena = (docelowa średnia × (suma dotychczasowych wag + waga kolejnej) − suma(ocena × waga)) ÷ waga kolejnej

Zobacz kod źródłowy (dla programistów)

lib/calculations/targetGrade.tstargetGrade()

lib/calculations/targetGrade.ts
/**
 * 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)}.`,
  };
}

Przeliczanie ocen +/-

Zamieniamy zapisy typu 5+ lub 4− na liczby. Szkoły robią to różnie, więc mamy zestaw domyślny, alternatywny (±0,25) i własną mapę.

Wzór

Przykład (zestaw domyślny): 5+ = 5,5 · 5− = 4,75 · 4+ = 4,75 · 4− = 3,75 · 3+ = 3,75 · 3− = 2,75

Zobacz kod źródłowy (dla programistów)

lib/calculations/plusMinusToNumeric.tsplusMinusToNumeric()

lib/calculations/plusMinusToNumeric.ts
/**
 * Przelicza ocenę tekstową (np. „5+”, „4−”) na wartość liczbową.
 *
 * @param grade - ocena jako string (dopuszczalne: „5+”, „5-”, „5−”, „4,5”, „4.5”)
 * @param ruleset - zestaw reguł przeliczenia
 * @param customMap - wymagane przy ruleset „custom”; klucze znormalizowane (bez spacji, − → -)
 * @returns wartość liczbowa oceny
 */
export function plusMinusToNumeric(
  grade: string,
  ruleset: PlusMinusRuleset,
  customMap?: Record<string, number>,
): number {
  const key = normalizeGradeKey(grade);

  // Jawna liczba dziesiętna (np. "4.5") — bez mapy +/-
  if (/^\d+\.\d+$/.test(key)) {
    return Number(key);
  }

  const map = resolveMap(ruleset, customMap);
  const value = map[key];
  if (value === undefined) {
    throw new Error(
      `Nieznana ocena "${grade}" (znormalizowana: "${key}") w ruleset "${ruleset}".`,
    );
  }
  return value;
}

Średnia ECTS

Jak średnia ważona, ale wagą są punkty ECTS. Przy poprawkach można uśrednić podejścia (styl UJ) albo wziąć ostatnią ocenę.

Wzór

Średnia ECTS = (ocena₁ × ECTS₁ + ocena₂ × ECTS₂ + …) ÷ (ECTS₁ + ECTS₂ + …)

Zobacz kod źródłowy (dla programistów)

lib/calculations/ectsWeightedAverage.tsectsWeightedAverage()

lib/calculations/ectsWeightedAverage.ts
/**
 * Oblicza średnią ważoną punktami ECTS.
 *
 * Wzór (po agregacji podejść): średnia = Σ(ocena × ECTS) / Σ(ECTS)
 *
 * Obsługa poprawek (`resitHandling`) — wpisy z tym samym `subjectId`:
 * - `"average"` (styl UJ): uśrednij oceny z podejść, potem zastosuj ECTS raz
 * - `"latest"`: weź ocenę z ostatniego wpisu dla danego przedmiotu
 *
 * @param entries - oceny z ECTS i identyfikatorem przedmiotu
 * @param resitHandling - sposób łączenia poprawek
 */
export function ectsWeightedAverage(
  entries: EctsGradeEntry[],
  resitHandling: "average" | "latest",
): number {
  if (entries.length === 0) {
    throw new Error("Nie można obliczyć średniej ECTS z pustej listy.");
  }

  const bySubject = new Map<
    string,
    { grades: number[]; ects: number }
  >();

  for (const entry of entries) {
    if (entry.ects < 0) {
      throw new Error("Liczba punktów ECTS nie może być ujemna.");
    }
    if (!entry.subjectId) {
      throw new Error("Każdy wpis ECTS wymaga subjectId.");
    }

    const existing = bySubject.get(entry.subjectId);
    if (!existing) {
      bySubject.set(entry.subjectId, {
        grades: [entry.grade],
        ects: entry.ects,
      });
    } else {
      existing.grades.push(entry.grade);
      // ECTS przedmiotu — ostatnia podana wartość (zwykle stała)
      existing.ects = entry.ects;
    }
  }

  let weightedSum = 0;
  let totalEcts = 0;

  for (const { grades, ects } of bySubject.values()) {
    if (ects === 0) {
      continue;
    }

    let grade: number;
    if (grades.length === 1) {
      grade = grades[0]!;
    } else if (resitHandling === "average") {
      grade = grades.reduce((a, b) => a + b, 0) / grades.length;
    } else {
      grade = grades[grades.length - 1]!;
    }

    weightedSum += grade * ects;
    totalEcts += ects;
  }

  if (totalEcts === 0) {
    throw new Error("Suma punktów ECTS musi być większa od zera.");
  }

  return weightedSum / totalEcts;
}

Mediana

Po uporządkowaniu ocen od najmniejszej do największej bierzemy wartość środkową (albo średnią z dwóch środkowych, gdy jest ich parzysta liczba).

Wzór

Mediana = wartość środkowa (gdy n nieparzyste) albo (środkowa₁ + środkowa₂) ÷ 2 (gdy n parzyste)

Zobacz kod źródłowy (dla programistów)

lib/calculations/median.tsmedian()

lib/calculations/median.ts
/**
 * Oblicza medianę zbioru liczb.
 *
 * Wzór:
 * - przy nieparzystej liczbie elementów: wartość środkowa
 * - przy parzystej: średnia dwóch środkowych
 *
 * @param values - lista wartości
 * @returns mediana
 * @throws gdy lista jest pusta
 */
export function median(values: number[]): number {
  if (values.length === 0) {
    throw new Error("Nie można obliczyć mediany z pustej listy.");
  }

  const sorted = [...values].sort((a, b) => a - b);
  const mid = Math.floor(sorted.length / 2);

  if (sorted.length % 2 === 1) {
    return sorted[mid]!;
  }

  return (sorted[mid - 1]! + sorted[mid]!) / 2;
}

Odchylenie standardowe

Pokazuje, jak bardzo oceny „rozjeżdżają się” wokół średniej. Im mniejsza wartość, tym oceny są bliżej siebie.

Wzór

Odchylenie (próba) = √[ suma( (ocenaᵢ − średnia)² ) ÷ (n − 1) ] · Odchylenie (populacja) = √[ suma( (ocenaᵢ − średnia)² ) ÷ n ]

Zobacz kod źródłowy (dla programistów)

lib/calculations/standardDeviation.tsstandardDeviation()

lib/calculations/standardDeviation.ts
/**
 * Oblicza odchylenie standardowe.
 *
 * Wzory:
 * - populacja (`population = true`):  σ = √(Σ(xᵢ − μ)² / N)
 * - próba (`population = false`, domyślnie):  s = √(Σ(xᵢ − x̄)² / (n − 1))
 *
 * @param values - lista wartości
 * @param population - true = odchylenie populacyjne; false = próbkowe (domyślnie)
 * @throws gdy lista jest pusta; przy próbie gdy mniej niż 2 elementy
 */
export function standardDeviation(
  values: number[],
  population = false,
): number {
  if (values.length === 0) {
    throw new Error(
      "Nie można obliczyć odchylenia standardowego z pustej listy.",
    );
  }

  if (!population && values.length < 2) {
    throw new Error(
      "Odchylenie standardowe z próby wymaga co najmniej 2 wartości.",
    );
  }

  const mean = simpleAverage(values);
  const squaredDiffs = values.reduce(
    (sum, x) => sum + (x - mean) ** 2,
    0,
  );
  const divisor = population ? values.length : values.length - 1;
  return Math.sqrt(squaredDiffs / divisor);
}

Dominanta (moda)

To ocena (lub oceny), która pojawia się najczęściej. Jeśli kilka ocen ma tę samą najwyższą częstość, zwracamy wszystkie.

Wzór

Dominanta = ocena(y) z największą liczbą wystąpień

Zobacz kod źródłowy (dla programistów)

lib/calculations/mode.tsmode()

lib/calculations/mode.ts
/**
 * Oblicza dominantę (modę) — wartość(i) występujące najczęściej.
 *
 * Gdy kilka wartości ma tę samą maksymalną częstość, zwraca wszystkie (multimodalność),
 * posortowane rosnąco. Gdy wszystkie wartości występują raz, zwraca wszystkie
 * (każda jest „równie modalna”).
 *
 * @param values - lista wartości
 * @returns tablica dominant (może być pusta tylko dla pustego wejścia — wtedy rzuca)
 */
export function mode(values: number[]): number[] {
  if (values.length === 0) {
    throw new Error("Nie można obliczyć dominanty z pustej listy.");
  }

  const counts = new Map<number, number>();
  for (const v of values) {
    counts.set(v, (counts.get(v) ?? 0) + 1);
  }

  let maxCount = 0;
  for (const c of counts.values()) {
    if (c > maxCount) maxCount = c;
  }

  return [...counts.entries()]
    .filter(([, c]) => c === maxCount)
    .map(([v]) => v)
    .sort((a, b) => a - b);
}

Czerwony pasek

Świadectwo z wyróżnieniem wymaga jednocześnie odpowiednio wysokiej średniej i oceny z zachowania.

Wzór

Czerwony pasek = średnia ≥ 4,75 ORAZ zachowanie ∈ {wzorowe, bardzo dobre}

Zobacz kod źródłowy (dla programistów)

lib/calculations/redStripeEligibility.tsredStripeEligibility()

lib/calculations/redStripeEligibility.ts
/**
 * Sprawdza uprawnienie do świadectwa z wyróżnieniem („czerwony pasek”).
 *
 * Warunki (uproszczone, zgodne z powszechną praktyką szkolną):
 * - średnia ocen ≥ 4,75
 * - ocena z zachowania: „wzorowe” lub „bardzo dobre”
 *
 * Uwaga: szczegółowe zasady mogą wynikać ze statutu szkoły / WSO —
 * ta funkcja implementuje kanoniczny próg produktowy.
 *
 * @param average - średnia ocen (arytmetyczna lub ważona — wg szkoły)
 * @param behaviorGrade - ocena z zachowania (tekst)
 */
export function redStripeEligibility(
  average: number,
  behaviorGrade: string,
): RedStripeResult {
  const normalizedBehavior = behaviorGrade.trim().toLowerCase();
  const behaviorOk = (RED_STRIPE_BEHAVIOR_GRADES as readonly string[]).includes(
    normalizedBehavior,
  );
  const averageOk = average >= RED_STRIPE_AVERAGE_THRESHOLD;

  if (averageOk && behaviorOk) {
    return {
      eligible: true,
      reason: `Uprawnienie spełnione: średnia ${average} ≥ ${RED_STRIPE_AVERAGE_THRESHOLD} oraz zachowanie „${normalizedBehavior}”.`,
    };
  }

  const parts: string[] = [];
  if (!averageOk) {
    parts.push(
      `średnia ${average} jest poniżej progu ${RED_STRIPE_AVERAGE_THRESHOLD}`,
    );
  }
  if (!behaviorOk) {
    parts.push(
      `zachowanie „${behaviorGrade.trim()}” nie należy do: ${RED_STRIPE_BEHAVIOR_GRADES.join(", ")}`,
    );
  }

  return {
    eligible: false,
    reason: `Brak uprawnienia: ${parts.join("; ")}.`,
  };
}

Konwersja GPA

Przybliżone przeliczenie polskiej skali 1–6 na GPA 0–4 (i odwrotnie). To nie jest oficjalny przelicznik uczelni — tylko szybkie oszacowanie.

Wzór

Przybliżenie: 6 → 4,0 GPA · 5 → 3,5 GPA · 4 → 3,0 GPA · 3 → 2,0 GPA · 2 → 1,0 GPA · 1 → 0 GPA (wartości pośrednie: interpolacja)

Zobacz kod źródłowy (dla programistów)

lib/calculations/gpaConvert.tsgpaConvert()

lib/calculations/gpaConvert.ts
/**
 * Konwertuje ocenę polską ↔ GPA według udokumentowanej tabeli przybliżeń.
 *
 * @param plGrade - wartość wejściowa (PL przy plToGpa, GPA przy gpaToPl)
 * @param direction - kierunek konwersji
 */
export function gpaConvert(plGrade: number, direction: GpaDirection): number {
  if (direction === "plToGpa") {
    return interpolate(plGrade, PL_TO_GPA_ANCHORS);
  }

  // gpaToPl — odwrócone kotwice [gpa, pl]
  const reversed = PL_TO_GPA_ANCHORS.map(
    ([pl, gpa]) => [gpa, pl] as [number, number],
  );
  return interpolate(plGrade, reversed);
}

Snapshot kodu wygenerowany:

Bibliografia i akty prawne

Poniższe odnośniki prowadzą do oficjalnych publikacji w Internetowym Systemie Aktów Prawnych (ISAP). Przed decyzjami o stypendium lub klasyfikacji zawsze sprawdź aktualny tekst jednolity oraz regulamin swojej szkoły/uczelni.

  1. 1. Rozporządzenie Ministra Edukacji Narodowej z dnia 22 lutego 2019 r. w sprawie oceniania, klasyfikowania i promowania uczniów i słuchaczy w szkołach publicznych (Dz.U. 2019 poz. 373)

    Zasady oceniania w szkołach publicznych (skala stopni, klasyfikacja). Tekst jednolity: Dz.U. 2023 poz. 2572.

    https://isap.sejm.gov.pl/isap.nsf/DocDetails.xsp?id=WDU20190000373

  2. 2. Ustawa z dnia 20 lipca 2018 r. — Prawo o szkolnictwie wyższym i nauce (Dz.U. 2018 poz. 1668)

    Ramowe zasady szkolnictwa wyższego, w tym kontekst stypendiów i punktów ECTS (szczegóły w regulaminach uczelni).

    https://isap.sejm.gov.pl/isap.nsf/DocDetails.xsp?id=WDU20180001668

Wróć do kalkulatora średniej ważonej albo na stronę główną.