Files
UT-Registration-Plus/src/views/lib/database/queryDistribution.ts
2024-10-04 21:39:30 -05:00

207 lines
6.7 KiB
TypeScript

import type { Course, Semester } from '@shared/types/Course';
import type { CourseSQLRow, Distribution } from '@shared/types/Distribution';
import type { QueryExecResult } from 'sql.js';
import { initializeDB } from './initializeDB';
type GradeDistributionParams = {
':department_code': string;
':course_number': string;
':instructor_last'?: string;
':semester'?: string;
};
/**
* fetches the aggregate distribution of grades for a given course from the course db, and the semesters that we have data for
* @param course the course to fetch the distribution for
* @returns a Distribution object containing the distribution of grades for the course, and
* an array of semesters that we have the distribution for
*/
export async function queryAggregateDistribution(course: Course): Promise<[Distribution, Semester[], boolean]> {
const db = await initializeDB();
const [query, params] = generateQuery(course, null, true);
let res: QueryExecResult | undefined;
let instructorIncluded = params[':instructor_last'] !== undefined;
if (instructorIncluded) {
// Only check for data on an instructor if the course has an instructor
res = db.exec(query, params)?.[0];
}
if (!instructorIncluded || !res?.columns?.length) {
// Either no instructor for the class, or no data for the instructor
instructorIncluded = false;
const [queryWithoutInstructor, paramsWithoutInstructor] = generateQuery(course, null, false);
res = db.exec(queryWithoutInstructor, paramsWithoutInstructor)?.[0];
if (!res?.columns?.length) {
throw new NoDataError(course);
}
}
const row: Required<CourseSQLRow> = {} as Required<CourseSQLRow>;
for (let i = 0; i < res.columns.length; i++) {
const col = res.columns[i] as keyof CourseSQLRow;
switch (col) {
case 'A':
case 'A_Minus':
case 'B_Plus':
case 'B':
case 'B_Minus':
case 'C_Plus':
case 'C':
case 'C_Minus':
case 'D_Plus':
case 'D':
case 'D_Minus':
case 'F':
case 'Other':
row[col] = res.values.reduce((acc, cur) => acc + (cur[i] as number), 0) as never;
break;
default:
row[col] = res.columns[i]![0]! as never;
}
}
const distribution: Distribution = {
A: row.A,
'A-': row.A_Minus,
'B+': row.B_Plus,
B: row.B,
'B-': row.B_Minus,
'C+': row.C_Plus,
C: row.C,
'C-': row.C_Minus,
'D+': row.D_Plus,
D: row.D,
'D-': row.D_Minus,
F: row.F,
Other: row.Other,
};
// get unique semesters from the data
const rawSemesters = res.values.reduce((acc, cur) => acc.add(cur[0] as string), new Set<string>());
const semesters: Semester[] = [];
rawSemesters.forEach((sem: string) => {
const [season, year] = sem.split(' ');
if (!season || !year) {
throw new Error('Season is undefined');
}
semesters.push({ year: parseInt(year, 10), season: season as Semester['season'] });
});
return [distribution, semesters, instructorIncluded];
}
/**
* Creates a SQL query that we can execute on the database to get the distribution of grades for a given course in a given semester
* @param course the course to fetch the distribution for
* @param semester the semester to fetch the distribution for OR null if we want the aggregate distribution
* @returns a SQL query string
*/
function generateQuery(
course: Course,
semester: Semester | null,
includeInstructor: boolean
): [string, GradeDistributionParams] {
const query = `
select * from grade_distributions
where Department_Code = :department_code
and Course_Number = :course_number
${includeInstructor ? `and Instructor_Last = :instructor_last collate nocase` : ''}
${semester ? `and Semester = :semester` : ''}
`;
const params: GradeDistributionParams = {
':department_code': course.department,
':course_number': course.number,
};
if (includeInstructor) {
params[':instructor_last'] = course.instructors[0]?.lastName;
}
if (semester) {
params[':semester'] = `${semester.season} ${semester.year}`;
}
return [query, params];
}
/**
* fetches the distribution of grades for a semester for a given course from the course db
* @param course the course to fetch the distribution for
* @param semester the semester to fetch the distribution for
* @returns a Distribution object containing the distribution of grades for the course
*/
export async function querySemesterDistribution(course: Course, semester: Semester): Promise<[Distribution, boolean]> {
const db = await initializeDB();
const [query, params] = generateQuery(course, semester, true);
let res = db.exec(query, params)?.[0];
let instructorIncluded = true;
if (!res?.columns?.length) {
instructorIncluded = false;
const [queryWithoutInstructor, paramsWithoutInstructor] = generateQuery(course, semester, false);
res = db.exec(queryWithoutInstructor, paramsWithoutInstructor)?.[0];
if (!res?.columns?.length) {
throw new NoDataError(course);
}
}
const row: Required<CourseSQLRow> = {} as Required<CourseSQLRow>;
for (let i = 0; i < res.columns.length; i++) {
const col = res.columns[i] as keyof CourseSQLRow;
switch (col) {
case 'A':
case 'A_Minus':
case 'B_Plus':
case 'B':
case 'B_Minus':
case 'C_Plus':
case 'C':
case 'C_Minus':
case 'D_Plus':
case 'D':
case 'D_Minus':
case 'F':
case 'Other':
row[col] = res.values.reduce((acc, cur) => acc + (cur[i] as number), 0) as never;
break;
default:
row[col] = res.columns[i]![0]! as never;
}
}
return [
{
A: row.A,
'A-': row.A_Minus,
'B+': row.B_Plus,
B: row.B,
'B-': row.B_Minus,
'C+': row.C_Plus,
C: row.C,
'C-': row.C_Minus,
'D+': row.D_Plus,
D: row.D,
'D-': row.D_Minus,
F: row.F,
Other: row.Other,
},
instructorIncluded,
];
}
/**
* A custom error class for when we don't have data for a course
*/
export class NoDataError extends Error {
constructor(course: Course) {
super(`No data for #${course.uniqueId} ${course.department} ${course.number}`);
this.name = 'NoDataError';
}
}