|
1 | 1 | const functions = require('firebase-functions'); |
2 | 2 | const admin = require('firebase-admin'); |
| 3 | +const { getLetterGrade } = require('./utils/gradeUtils'); |
| 4 | +const { calculateExactGradeStatistics, generateGradeAnalysis } = require('./utils/calculationUtils'); |
3 | 5 |
|
4 | 6 | /** |
5 | 7 | * Cloud Function to calculate current grade based on submitted assignments |
@@ -60,164 +62,6 @@ exports.calculateCurrentGrade = functions.https.onCall(async (data, context) => |
60 | 62 | } |
61 | 63 | }); |
62 | 64 |
|
63 | | -/** |
64 | | - * Calculate grade statistics with precision |
65 | | - * @param {Object} data - Formatted data from formatDataForCalculation |
66 | | - * @returns {Object} Calculated statistics |
67 | | - */ |
68 | | -function calculateExactGradeStatistics(data) { |
69 | | - const { gradeWeights, completedAssignments, remainingAssignments } = data; |
70 | | - |
71 | | - // Initialize tracking variables |
72 | | - const categoryStats = {}; |
73 | | - let totalWeightCovered = 0; |
74 | | - |
75 | | - // Calculate stats for each category |
76 | | - gradeWeights.forEach(category => { |
77 | | - const categoryAssignments = completedAssignments.filter( |
78 | | - a => a.category === category.name |
79 | | - ); |
80 | | - |
81 | | - const categoryRemaining = remainingAssignments.filter( |
82 | | - a => a.category === category.name |
83 | | - ); |
84 | | - |
85 | | - // Calculate total points and max possible in this category |
86 | | - let totalPoints = 0; |
87 | | - let maxPoints = 0; |
88 | | - |
89 | | - categoryAssignments.forEach(assignment => { |
90 | | - // Handle numeric grades and special cases like "Dropped" |
91 | | - if (typeof assignment.grade === 'number') { |
92 | | - totalPoints += assignment.grade; |
93 | | - maxPoints += assignment.maxPoints || 100; |
94 | | - } else if (assignment.grade !== 'Dropped') { |
95 | | - const numericGrade = parseFloat(assignment.grade); |
96 | | - if (!isNaN(numericGrade)) { |
97 | | - totalPoints += numericGrade; |
98 | | - maxPoints += assignment.maxPoints || 100; |
99 | | - } |
100 | | - } |
101 | | - }); |
102 | | - |
103 | | - // Calculate category average |
104 | | - const categoryAverage = maxPoints > 0 ? (totalPoints / maxPoints) * 100 : null; |
105 | | - |
106 | | - // Store stats |
107 | | - categoryStats[category.name] = { |
108 | | - completed: categoryAssignments, |
109 | | - remaining: categoryRemaining, |
110 | | - totalPoints, |
111 | | - maxPoints, |
112 | | - average: categoryAverage, |
113 | | - weight: category.weight |
114 | | - }; |
115 | | - |
116 | | - // Add to total weight if we have assignments in this category |
117 | | - if (maxPoints > 0) { |
118 | | - totalWeightCovered += category.weight; |
119 | | - } |
120 | | - }); |
121 | | - |
122 | | - // Calculate overall current grade |
123 | | - let currentGradeWeighted = 0; |
124 | | - |
125 | | - Object.values(categoryStats).forEach(stats => { |
126 | | - if (stats.average !== null) { |
127 | | - // Scale by weight |
128 | | - currentGradeWeighted += (stats.average / 100) * stats.weight; |
129 | | - } |
130 | | - }); |
131 | | - |
132 | | - // Normalize by covered weight if needed |
133 | | - const currentGrade = totalWeightCovered > 0 |
134 | | - ? (currentGradeWeighted / totalWeightCovered) * 100 |
135 | | - : 0; |
136 | | - |
137 | | - // Calculate max possible grade (if all remaining is 100%) |
138 | | - let maxPossibleGrade = currentGradeWeighted; |
139 | | - let remainingWeight = 0; |
140 | | - |
141 | | - Object.values(categoryStats).forEach(stats => { |
142 | | - // Count weight for categories with remaining assignments |
143 | | - if (stats.remaining.length > 0) { |
144 | | - const categoryRemainingWeight = stats.weight * (stats.remaining.length / |
145 | | - (stats.completed.length + stats.remaining.length || 1)); |
146 | | - |
147 | | - remainingWeight += categoryRemainingWeight; |
148 | | - maxPossibleGrade += categoryRemainingWeight; |
149 | | - } |
150 | | - }); |
151 | | - |
152 | | - // Normalize max grade |
153 | | - maxPossibleGrade = totalWeightCovered > 0 |
154 | | - ? (maxPossibleGrade / (totalWeightCovered + remainingWeight)) * 100 |
155 | | - : 100; |
156 | | - |
157 | | - // Min grade (if all remaining is 0%) |
158 | | - const minPossibleGrade = totalWeightCovered > 0 |
159 | | - ? (currentGradeWeighted / (totalWeightCovered + remainingWeight)) * 100 |
160 | | - : 0; |
161 | | - |
162 | | - // Format for return |
163 | | - return { |
164 | | - current_grade: currentGrade, |
165 | | - current_percentage: currentGrade, |
166 | | - letter_grade: getLetterGrade(currentGrade), |
167 | | - max_possible_grade: maxPossibleGrade, |
168 | | - min_possible_grade: minPossibleGrade, |
169 | | - categorized_grades: categoryStats, |
170 | | - analysis: generateGradeAnalysis({ |
171 | | - currentGrade, |
172 | | - maxPossibleGrade, |
173 | | - minPossibleGrade, |
174 | | - letterGrade: getLetterGrade(currentGrade), |
175 | | - categorizedGrades: categoryStats |
176 | | - }) |
177 | | - }; |
178 | | -} |
179 | | - |
180 | | -/** |
181 | | - * Get letter grade from numeric value |
182 | | - * @param {number} grade - Numeric grade |
183 | | - * @returns {string} Letter grade |
184 | | - */ |
185 | | -function getLetterGrade(grade) { |
186 | | - if (grade >= 93) return 'A'; |
187 | | - if (grade >= 90) return 'A-'; |
188 | | - if (grade >= 87) return 'B+'; |
189 | | - if (grade >= 83) return 'B'; |
190 | | - if (grade >= 80) return 'B-'; |
191 | | - if (grade >= 77) return 'C+'; |
192 | | - if (grade >= 73) return 'C'; |
193 | | - if (grade >= 70) return 'C-'; |
194 | | - if (grade >= 67) return 'D+'; |
195 | | - if (grade >= 63) return 'D'; |
196 | | - if (grade >= 60) return 'D-'; |
197 | | - return 'F'; |
198 | | -} |
199 | | - |
200 | | -/** |
201 | | - * Generate natural language analysis of grade stats |
202 | | - * @param {Object} stats - Grade statistics |
203 | | - * @returns {string} Analysis text |
204 | | - */ |
205 | | -function generateGradeAnalysis(stats) { |
206 | | - const { currentGrade, maxPossibleGrade, minPossibleGrade, letterGrade } = stats; |
207 | | - const analysis = []; |
208 | | - |
209 | | - analysis.push(`Current grade is ${currentGrade.toFixed(1)}% (${letterGrade})`); |
210 | | - |
211 | | - if (maxPossibleGrade > currentGrade) { |
212 | | - analysis.push(`Maximum possible grade is ${maxPossibleGrade.toFixed(1)}%`); |
213 | | - } |
214 | | - |
215 | | - if (minPossibleGrade < currentGrade) { |
216 | | - analysis.push(`Minimum possible grade is ${minPossibleGrade.toFixed(1)}%`); |
217 | | - } |
218 | | - |
219 | | - return analysis.join('. '); |
220 | | -} |
221 | 65 |
|
222 | 66 | /** |
223 | 67 | * Fetch structured data from Firestore |
|
0 commit comments