Functional Weave
Code in Rust

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,
  };
}