From 2c2b49050b9ddc68f4fdefb4443c39c13e2d7673 Mon Sep 17 00:00:00 2001 From: Jonas July Date: Wed, 8 May 2024 14:11:10 +0200 Subject: [PATCH 1/2] Improve conversion between total_points and grades While the students are graded with points in 0.25 intervals, the actual grades are non-linear and discrete. This requires a conversion step. Since the grading steps are known multiples of 10, a fast approach is to calculate the lower bound and use that value for the conversion table. While this operation is O(1), it is fragile and error-prone due to the use of modular arithmetic and the requirement to finally yield exact integers. The grading boundaries create a nicely increasing list, so a more general approach is to bisect it and find the lower and upper bounds. It's theoretically a bit slower with O(log(n)), but the small size of the grading table means that there is no significant impact. This approach also makes it trivially simple to calculate the necessary points to the next grade - adding an infinite sentinel ensures that there is always an upper bound that can't be reached, so people with a close to perfect grade don't get marked as close to the next higher grade. The database has to ensure that the points are in the valid range. --- bp/models.py | 52 ++++++++++++++++++++++++++++++++++------------------ 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/bp/models.py b/bp/models.py index 851819f..db64aa5 100644 --- a/bp/models.py +++ b/bp/models.py @@ -1,3 +1,4 @@ +import bisect import datetime import json from datetime import timedelta, date @@ -126,30 +127,45 @@ def grade_complete(self): def total_points(self): return sum([self.ag_grade_points_value, self.pitch_grade_points_value, self.docs_grade_points_value]) + @staticmethod + def grade_with_grade_differences(points): + grades = { + 0 : 5.0, + 100: 4.0, + 110: 3.7, + 120: 3.3, + 130: 3.0, + 140: 2.7, + 150: 2.3, + 160: 2.0, + 170: 1.7, + 180: 1.3, + 190: 1.0 + } + grade_lower = sorted((*grades.keys(), Decimal("Infinity"))) + + # lower_bound = max((g for g in grade_lower if g <= points)) + # upper_bound = min((g for g in grade_lower if g > points)) + + ip = bisect.bisect(grade_lower, points) + lower_bound, upper_bound = grade_lower[ip - 1], grade_lower[ip] + + return grades[lower_bound], (lower_bound - points, upper_bound - points) + + @staticmethod + def upper_grade_difference(points): + """Absolute number of points necessary for the next higher grade. Can be infinite.""" + _, (_, upper_grade_difference) = Project.grade_with_grade_differences(points) + return upper_grade_difference + @property def grade(self): - points = round(self.total_points, 0) - round(self.total_points, 0) % 10 - if points < 100: - grade = 5.0 - else: - grades = { - 100: 4.0, - 110: 3.7, - 120: 3.3, - 130: 3.0, - 140: 2.7, - 150: 2.3, - 160: 2.0, - 170: 1.7, - 180: 1.3, - 190: 1.0 - } - grade = grades[points] + grade, _ = self.grade_with_grade_differences(self.total_points) return grade @property def grade_close_to_higher_grade(self): - return self.grade_complete and self.total_points > 100 and self.total_points % 10 > (10 - 2) and self.grade != 1.0 + return self.grade_complete and self.total_points > 100 and Project.upper_grade_difference(self.total_points) < 2 @property def ag_grade_points_value(self): From 3cab755d217405901ddf74082f87cbe398a6978b Mon Sep 17 00:00:00 2001 From: Jonas July Date: Wed, 8 May 2024 14:26:49 +0200 Subject: [PATCH 2/2] Fortify grade conversion Using -infinity instead of 0 for the lower bound ensures that negative points don't crash the server. This is similar to previous behaviour. If the points are close to 100, there is no reason not to show it as close to the next grade; this was only neessary to workaround the previous conversion which wasn't as flexible. --- bp/models.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/bp/models.py b/bp/models.py index db64aa5..2c585dd 100644 --- a/bp/models.py +++ b/bp/models.py @@ -129,20 +129,21 @@ def total_points(self): @staticmethod def grade_with_grade_differences(points): + inf = Decimal("Infinity") grades = { - 0 : 5.0, - 100: 4.0, - 110: 3.7, - 120: 3.3, - 130: 3.0, - 140: 2.7, - 150: 2.3, - 160: 2.0, - 170: 1.7, - 180: 1.3, - 190: 1.0 + -inf: 5.0, + 100: 4.0, + 110: 3.7, + 120: 3.3, + 130: 3.0, + 140: 2.7, + 150: 2.3, + 160: 2.0, + 170: 1.7, + 180: 1.3, + 190: 1.0 } - grade_lower = sorted((*grades.keys(), Decimal("Infinity"))) + grade_lower = sorted((*grades.keys(), inf)) # lower_bound = max((g for g in grade_lower if g <= points)) # upper_bound = min((g for g in grade_lower if g > points)) @@ -165,7 +166,7 @@ def grade(self): @property def grade_close_to_higher_grade(self): - return self.grade_complete and self.total_points > 100 and Project.upper_grade_difference(self.total_points) < 2 + return self.grade_complete and Project.upper_grade_difference(self.total_points) < 2 @property def ag_grade_points_value(self):