Skip to content

Commit

Permalink
Added cumulative GPA and GPA by semester calculations with course grades
Browse files Browse the repository at this point in the history
  • Loading branch information
kevinmonisit committed Jan 16, 2025
1 parent 6af5749 commit 9609a74
Show file tree
Hide file tree
Showing 8 changed files with 291 additions and 26 deletions.
7 changes: 6 additions & 1 deletion app/features/middlePanel/dashboard/ScheduleBoard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,12 @@ export function ScheduleBoard({
label={
minimal
? undefined
: `${useScheduleStore.getState().semesterByID[containerId]?.title || containerId} (${calculateSemesterCredits(coursesBySemesterID[containerId] || [], courses)} credits, Total: ${calculateRunningCredits(semesterOrder, coursesBySemesterID, courses, containerId)})`
: `${
useScheduleStore.getState().semesterByID[containerId]
?.title || containerId
}
(${calculateSemesterCredits(coursesBySemesterID[containerId] || [], courses)} credits,
Total: ${calculateRunningCredits(semesterOrder, coursesBySemesterID, courses, containerId)})`
}
columns={columns}
items={coursesBySemesterID[containerId]}
Expand Down
11 changes: 10 additions & 1 deletion app/features/middlePanel/dashboard/components/ui/Item/Item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Handle, Remove } from './components';

import styles from './Item.module.scss';
import useAuxiliaryStore from '@/lib/hooks/stores/useAuxiliaryStore';
import { useSettingsStore } from '@/lib/hooks/stores/useSettingsStore';
import { CourseID } from '@/types/models';
import { useScheduleStore } from '@/lib/hooks/stores/useScheduleStore';
import CoreList from '@/app/components/CoreList';
Expand Down Expand Up @@ -140,7 +141,15 @@ export const Item = React.memo(
}}
>
<div className='flex flex-col gap-2'>
<div>{value}</div>
<div className='flex items-center gap-2'>
<div>{value}</div>
{course?.grade &&
!useSettingsStore.getState().visuals.showGrades && (
<div className='text-sm text-gray-600'>
({course.grade})
</div>
)}
</div>
{showCores && course && course.cores.length > 0 && (
<div>
<CoreList color='blue' cores={course.cores} />
Expand Down
13 changes: 13 additions & 0 deletions app/features/middlePanel/dashboard/utils/credits/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ import {
CoursesBySemesterID,
} from '@/types/models';

/**
* TODO:
* I believe that this should function should just take in the courses array.
* Why does it need to take in an array of course IDs and then a courses map?
*
* This seems a bit inelegant.
*/
export function calculateSemesterCredits(
courseIds: CourseID[],
courses: Record<CourseID, Course>
Expand All @@ -15,6 +22,12 @@ export function calculateSemesterCredits(
}, 0);
}

/**
* TODO:
* The coursesBySemesterID variable needs to be deleted.
* It is redundant and a duplicate data structure,
* and I'm not sure why I've made it this way.
*/
export function calculateRunningCredits(
semesterOrder: SemesterID[],
coursesBySemesterID: CoursesBySemesterID,
Expand Down
73 changes: 73 additions & 0 deletions app/features/middlePanel/dashboard/utils/gpa/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { Course, CourseID } from '@/types/models';
import { GradePointMap } from '@/lib/hooks/stores/useSettingsStore';

/**
* Calculate the GPA for a single course based on its grade and the grade point system
* Returns null if the grade is Pass/Fail, indicating it shouldn't affect GPA
*/
export function calculateCourseGPA(
course: Course,
gradePoints: GradePointMap
): number | null {
if (!course.grade) return 0;

// Grade doesn't exist in the system
if (!(course.grade in gradePoints)) return 0;

// Pass/Fail grades return null to indicate they don't affect GPA
return gradePoints[course.grade];
}

/**
* Calculate the GPA for a semester based on its courses and the grade point system
* Ignores Pass/Fail grades in the calculation
*/
export function calculateSemesterGPA(
courseIds: CourseID[],
courses: Record<CourseID, Course>,
gradePoints: GradePointMap
): number {
let totalPoints = 0;
let totalCredits = 0;

courseIds.forEach((courseId) => {
const course = courses[courseId];
if (course && course.grade) {
const courseGPA = calculateCourseGPA(course, gradePoints);
// Only include in GPA calculation if it's not Pass/Fail
if (courseGPA == null) return;

totalPoints += courseGPA * course.credits;
totalCredits += course.credits;
}
});

return totalCredits > 0 ? totalPoints / totalCredits : 0;
}

/**
* Calculate the cumulative GPA across multiple semesters
* Ignores Pass/Fail grades in the calculation
*/
export function calculateCumulativeGPA(
allCourseIds: CourseID[],
courses: Record<CourseID, Course>,
gradePoints: GradePointMap
): number {
let totalPoints = 0;
let totalCredits = 0;

allCourseIds.forEach((courseId) => {
const course = courses[courseId];
if (course && course.grade) {
const courseGPA = calculateCourseGPA(course, gradePoints);
// Only include in GPA calculation if it's not Pass/Fail
if (courseGPA == null) return;

totalPoints += courseGPA * course.credits;
totalCredits += course.credits;
}
});

return totalCredits > 0 ? totalPoints / totalCredits : 0;
}
54 changes: 40 additions & 14 deletions app/features/rightPanel/courseInfoDisplay/CourseInfo.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useScheduleStore } from '@/lib/hooks/stores/useScheduleStore';
import { useState } from 'react';
import { useScheduleStore } from '@/lib/hooks/stores/useScheduleStore';
import CoreInput from '@/app/components/CoreInput';
import NotesArea from './components/NotesArea';
import { useSettingsStore } from '@/lib/hooks/stores/useSettingsStore';

interface CourseInfoProps {
id: string;
Expand All @@ -11,26 +11,29 @@ export default function CourseInfo({ id }: CourseInfoProps) {
const currentCourse = useScheduleStore((state) => state.courses[id]);
const globalCores = useScheduleStore((state) => state.globalCores);
const updateCourse = useScheduleStore((state) => state.updateCourse);
const gradePoints = useSettingsStore((state) => state.gradePoints);
const [isEditing, setIsEditing] = useState(false);
const [editForm, setEditForm] = useState({
name: '',
credits: 0,
cores: [] as string[],
grade: null as string | null,
});
const [currentCore, setCurrentCore] = useState('');

if (!currentCourse) {
return <div className='p-4 text-gray-500'>Loading course... {id}</div>;
}

const { name, credits, cores } = currentCourse;
const { name, credits, cores, grade } = currentCourse;

const handleEditToggle = () => {
if (!isEditing) {
setEditForm({
name,
credits,
cores: cores || [],
grade,
});
}
setIsEditing(!isEditing);
Expand All @@ -41,6 +44,7 @@ export default function CourseInfo({ id }: CourseInfoProps) {
name: editForm.name,
credits: editForm.credits,
cores: editForm.cores,
grade: editForm.grade,
});
setIsEditing(false);
};
Expand Down Expand Up @@ -77,9 +81,42 @@ export default function CourseInfo({ id }: CourseInfoProps) {
credits
)}
</p>
<p>
<span className='font-medium'>Grade:</span>{' '}
{isEditing ? (
<select
value={editForm.grade || ''}
onChange={(e) =>
setEditForm({
...editForm,
grade: e.target.value || null,
})
}
className='select select-bordered select-sm'
>
<option value=''>None</option>
{Object.keys(gradePoints).map((grade) => (
<option key={grade} value={grade}>
{grade}
</option>
))}
</select>
) : (
grade || 'N/A'
)}
</p>
</div>
</div>

<div className='flex justify-center'>
<button
onClick={isEditing ? handleSubmit : handleEditToggle}
className='max-w-[200px] rounded-lg bg-gray-200 px-4 py-2 transition-colors hover:bg-gray-300'
>
{isEditing ? 'Save Changes' : 'Edit Course'}
</button>
</div>

<div>
<h3 className='mb-2 text-sm font-medium'>Cores:</h3>
{isEditing ? (
Expand Down Expand Up @@ -114,17 +151,6 @@ export default function CourseInfo({ id }: CourseInfoProps) {
</div>
)}
</div>

<div className='flex justify-center'>
<button
onClick={isEditing ? handleSubmit : handleEditToggle}
className='max-w-[200px] rounded-lg bg-gray-200 px-4 py-2 transition-colors hover:bg-gray-300'
>
{isEditing ? 'Save Changes' : 'Edit Course'}
</button>
</div>

<NotesArea id={id} />
</div>
);
}
95 changes: 85 additions & 10 deletions app/features/rightPanel/courseInfoDisplay/SemesterInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,38 @@ import {
calculateSemesterCredits,
calculateRunningCredits,
} from '@/app/features/middlePanel/dashboard/utils/credits';
import {
calculateSemesterGPA,
calculateCumulativeGPA,
} from '@/app/features/middlePanel/dashboard/utils/gpa';
import CoreList from '@/app/components/CoreList';
import { useSettingsStore } from '@/lib/hooks/stores/useSettingsStore';
import { CourseID } from '@/types/models';

