Functional Weave
Code in Python

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,
    )