education.grade-boundaries@1.0.0
impl/typescript.ts
2,221 bytes · the TypeScript implementation · view raw
import { type GradeBoundary, type GradeResult } from "./education_grade_boundaries_types.ts";
/**
* The grade a mark earns from a boundary table, and the gap to the next grade.
*
* Rows are sorted by minMark here because published tables run highest grade
* first, and a lookup that assumes ascending order hands out the wrong grade.
*/
export function gradeForMark(mark: number, maxMark: number, boundaries: readonly GradeBoundary[]): GradeResult {
if (!Number.isInteger(maxMark) || maxMark < 1) {
throw new RangeError(`maxMark must be a positive whole number, received ${maxMark}`);
}
if (!Number.isInteger(mark) || mark < 0 || mark > maxMark) {
throw new RangeError(`mark must be a whole number from 0 to maxMark (${maxMark}), received ${mark}`);
}
if (boundaries.length === 0) {
throw new RangeError("boundaries must not be empty");
}
const seen = new Set<string>();
for (const b of boundaries) {
if (typeof b.grade !== "string" || b.grade.length === 0) {
throw new RangeError("every boundary needs a non-empty grade");
}
if (!Number.isInteger(b.minMark) || b.minMark < 0 || b.minMark > maxMark) {
throw new RangeError(`boundary minMark must be a whole number from 0 to maxMark (${maxMark}), received ${b.minMark} for grade ${b.grade}`);
}
if (seen.has(b.grade)) {
throw new RangeError(`grade ${b.grade} appears twice in boundaries`);
}
seen.add(b.grade);
}
const sorted = [...boundaries].sort((a, b) => a.minMark - b.minMark);
for (let i = 1; i < sorted.length; i++) {
if (sorted[i].minMark === sorted[i - 1].minMark) {
throw new RangeError(`grades ${sorted[i - 1].grade} and ${sorted[i].grade} share the boundary ${sorted[i].minMark}`);
}
}
let index = -1;
for (let i = 0; i < sorted.length; i++) {
if (sorted[i].minMark <= mark) index = i;
}
const awarded = index >= 0 ? sorted[index] : null;
const next = index + 1 < sorted.length ? sorted[index + 1] : null;
return {
grade: awarded === null ? null : awarded.grade,
minMark: awarded === null ? null : awarded.minMark,
nextGrade: next === null ? null : next.grade,
marksToNext: next === null ? null : next.minMark - mark,
};
}