interface SemesterInfoProps {
id: string;
}

export default function SemesterInfo({ id }: SemesterInfoProps) {
/**
* TODO:
* Some of these variables need to be renamed as they confuse me...
*
* What is semesterOrder again? I feel like it should be
* orderedArrayOfSemesters or orderArrayOfSemesterIDs
*/
const coursesBySemesterID = useScheduleStore(
(state) => state.coursesBySemesterID
);
const globalCoursesMap = useScheduleStore((state) => state.courses);

const currentSemester = useScheduleStore((state) => state.semesterByID[id]);
const updateSemester = useScheduleStore((state) => state.updateSemester);
const removeSemester = useScheduleStore((state) => state.removeSemester);
const setCurrentInfo = useAuxiliaryStore((state) => state.setCurrentInfo);
const courses = useScheduleStore((state) => state.courses);
const coursesBySemesterID = useScheduleStore(
(state) => state.coursesBySemesterID
);

const semesterOrder = useScheduleStore((state) => state.semesterOrder);
const gradePoints = useSettingsStore((state) => state.gradePoints);
const [isEditing, setIsEditing] = useState(false);
const [editForm, setEditForm] = useState({
title: '',
Expand All @@ -32,22 +48,65 @@ export default function SemesterInfo({ id }: SemesterInfoProps) {
return <div className='p-4 text-gray-500'>Loading semester... {id}</div>;
}

/**
* TODO: THIS NEEDS TO BE REFACTORED
*
* When coursesIdsOfCurrentSemester gets rewritten properly to
* coursesOfCurrentSemester = semester.courses,
*
* it will show something erroneous. That is because semester.courses
* IS NEVER UPDATED when we update the board. The only thing updated
* is the coursesBySemesterID.
*
* This is why data shouldn't be duplicated so much.
*/
const courseIDsOfCurrentSemester = coursesBySemesterID[id];
const { title } = currentSemester;
const semesterCredits = calculateSemesterCredits(
coursesBySemesterID[id] || [],
courses
courseIDsOfCurrentSemester,
globalCoursesMap
);

const totalCredits = calculateRunningCredits(
semesterOrder,
coursesBySemesterID,
courses,
// TODO: this is gross. rewrite this in a refactor.
{ [id]: courseIDsOfCurrentSemester },
globalCoursesMap,
id
);

const semesterGPA = calculateSemesterGPA(
courseIDsOfCurrentSemester,
globalCoursesMap,
gradePoints
);

// Get all course IDs up to and including current semester
const currentSemesterIndex = semesterOrder.indexOf(id);
const coursesUpToCurrent = semesterOrder
.slice(0, currentSemesterIndex + 1)
.flatMap((semId) => coursesBySemesterID[semId]);

// Check if any course up to current semester has N/A grade
const hasUngradedCumulative = coursesUpToCurrent.some(
(courseId: CourseID) => globalCoursesMap[courseId]?.grade === null
);

const cumulativeGPA = calculateCumulativeGPA(
coursesUpToCurrent,
globalCoursesMap,
gradePoints
);

// Check if any course in the semester has N/A grade
const hasUngraded = courseIDsOfCurrentSemester.some(
(courseId: CourseID) => globalCoursesMap[courseId]?.grade === null
);

// Get all cores fulfilled in this semester
const semesterCores = new Set<string>();
(coursesBySemesterID[id] || []).forEach((courseId) => {
const course = courses[courseId];
courseIDsOfCurrentSemester.forEach((courseId: CourseID) => {
const course = globalCoursesMap[courseId];
if (course?.cores) {
course.cores.forEach((core) => semesterCores.add(core));
}
Expand Down Expand Up @@ -124,6 +183,22 @@ export default function SemesterInfo({ id }: SemesterInfoProps) {
<div>
<span className='font-medium'>Total Credits:</span> {totalCredits}
</div>
<div>
<span className='font-medium'>Semester GPA:</span>{' '}
{hasUngraded ? (
<strong>missing grades</strong>
) : (
semesterGPA.toFixed(2)
)}
</div>
<div>
<span className='font-medium'>Cumulative GPA:</span>{' '}
{hasUngradedCumulative ? (
<strong>N/A</strong>
) : (
cumulativeGPA.toFixed(2)
)}
</div>
{semesterCores.size > 0 && (
<div>
<span className='mb-1 block font-medium'>Cores Fulfilled:</span>
Expand Down
Loading

0 comments on commit 9609a74

Please sign in to comment.