diff --git a/src/shared/types/Course.ts b/src/shared/types/Course.ts
index 0e47a957..86d4806c 100644
--- a/src/shared/types/Course.ts
+++ b/src/shared/types/Course.ts
@@ -44,7 +44,12 @@ export type Semester = {
export class Course {
/** Every course has a uniqueId within UT's registrar system corresponding to each course section */
uniqueId!: number;
- /** This is the course number for a course, i.e CS 314 would be 314, MAL 306H would be 306H */
+ /**
+ * This is the course number for a course, i.e CS 314 would be 314, MAL 306H would be 306H.
+ * UT prefixes summer courses with f, s, n, or w:
+ * [f]irst term, [s]econd term, [n]ine week term, [w]hole term.
+ * So, the first term of PSY 301 over the summer would be 'f301'
+ */
number!: string;
/** The full name of the course, i.e. CS 314 Data Structures and Algorithms */
fullName!: string;
@@ -91,6 +96,46 @@ export class Course {
}
this.colors = course.colors ? structuredClone(course.colors) : getCourseColors('emerald', 500);
this.core = course.core ?? [];
+ if (course.semester.season === 'Summer') {
+ // A bug from and old version put the summer term in the course,
+ // so we need to handle that case
+ const { department, number } = Course.cleanSummerTerm(course.department, course.number);
+ this.department = department;
+ this.number = number;
+ }
+ }
+
+ /**
+ * Due to a bug in an older version, the summer term was included in the course department code,
+ * instead of the course number.
+ * UT prefixes summer courses with f, s, n, or w:
+ * [f]irst term, [s]econd term, [n]ine week term, [w]hole term
+ *
+ * @param department - The course department code, like 'C S'
+ * @param number - The course number, like '314H'
+ * @returns The properly formatted department and course number
+ * @example
+ * ```ts
+ * cleanSummerTerm('C S', '314H') // { department: 'C S', number: '314H' }
+ * cleanSummerTerm('P R', 'f378') // { department: 'P R', number: 'f378' }
+ * cleanSummerTerm('P R f', '378') // { department: 'P R', number: 'f378' }
+ * cleanSummerTerm('P S', 'n303') // { department: 'P S', number: 'n303' }
+ * cleanSummerTerm('P S n', '303') // { department: 'P S', number: 'n303' }
+ * ```
+ */
+ static cleanSummerTerm(department: string, number: string): { department: string; number: string } {
+ // UT prefixes summer courses with f, s, n, or w:
+ // [f]irst term, [s]econd term, [n]ine week term, [w]hole term
+ const summerTerm = department.match(/[fsnw]$/);
+
+ if (!summerTerm) {
+ return { department, number };
+ }
+
+ return {
+ department: department.slice(0, -1).trim(),
+ number: summerTerm[0] + number,
+ };
}
/**
@@ -111,6 +156,18 @@ export class Course {
return conflicts;
}
+
+ /**
+ * @returns The course number without the summer term
+ * @example
+ * ```ts
+ * const c = new Course({ number: 'f301', ... });
+ * c.getNumberWithoutTerm() // '301'
+ * ```
+ */
+ getNumberWithoutTerm(): string {
+ return this.number.replace(/^\D/, ''); // Remove nondigit at start, if it exists
+ }
}
/**
diff --git a/src/shared/types/tests/Course.test.ts b/src/shared/types/tests/Course.test.ts
new file mode 100644
index 00000000..efeed0a1
--- /dev/null
+++ b/src/shared/types/tests/Course.test.ts
@@ -0,0 +1,57 @@
+import { describe, expect, it } from 'vitest';
+
+import { Course } from '../Course';
+
+describe('Course::cleanSummerTerm', () => {
+ it("shouldn't affect already cleaned summer terms", () => {
+ const inputs = [
+ ['C S', '314H'],
+ ['P R', 'f378'],
+ ['P S', 'f303'],
+ ['WGS', 's301'],
+ ['S W', 'n360K'],
+ ['GOV', 'w312L'],
+ ['J', 's311F'],
+ ['J S', '311F'],
+ ] as const;
+ const expected = [
+ { department: 'C S', number: '314H' },
+ { department: 'P R', number: 'f378' },
+ { department: 'P S', number: 'f303' },
+ { department: 'WGS', number: 's301' },
+ { department: 'S W', number: 'n360K' },
+ { department: 'GOV', number: 'w312L' },
+ { department: 'J', number: 's311F' },
+ { department: 'J S', number: '311F' },
+ ];
+
+ const results = inputs.map(input => Course.cleanSummerTerm(input[0], input[1]));
+
+ expect(results).toEqual(expected);
+ });
+
+ it('should move summer term indicator to course number', () => {
+ const inputs = [
+ ['P R f', '378'],
+ ['P S f', '303'],
+ ['WGS s', '301'],
+ ['S W n', '360K'],
+ ['GOV w', '312L'],
+ ['J s', '311F'],
+ ['J S', '311F'],
+ ] as const;
+ const expected = [
+ { department: 'P R', number: 'f378' },
+ { department: 'P S', number: 'f303' },
+ { department: 'WGS', number: 's301' },
+ { department: 'S W', number: 'n360K' },
+ { department: 'GOV', number: 'w312L' },
+ { department: 'J', number: 's311F' },
+ { department: 'J S', number: '311F' },
+ ];
+
+ const results = inputs.map(input => Course.cleanSummerTerm(input[0], input[1]));
+
+ expect(results).toEqual(expected);
+ });
+});
diff --git a/src/stories/components/SortableList.stories.tsx b/src/stories/components/SortableList.stories.tsx
index 9d96b618..ec1fad0c 100644
--- a/src/stories/components/SortableList.stories.tsx
+++ b/src/stories/components/SortableList.stories.tsx
@@ -73,7 +73,7 @@ const generateCourses = (count: number): Course[] => {
const exampleCourses = generateCourses(numberOfCourses);
-type CourseWithId = Course & BaseItem;
+type CourseWithId = { course: Course } & BaseItem;
const meta = {
title: 'Components/Common/SortableList',
@@ -91,11 +91,10 @@ export const Default: Story = {
args: {
draggables: exampleCourses.map(course => ({
id: course.uniqueId,
- ...course,
- getConflicts: course.getConflicts,
+ course,
})),
onChange: () => {},
- renderItem: course => ,
+ renderItem: ({ id, course }) => ,
},
render: args => (
diff --git a/src/views/components/PopupMain.tsx b/src/views/components/PopupMain.tsx
index 1ee5864e..53d8b24f 100644
--- a/src/views/components/PopupMain.tsx
+++ b/src/views/components/PopupMain.tsx
@@ -155,15 +155,14 @@ export default function PopupMain(): JSX.Element {
({
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 => (
-
+ renderItem={({ id, course }) => (
+
)}
/>
)}
diff --git a/src/views/components/injected/CourseCatalogInjectedPopup/GradeDistribution.tsx b/src/views/components/injected/CourseCatalogInjectedPopup/GradeDistribution.tsx
index ff8ccfb9..2548107c 100644
--- a/src/views/components/injected/CourseCatalogInjectedPopup/GradeDistribution.tsx
+++ b/src/views/components/injected/CourseCatalogInjectedPopup/GradeDistribution.tsx
@@ -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
Grade Distribution for{' '}
- {course.department} {course.number}
+ {course.department} {course.getNumberWithoutTerm()}
)}
diff --git a/src/views/lib/CourseCatalogScraper.test.ts b/src/views/lib/CourseCatalogScraper.test.ts
new file mode 100644
index 00000000..bb713f86
--- /dev/null
+++ b/src/views/lib/CourseCatalogScraper.test.ts
@@ -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);
+ });
+});
diff --git a/src/views/lib/CourseCatalogScraper.ts b/src/views/lib/CourseCatalogScraper.ts
index cdcd9182..00bfb095 100644
--- a/src/views/lib/CourseCatalogScraper.ts
+++ b/src/views/lib/CourseCatalogScraper.ts
@@ -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];
}
diff --git a/src/views/lib/database/queryDistribution.ts b/src/views/lib/database/queryDistribution.ts
index 0af31d0d..29a687d5 100644
--- a/src/views/lib/database/queryDistribution.ts
+++ b/src/views/lib/database/queryDistribution.ts
@@ -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) {