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