import type { Course, Semester } from '@shared/types/Course'; import type { CourseSQLRow, Distribution } from '@shared/types/Distribution'; import { initializeDB } from './initializeDB'; /** * 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[]]> { const db = await initializeDB(); const query = generateQuery(course, null); const res = db.exec(query)?.[0]; if (!res?.columns?.length) { throw new NoDataError(course); } let row: Required = {} as Required; res.columns.forEach((col, i) => { row[res.columns[i]] = res.values[0][i]; }); const distribution: Distribution = { A: row.a2, 'A-': row.a3, 'B+': row.b1, B: row.b2, 'B-': row.b3, 'C+': row.c1, C: row.c2, 'C-': row.c3, 'D+': row.d1, D: row.d2, 'D-': row.d3, F: row.f, }; // the db file for some reason has duplicate semesters, so we use a set to remove duplicates const rawSemesters = new Set(); row.semesters.split(',').forEach((sem: string) => { rawSemesters.add(sem); }); const semesters: Semester[] = []; rawSemesters.forEach((sem: string) => { const [season, year] = sem.split(' '); semesters.push({ year: parseInt(year, 10), season: season as Semester['season'] }); }); return [distribution, semesters]; } /** * 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): string { const profName = course.instructors[0]?.fullName; const query = ` select * from ${semester ? 'grades' : 'agg'} where dept like '%${course.department}%' ${profName ? `and prof like '%${profName}%'` : ''} and course_nbr like '%${course.number}%' ${semester ? `and sem like '%${semester.season} ${semester.year}%'` : ''} order by a1+a2+a3+b1+b2+b3+c1+c2+c3+d1+d2+d3+f desc `; return query; } /** * 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 { const db = await initializeDB(); const query = generateQuery(course, semester); const res = db.exec(query)?.[0]; if (!res?.columns?.length) { throw new NoDataError(course); } let row: Required = {} as Required; res.columns.forEach((col, i) => { row[res.columns[i]] = res.values[0][i]; }); const distribution: Distribution = { A: row.a2, 'A-': row.a3, 'B+': row.b1, B: row.b2, 'B-': row.b3, 'C+': row.c1, C: row.c2, 'C-': row.c3, 'D+': row.d1, D: row.d2, 'D-': row.d3, F: row.f, }; return distribution; } /** * 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'; } }