Functional Weave
Code in Rust

education.degree-classification@1.0.0

impl/typescript.ts

2,966 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 { type Rational, addRational, divideRational, multiplyRational, rational, rationalToInteger } from "./math_rational.ts";  ← from math.rational ^1.0.0 · built alongside by fune
import { type RoundingMode } from "./math_round_div.ts";  ← from math.round-div ^1.0.0 · built alongside by fune
import { DEGREE_CLASSES } from "./education_degree_classification_data.ts";  ← this capability’s own data, compiled from data/degree-classes.json into the same file by fune build
import { type DegreeClass, type DegreeClassification, type LevelWeight, type ModuleMark } from "./education_degree_classification_types.ts";

/**
 * Honours classification: credit-weighted mean per level, levels combined by
 * their weights, rounded once, then banded.
 */
export function degreeClassification(
  modules: readonly ModuleMark[],
  levelWeights: readonly LevelWeight[],
  decimals: number,
  mode: RoundingMode,
): DegreeClassification {
  if (levelWeights.length === 0) {
    throw new RangeError("levelWeights must name at least one level");
  }
  if (!Number.isInteger(decimals) || decimals < 0 || decimals > 6) {
    throw new RangeError(`decimals must be a whole number from 0 to 6, received ${decimals}`);
  }
  for (const m of modules) {
    if (!Number.isInteger(m.mark) || m.mark < 0 || m.mark > 100) {
      throw new RangeError(`mark must be a whole number from 0 to 100, received ${m.mark}`);
    }
    if (!Number.isInteger(m.credits) || m.credits < 1) {
      throw new RangeError(`credits must be a whole number of 1 or more, received ${m.credits}`);
    }
  }
  let totalWeight = 0;
  let sum: Rational = rational(0, 1);
  let countedCredits = 0;
  for (let i = 0; i < levelWeights.length; i++) {
    const lw = levelWeights[i];
    if (!Number.isInteger(lw.weight) || lw.weight < 0) {
      throw new RangeError(`weight must be a whole number of 0 or more, received ${lw.weight} for level ${lw.level}`);
    }
    for (let j = 0; j < i; j++) {
      if (levelWeights[j].level === lw.level) throw new RangeError(`level ${lw.level} appears twice in levelWeights`);
    }
    if (lw.weight === 0) continue;
    let marks = 0;
    let credits = 0;
    for (const m of modules) {
      if (m.level === lw.level) {
        marks += m.mark * m.credits;
        credits += m.credits;
      }
    }
    if (credits === 0) {
      throw new RangeError(`no modules at level ${lw.level}, which levelWeights counts`);
    }
    countedCredits += credits;
    totalWeight += lw.weight;
    sum = addRational(sum, multiplyRational(rational(marks, credits), rational(lw.weight, 1)));
  }
  if (totalWeight === 0) {
    throw new RangeError("levelWeights must not all be zero");
  }
  const average = divideRational(sum, rational(totalWeight, 1));
  const scale = 10 ** decimals;
  const scaled = rationalToInteger(multiplyRational(average, rational(scale, 1)), mode);
  const bands = [...DEGREE_CLASSES].sort((a, b) => b.minMark - a.minMark);
  const band = bands.find((b) => scaled >= b.minMark * scale) ?? bands[bands.length - 1];
  return {
    average,
    scaled,
    decimals,
    classification: band.classification as DegreeClass,
    label: band.label,
    countedCredits,
  };
}