Skip to content

Commit

Permalink
Initial Grade Calculator
Browse files Browse the repository at this point in the history
  • Loading branch information
rahulon12 committed Dec 26, 2024
1 parent 1d43a3d commit bb58b72
Show file tree
Hide file tree
Showing 5 changed files with 275 additions and 0 deletions.
16 changes: 16 additions & 0 deletions CanvasPlusPlayground.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@
B7F9503B2D127AD0004BB470 /* Profile.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7F9503A2D127AD0004BB470 /* Profile.swift */; };
B7F9503D2D12ADAE004BB470 /* Submission.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7F9503C2D12ADAE004BB470 /* Submission.swift */; };
B7F9503F2D133435004BB470 /* AssignmentGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7F9503E2D133435004BB470 /* AssignmentGroup.swift */; };
B7F950412D149290004BB470 /* CourseGradeCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7F950402D14928C004BB470 /* CourseGradeCalculator.swift */; };
B7F950432D149C5E004BB470 /* GradeCalculatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7F950422D149C5E004BB470 /* GradeCalculatorView.swift */; };
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
Expand Down Expand Up @@ -198,6 +200,8 @@
B7F9503A2D127AD0004BB470 /* Profile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Profile.swift; sourceTree = "<group>"; };
B7F9503C2D12ADAE004BB470 /* Submission.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Submission.swift; sourceTree = "<group>"; };
B7F9503E2D133435004BB470 /* AssignmentGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssignmentGroup.swift; sourceTree = "<group>"; };
B7F950402D14928C004BB470 /* CourseGradeCalculator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseGradeCalculator.swift; sourceTree = "<group>"; };
B7F950422D149C5E004BB470 /* GradeCalculatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradeCalculatorView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -442,6 +446,7 @@
A324BA5F2D0798A1005F53FA /* Grades */ = {
isa = PBXGroup;
children = (
B7F950442D149C6E004BB470 /* GradeCalculator */,
7F8535732C98DE3C0023E384 /* CourseGradeView.swift */,
A31A81E62D0CCB69003C37EB /* GradesViewModel.swift */,
);
Expand Down Expand Up @@ -536,6 +541,15 @@
path = Profile;
sourceTree = "<group>";
};
B7F950442D149C6E004BB470 /* GradeCalculator */ = {
isa = PBXGroup;
children = (
B7F950422D149C5E004BB470 /* GradeCalculatorView.swift */,
B7F950402D14928C004BB470 /* CourseGradeCalculator.swift */,
);
path = GradeCalculator;
sourceTree = "<group>";
};
/* End PBXGroup section */

