education.attendance-rate@1.0.1
impl/typescript.ts
2,332 bytes · the TypeScript 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.
import { parseIsoDate } from "./dates_add_days.ts"; ← from dates.add-days ^1.0.0 · built alongside by fune
import { roundDiv } from "./math_round_div.ts"; ← from math.round-div ^1.0.0 · built alongside by fune
import { ABSENCE_THRESHOLDS, ABSENCE_THRESHOLDS_HISTORY, ABSENCE_THRESHOLDS_HORIZON } from "./education_attendance_rate_data.ts"; ← this capability’s own data, compiled from data/absence-thresholds.json into the same file by fune build
import { type AttendanceRate } from "./education_attendance_rate_types.ts";
function thresholdOn(measure: string, onDate: string): number | null {
for (const r of ABSENCE_THRESHOLDS) {
if (r.measure === measure && onDate >= r.validFrom && (r.validTo === null || onDate <= r.validTo)) return r.basisPoints;
}
return null;
}
/**
* 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.
*/
export function attendanceRate(possibleSessions: number, missedSessions: number, onDate: string): AttendanceRate {
if (!Number.isInteger(possibleSessions) || possibleSessions < 1) {
throw new RangeError(`possibleSessions must be a whole number of 1 or more, received ${possibleSessions}`);
}
if (!Number.isInteger(missedSessions) || missedSessions < 0 || missedSessions > possibleSessions) {
throw new RangeError(`missedSessions must be a whole number from 0 to possibleSessions (${possibleSessions}), received ${missedSessions}`);
}
parseIsoDate(onDate);
const persistent = thresholdOn("persistent", onDate);
if (persistent === null) {
if (ABSENCE_THRESHOLDS_HISTORY !== "full" && ABSENCE_THRESHOLDS_HORIZON !== null && onDate < ABSENCE_THRESHOLDS_HORIZON) {
throw new RangeError(
`no persistent absence definition on ${onDate}: this build was installed with history=${ABSENCE_THRESHOLDS_HISTORY}, so it only carries rules from ${ABSENCE_THRESHOLDS_HORIZON}. Reinstall with history=full for earlier years.`,
);
}
throw new RangeError(`no persistent absence definition on ${onDate}`);
}
const severe = thresholdOn("severe", onDate);
const absence = roundDiv(missedSessions * 10000, possibleSessions, "half-up");
return {
attendanceBasisPoints: 10000 - absence,
absenceBasisPoints: absence,
persistentlyAbsent: missedSessions * 10000 >= possibleSessions * persistent,
severelyAbsent: severe === null ? null : missedSessions * 10000 >= possibleSessions * severe,
};
}