Functional Weave
Code in Rust

education.grade-boundaries@1.0.0

impl/python.py

2,369 bytes · the Python implementation · view raw

from typing import Sequence

from .education_grade_boundaries_types import GradeBoundary, GradeResult


def _is_int(value: object) -> bool:
    return isinstance(value, int) and not isinstance(value, bool)


def grade_for_mark(mark: int, max_mark: int, boundaries: Sequence[GradeBoundary]) -> GradeResult:
    """The grade a mark earns from a boundary table, and the gap to the next grade.

    Rows are sorted by min_mark here because published tables run highest
    grade first, and a lookup that assumes ascending order hands out the wrong
    grade.
    """
    if not _is_int(max_mark) or max_mark < 1:
        raise ValueError("maxMark must be a positive whole number, received %s" % (max_mark,))
    if not _is_int(mark) or mark < 0 or mark > max_mark:
        raise ValueError("mark must be a whole number from 0 to maxMark (%s), received %s" % (max_mark, mark))
    if len(boundaries) == 0:
        raise ValueError("boundaries must not be empty")
    seen = set()
    for b in boundaries:
        if not isinstance(b.grade, str) or len(b.grade) == 0:
            raise ValueError("every boundary needs a non-empty grade")
        if not _is_int(b.min_mark) or b.min_mark < 0 or b.min_mark > max_mark:
            raise ValueError(
                "boundary minMark must be a whole number from 0 to maxMark (%s), received %s for grade %s"
                % (max_mark, b.min_mark, b.grade)
            )
        if b.grade in seen:
            raise ValueError("grade %s appears twice in boundaries" % (b.grade,))
        seen.add(b.grade)
    ordered = sorted(boundaries, key=lambda b: b.min_mark)
    for i in range(1, len(ordered)):
        if ordered[i].min_mark == ordered[i - 1].min_mark:
            raise ValueError(
                "grades %s and %s share the boundary %s" % (ordered[i - 1].grade, ordered[i].grade, ordered[i].min_mark)
            )

    index = -1
    for i, b in enumerate(ordered):
        if b.min_mark <= mark:
            index = i
    awarded = ordered[index] if index >= 0 else None
    nxt = ordered[index + 1] if index + 1 < len(ordered) else None
    return GradeResult(
        grade=None if awarded is None else awarded.grade,
        min_mark=None if awarded is None else awarded.min_mark,
        next_grade=None if nxt is None else nxt.grade,
        marks_to_next=None if nxt is None else nxt.min_mark - mark,
    )