/* Begin PBXNativeTarget section */
Expand Down Expand Up @@ -652,13 +666,15 @@
B53D95A22CA0A22A00647EE9 /* PeopleView.swift in Sources */,
A3049B752D15DC1C002F3166 /* GetCoursesRequest.swift in Sources */,
B7F9503F2D133435004BB470 /* AssignmentGroup.swift in Sources */,
B7F950432D149C5E004BB470 /* GradeCalculatorView.swift in Sources */,
B76455042C8DF61B002DF00E /* CourseView.swift in Sources */,
A3049B682D0F3ECA002F3166 /* QuizzesViewModel.swift in Sources */,
9B92A4252C93856100C21CFC /* CourseAnnouncementManager.swift in Sources */,
B76455052C8DF61B002DF00E /* SetupView.swift in Sources */,
A3049B772D15DC36002F3166 /* GetCourseRequest.swift in Sources */,
B7AD54F32CD41D7900FB09BB /* LLMEvaluator.swift in Sources */,
B7AD54F42CD41D7900FB09BB /* Models.swift in Sources */,
B7F950412D149290004BB470 /* CourseGradeCalculator.swift in Sources */,
B7AD54F52CD41D7900FB09BB /* Data.swift in Sources */,
A3049B852D161455002F3166 /* GetEnrollmentsRequest.swift in Sources */,
A3D161472D169C0A004055FB /* APIRequest+Network.swift in Sources */,
Expand Down
4 changes: 4 additions & 0 deletions CanvasPlusPlayground/Common/Utilities/String+Numbers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,8 @@ extension Double {
var toInt: Int {
Int(self)
}

func rounded(toPlaces places: Int) -> String {
String(format: "%.\(places)f", self)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ struct CourseAssignmentsView: View {
@State private var assignmentManager: CourseAssignmentManager

@State private var isLoadingAssignments = true
@State private var showGradeCalculator: Bool = false

init(course: Course, showGrades: Bool = false) {
self.course = course
Expand Down Expand Up @@ -43,6 +44,17 @@ struct CourseAssignmentsView: View {
}
.statusToolbarItem("Assignments", isVisible: isLoadingAssignments)
.navigationTitle(course.displayName)
.sheet(isPresented: $showGradeCalculator) {
NavigationStack {
GradeCalculatorView(
assignmentGroups: assignmentManager.assignmentGroups
)
}
}
.toolbar {
Button("Calculate Grades") { showGradeCalculator = true }
.disabled(assignmentManager.assignmentGroups.isEmpty)
}
}

private func loadAssignments() async {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
//
// CourseGradeCalculator.swift
// CanvasPlusPlayground
//
// Created by Rahul on 12/19/24.
//

import SwiftUI

@Observable
class CourseGradeCalculatorBridge {
typealias GradeCalculatorAssignmentGroup = CourseGradeCalculator.GradeCalculatorAssignmentGroup

private(set) var _calculator: CourseGradeCalculator

var assignmentGroups: [GradeCalculatorAssignmentGroup] {
get {
_calculator.assignmentGroups
}

set {
_calculator.assignmentGroups = newValue
}
}

var finalGrade: String {
_calculator.finalGrade
}

var expandedAssignmentGroups: [GradeCalculatorAssignmentGroup: Bool] = [:]

init(from assignmentGroups: [AssignmentGroup]) {
_calculator = CourseGradeCalculator(from: assignmentGroups)
expandedAssignmentGroups = Dictionary(
uniqueKeysWithValues: _calculator.assignmentGroups.lazy.map { ($0, true) }
)
}
}

struct CourseGradeCalculator {
struct GradeCalculatorAssignmentGroup: Identifiable, Hashable {
let id: Int
let name: String
var assignments: [GradeCalculatorAssignment]
var groupWeight: Double

var assignmentWeight: Double {
groupWeight / Double(assignments.count)
}

var displayWeight: String {
"\((groupWeight * 100.0).truncatingTrailingZeros)%"
}
}

struct GradeCalculatorAssignment: Identifiable, Hashable {
let id: Int
let name: String
var weight: Double?
var pointsPossible: Double?
var score: Double?
}

var assignmentGroups: [GradeCalculatorAssignmentGroup]

var finalGrade: String {
var grade = calculateFinalGrade()
if grade.isNaN {
grade = 0
}

return Double(grade * 100.0).rounded(toPlaces: 2) + "%"
}

init(from canvasAssignmentGroup: [AssignmentGroup]) {
assignmentGroups = canvasAssignmentGroup.map { group in
GradeCalculatorAssignmentGroup(
id: group.id ?? UUID().hashValue,
name: group.name ?? "",
assignments: group.assignments?.map { assignment in
GradeCalculatorAssignment(
id: assignment.id,
name: assignment.name ?? "",
weight: nil,
pointsPossible: assignment.pointsPossible ?? 0,
score: assignment.submission?.score
)
} ?? [],
groupWeight: (Double(group.groupWeight ?? 0) / 100.0)
)
}
}

// MARK: - Private
private func calculateFinalGrade() -> Double {
var totalWeight = assignmentGroups.reduce(0.0) { total, group in
total + group.groupWeight
}

// Groups are unweighted
if totalWeight == 0 {
var totalPossible: Double = 0
let earned: Double = assignmentGroups.reduce(0) { total, group in
total + group.assignments.reduce(0) { total, assignment in
guard let score = assignment.score, let pointsPossible = assignment.pointsPossible else {
return total
}

totalPossible += pointsPossible

return Double(total + score)
}
}

return earned / totalPossible
} else {
return assignmentGroups.reduce(0) { total, group in
total + group.assignments.reduce(0) { total, assignment in
guard let score = assignment.score, let pointsPossible = assignment.pointsPossible else {
return total
}

return total + (score / pointsPossible) * (assignment.weight ?? group.assignmentWeight)
}
} / totalWeight
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
//
// GradeCalculatorView.swift
// CanvasPlusPlayground
//
// Created by Rahul on 12/19/24.
//

import SwiftUI

struct GradeCalculatorView: View {
@Environment(\.dismiss) private var dismiss
@State private var gradeCalculator: CourseGradeCalculatorBridge

@FocusState private var assignmentRowFocus: CourseGradeCalculator.GradeCalculatorAssignment?

init(assignmentGroups: [AssignmentGroup]) {
_gradeCalculator = .init(
initialValue: CourseGradeCalculatorBridge(from: assignmentGroups)
)
}

var body: some View {
List($gradeCalculator.assignmentGroups, id: \.id) { $group in
DisclosureGroup(
isExpanded: isExpanded(for: group)
) {
ForEach($group.assignments, id: \.id) { $assignment in
assignmentRow(for: $assignment)
}
} label: {
groupHeader(for: group)
}
}
#if os(macOS)
.frame(width: 600, height: 400)
#endif
.navigationTitle("Calculate Grades")
.toolbar {
ToolbarItem(placement: .destructiveAction) {
Text("Total: \(gradeCalculator.finalGrade)")
.contentTransition(.numericText())
.animation(.default, value: gradeCalculator.finalGrade)
}

ToolbarItem(placement: .cancellationAction) {
Button {
dismiss()
} label: {
#if os(macOS)
Text("Done")
#else
Image(systemName: "xmark")
#endif
}
.keyboardShortcut(assignmentRowFocus == nil ? .defaultAction : .none)
}

ToolbarItemGroup(placement: .keyboard) {
Spacer()
Button("Done") {
assignmentRowFocus = nil
}
.bold()
}
}
}

private func groupHeader(for group: CourseGradeCalculatorBridge.GradeCalculatorAssignmentGroup) -> some View {
HStack {
Text(group.name)
Spacer()
Text("\(group.displayWeight)")
}
.bold()
.padding(4)
}

private func assignmentRow(for assignment: Binding<CourseGradeCalculator.GradeCalculatorAssignment>) -> some View {
HStack {
Text(assignment.wrappedValue.name)

Spacer()

TextField(
"Score",
value: assignment.score,
format: .number
)
.focused($assignmentRowFocus, equals: assignment.wrappedValue)
.fixedSize()
.font(.title3)
.fontWeight(.semibold)
.foregroundStyle(.tint)
#if os(iOS)
.keyboardType(.numberPad)
#endif

Text(
" / " +
"\(assignment.wrappedValue.pointsPossible?.truncatingTrailingZeros ?? "-")"
)
}
.padding(.vertical, 4)
}

private func isExpanded(
for group: CourseGradeCalculatorBridge.GradeCalculatorAssignmentGroup
) -> Binding<Bool> {
.init {
gradeCalculator.expandedAssignmentGroups[group, default: true]
} set: { newValue in
gradeCalculator.expandedAssignmentGroups[group] = newValue
}
}
}

0 comments on commit bb58b72

Please sign in to comment.