from typing import Optional from .dates_add_days import parse_iso_date from .education_attendance_rate_data import ABSENCE_THRESHOLDS, ABSENCE_THRESHOLDS_HISTORY, ABSENCE_THRESHOLDS_HORIZON from .education_attendance_rate_types import AttendanceRate from .math_round_div import round_div def _is_int(value: object) -> bool: return isinstance(value, int) and not isinstance(value, bool) def _threshold_on(measure: str, on_date: str) -> Optional[int]: for r in ABSENCE_THRESHOLDS: if r.measure == measure and on_date >= r.valid_from and (r.valid_to is None or on_date <= r.valid_to): return r.basis_points return None def attendance_rate(possible_sessions: int, missed_sessions: int, on_date: str) -> AttendanceRate: """Attendance and absence in basis points, and the DfE absence flags. The flags compare missed x 10000 with possible x threshold, exactly: a rate of 9.996% rounds to 10.00% but is not persistent absence. """ if not _is_int(possible_sessions) or possible_sessions < 1: raise ValueError("possibleSessions must be a whole number of 1 or more, received %s" % (possible_sessions,)) if not _is_int(missed_sessions) or missed_sessions < 0 or missed_sessions > possible_sessions: raise ValueError( "missedSessions must be a whole number from 0 to possibleSessions (%s), received %s" % (possible_sessions, missed_sessions) ) parse_iso_date(on_date) persistent = _threshold_on("persistent", on_date) if persistent is None: if ABSENCE_THRESHOLDS_HISTORY != "full" and ABSENCE_THRESHOLDS_HORIZON is not None and on_date < ABSENCE_THRESHOLDS_HORIZON: raise ValueError( "no persistent absence definition on %s: this build was installed with history=%s, so it only carries rules from %s. " "Reinstall with history=full for earlier years." % (on_date, ABSENCE_THRESHOLDS_HISTORY, ABSENCE_THRESHOLDS_HORIZON) ) raise ValueError("no persistent absence definition on %s" % (on_date,)) severe = _threshold_on("severe", on_date) absence = round_div(missed_sessions * 10000, possible_sessions, "half-up") return AttendanceRate( attendance_basis_points=10000 - absence, absence_basis_points=absence, persistently_absent=missed_sessions * 10000 >= possible_sessions * persistent, severely_absent=None if severe is None else missed_sessions * 10000 >= possible_sessions * severe, )