-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
275 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
128 changes: 128 additions & 0 deletions
128
CanvasPlusPlayground/Features/Grades/GradeCalculator/CourseGradeCalculator.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} | ||
} |
115 changes: 115 additions & 0 deletions
115
CanvasPlusPlayground/Features/Grades/GradeCalculator/GradeCalculatorView.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} | ||
} |