Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import { type RoundingMode, roundDiv } from "./math_round_div.ts"; ← from math.round-div ^1.0.0 · built alongside by fune
import { DEFAULT_GRADE_POINTS } from "./education_gpa_data.ts"; ← this capability’s own data, compiled from data/default-grade-points.json into the same file by fune build
import { type GpaCourse, type GpaResult, type GradePoint } from "./education_gpa_types.ts";
/**
* Credit-weighted grade point average in hundredths, rounded once.
*
* Quality points are summed exactly in integers (hundredths x credits), so the
* rounding mode the caller names is the only rounding that happens.
*/
export function gpa(courses: readonly GpaCourse[], gradePoints: readonly GradePoint[] | null, mode: RoundingMode): GpaResult {
const table: readonly GradePoint[] = gradePoints === null ? DEFAULT_GRADE_POINTS : gradePoints;
if (table.length === 0) {
throw new RangeError("gradePoints must not be empty; pass null for the default table");
}
const points = new Map<string, number | null>();
for (const row of table) {
if (points.has(row.grade)) {
throw new RangeError(`grade "${row.grade}" appears twice in gradePoints`);
}
if (row.points !== null && (!Number.isInteger(row.points) || row.points < 0)) {
throw new RangeError(`points must be whole hundredths of 0 or more, received ${row.points} for grade "${row.grade}"`);
}
points.set(row.grade, row.points);
}
let qualityPoints = 0;
let gpaCredits = 0;
let excludedCredits = 0;
for (const course of courses) {
if (!Number.isInteger(course.credits) || course.credits < 0) {
throw new RangeError(`credits must be a whole number of 0 or more, received ${course.credits}`);
}
if (!points.has(course.grade)) {
throw new RangeError(`grade "${course.grade}" is not in the grade-point table`);
}
const p = points.get(course.grade) as number | null;
if (p === null) {
excludedCredits += course.credits;
} else {
qualityPoints += p * course.credits;
gpaCredits += course.credits;
}
}
// Validate the mode even when there is nothing to divide.
roundDiv(0, 1, mode);
return {
gpa: gpaCredits === 0 ? null : roundDiv(qualityPoints, gpaCredits, mode),
qualityPoints,
gpaCredits,
excludedCredits,
};
}