feat: support summer grades, fix summer course parser (#596)

* feat: support summer grades, fix summer course parser

* chore: lint

* docs: mention summer terms in Course::number description

* feat: Course::getNumberWithoutTerm, strip summer term indicator when displaying grades

---------

Co-authored-by: doprz <52579214+doprz@users.noreply.github.com>
This commit is contained in:
Samuel Gunter
2025-06-08 21:10:05 -07:00
committed by GitHub
parent eb8141ee8c
commit 2d92dd47f0
8 changed files with 196 additions and 28 deletions

View File

@@ -155,15 +155,14 @@ export default function PopupMain(): JSX.Element {
<SortableList
draggables={activeSchedule.courses.map(course => ({
id: course.uniqueId,
...course,
getConflicts: course.getConflicts,
course,
}))}
onChange={reordered => {
activeSchedule.courses = reordered.map(({ id: _id, ...course }) => course);
activeSchedule.courses = reordered.map(({ course }) => course);
replaceSchedule(getActiveSchedule(), activeSchedule);
}}
renderItem={course => (
<PopupCourseBlock key={course.id} course={course} colors={course.colors} />
renderItem={({ id, course }) => (
<PopupCourseBlock key={id} course={course} colors={course.colors} />
)}
/>
)}

View File

@@ -215,7 +215,7 @@ export default function GradeDistribution({ course }: GradeDistributionProps): J
options={{
...chartOptions,
title: {
text: `There is currently no grade distribution data for ${course.department} ${course.number}`,
text: `There is currently no grade distribution data for ${course.department} ${course.getNumberWithoutTerm()}`,
},
tooltip: { enabled: false },
}}
@@ -228,7 +228,7 @@ export default function GradeDistribution({ course }: GradeDistributionProps): J
<Text variant='small' className='text-ut-black'>
Grade Distribution for{' '}
<Text variant='small' className='font-extrabold!' as='strong'>
{course.department} {course.number}
{course.department} {course.getNumberWithoutTerm()}
</Text>
</Text>
<select
@@ -267,7 +267,8 @@ export default function GradeDistribution({ course }: GradeDistributionProps): J
<div className='mt-3 flex flex-wrap content-center items-center self-stretch justify-center gap-3 text-center'>
<Text variant='small' className='text-theme-red'>
We couldn&apos;t find {semester !== 'Aggregate' && ` ${semester}`} grades for this
instructor, so here are the grades for all {course.department} {course.number} sections.
instructor, so here are the grades for all {course.department}{' '}
{course.getNumberWithoutTerm()} sections.
</Text>
</div>
)}

View File

@@ -0,0 +1,34 @@
import { describe, expect, it } from 'vitest';
import { CourseCatalogScraper } from './CourseCatalogScraper';
describe('CourseCatalogScraper::separateCourseName', () => {
it('should separate a simple course', () => {
// UT Formats strings weird... lots of meaningless spaces
const input = 'C S 314H DATA STRUCTURES: HONORS ';
const expected = ['DATA STRUCTURES: HONORS', 'C S', '314H'];
const result = CourseCatalogScraper.separateCourseName(input);
expect(result).toEqual(expected);
});
it('separate summer courses ', () => {
// UT Formats strings weird... lots of meaningless spaces
const inputs = [
'P R f378 PUBLIC RELATNS TECHNIQUES-IRL (First term) ',
'CRP s396 INDEPENDENT RESEARCH IN CRP (Second term) ',
'B A n284S 1-MANAGERIAL MICROECON-I-DAL (Nine week term) ',
'J w379 JOURNALISM INDEPENDENT STUDY (Whole term) ',
];
const expected = [
['PUBLIC RELATNS TECHNIQUES-IRL (First term)', 'P R', 'f378'],
['INDEPENDENT RESEARCH IN CRP (Second term)', 'CRP', 's396'],
['1-MANAGERIAL MICROECON-I-DAL (Nine week term)', 'B A', 'n284S'],
['JOURNALISM INDEPENDENT STUDY (Whole term)', 'J', 'w379'],
];
const results = inputs.map(input => CourseCatalogScraper.separateCourseName(input));
expect(results).toEqual(expected);
});
});

View File

@@ -75,7 +75,7 @@ export class CourseCatalogScraper {
fullName = fullName.replace(/\s\s+/g, ' ').trim();
const [courseName, department, number] = this.separateCourseName(fullName);
const [courseName, department, number] = CourseCatalogScraper.separateCourseName(fullName);
const [status, isReserved] = this.getStatus(row);
const newCourse = new Course({
@@ -113,16 +113,31 @@ export class CourseCatalogScraper {
*
* @example
* ```
* separateCourseName("CS 314H - Honors Discrete Structures") => ["Honors Discrete Structures", "CS", "314H"]
* separateCourseName("C S 314H DATA STRUCTURES: HONORS") => ["DATA STRUCTURES: HONORS", "C S", "314H"]
* ```
* @param courseFullName - the full name of the course (e.g. "CS 314H - Honors Discrete Structures")
* @returns an array of the course name , department, and number
* @param courseFullName - the full name of the course (e.g. "C S 314H DATA STRUCTURES: HONORS")
* @returns an array of the course name, department, and number
*/
separateCourseName(courseFullName: string): [courseName: string, department: string, number: string] {
let courseNumberIndex = courseFullName.search(/\d/);
let department = courseFullName.substring(0, courseNumberIndex).trim();
let number = courseFullName.substring(courseNumberIndex, courseFullName.indexOf(' ', courseNumberIndex)).trim();
let courseName = courseFullName.substring(courseFullName.indexOf(' ', courseNumberIndex)).trim();
static separateCourseName(courseFullName: string): [courseName: string, department: string, number: string] {
// C S 314H DATA STRUCTURES: HONORS
// ^ Here for normal courses
// B A n284S 1-MANAGERIAL MICROECON-I-DAL (Nine week term)
// ^ Also works for summer courses ([f]irst term, [s]econd term, [n]ine week term, [w]hole term)
const courseNumberIndex = courseFullName.search(/\w?\d/);
if (courseNumberIndex === -1) {
throw new Error("Course name doesn't have a course number");
}
// Everything before the course number
const department = courseFullName.substring(0, courseNumberIndex).trim();
const number = courseFullName
.substring(courseNumberIndex, courseFullName.indexOf(' ', courseNumberIndex))
.trim();
// Everything after the course number
const courseName = courseFullName.substring(courseFullName.indexOf(' ', courseNumberIndex)).trim();
return [courseName, department, number];
}

View File

@@ -109,16 +109,22 @@ function generateQuery(
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` : ''}
SELECT * FROM grade_distributions
WHERE Department_Code = :department_code
AND Course_Number COLLATE NOCASE IN (
:course_number,
concat('F', :course_number), -- Check summer courses with prefix, too
concat('S', :course_number),
concat('N', :course_number),
concat('W', :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,
':course_number': course.getNumberWithoutTerm(),
};
if (includeInstructor) {