education.degree-classification@1.0.0
impl/python.py
2,952 bytes · the Python implementation · view raw
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
from typing import Sequence
from .education_degree_classification_data import DEGREE_CLASSES ← this capability’s own data, compiled from data/degree-classes.json into the same file by fune build
from .education_degree_classification_types import DegreeClassification, LevelWeight, ModuleMark
from .math_rational import add_rational, divide_rational, multiply_rational, rational, rational_to_integer ← from math.rational ^1.0.0 · built alongside by fune
from .math_round_div import RoundingMode ← from math.round-div ^1.0.0 · built alongside by fune
def _is_int(value: object) -> bool:
return isinstance(value, int) and not isinstance(value, bool)
def degree_classification(
modules: Sequence[ModuleMark], level_weights: Sequence[LevelWeight], decimals: int, mode: RoundingMode
) -> DegreeClassification:
"""Honours classification: credit-weighted mean per level, levels combined
by their weights, rounded once, then banded."""
if len(level_weights) == 0:
raise ValueError("levelWeights must name at least one level")
if not _is_int(decimals) or decimals < 0 or decimals > 6:
raise ValueError("decimals must be a whole number from 0 to 6, received %s" % (decimals,))
for m in modules:
if not _is_int(m.mark) or m.mark < 0 or m.mark > 100:
raise ValueError("mark must be a whole number from 0 to 100, received %s" % (m.mark,))
if not _is_int(m.credits) or m.credits < 1:
raise ValueError("credits must be a whole number of 1 or more, received %s" % (m.credits,))
total_weight = 0
total = rational(0, 1)
counted_credits = 0
for i, lw in enumerate(level_weights):
if not _is_int(lw.weight) or lw.weight < 0:
raise ValueError("weight must be a whole number of 0 or more, received %s for level %s" % (lw.weight, lw.level))
for j in range(i):
if level_weights[j].level == lw.level:
raise ValueError("level %s appears twice in levelWeights" % (lw.level,))
if lw.weight == 0:
continue
marks = 0
credits = 0
for m in modules:
if m.level == lw.level:
marks += m.mark * m.credits
credits += m.credits
if credits == 0:
raise ValueError("no modules at level %s, which levelWeights counts" % (lw.level,))
counted_credits += credits
total_weight += lw.weight
total = add_rational(total, multiply_rational(rational(marks, credits), rational(lw.weight, 1)))
if total_weight == 0:
raise ValueError("levelWeights must not all be zero")
average = divide_rational(total, rational(total_weight, 1))
scale = 10**decimals
scaled = rational_to_integer(multiply_rational(average, rational(scale, 1)), mode)
bands = sorted(DEGREE_CLASSES, key=lambda b: -b.min_mark)
band = next((b for b in bands if scaled >= b.min_mark * scale), bands[-1])
return DegreeClassification(
average=average,
scaled=scaled,
decimals=decimals,
classification=band.classification,
label=band.label,
counted_credits=counted_credits,
)