diff --git a/openedx/core/lib/grade_utils.py b/openedx/core/lib/grade_utils.py index 601b994d13..2e7073e962 100644 --- a/openedx/core/lib/grade_utils.py +++ b/openedx/core/lib/grade_utils.py @@ -1,6 +1,7 @@ """ Helpers functions for grades and scores. """ +import math def compare_scores(earned1, possible1, earned2, possible2, treat_undefined_as_zero=False): @@ -42,3 +43,26 @@ def is_score_higher_or_equal(earned1, possible1, earned2, possible2, treat_undef """ is_higher_or_equal, _, _ = compare_scores(earned1, possible1, earned2, possible2, treat_undefined_as_zero) return is_higher_or_equal + + +def round_away_from_zero(number, digits=0): + """ + Round numbers using the 'away from zero' strategy as opposed to the + 'Banker's rounding strategy.' The strategy refers to how we round when + a number is half way between two numbers. eg. 0.5, 1.5, etc. In python 2 + positive numbers in this category would be rounded up and negative numbers + would be rounded down. ie. away from zero. In python 3 numbers round + towards even. So 0.5 would round to 0 but 1.5 would round to 2. + + See here for more on floating point rounding strategies: + https://en.wikipedia.org/wiki/IEEE_754#Rounding_rules + + We want to continue to round away from zero so that student grades remain + consistent and don't suddenly change. + """ + p = 10.0 ** digits + + if number >= 0: + return float(math.floor((number * p) + 0.5)) / p + else: + return float(math.ceil((number * p) - 0.5)) / p diff --git a/openedx/core/lib/tests/test_grade_utils.py b/openedx/core/lib/tests/test_grade_utils.py index 9c4f08216d..4d6dd00912 100644 --- a/openedx/core/lib/tests/test_grade_utils.py +++ b/openedx/core/lib/tests/test_grade_utils.py @@ -7,7 +7,7 @@ from unittest import TestCase import ddt -from ..grade_utils import compare_scores +from ..grade_utils import compare_scores, round_away_from_zero @ddt.ddt @@ -45,3 +45,15 @@ class TestGradeUtils(TestCase): assert is_higher is True assert 0 == percentage_1 assert 0 == percentage_2 + + @ddt.data( + (0.5, 1), + (1.45, 1.5, 1), + (-0.5, -1.0), + (-0.1, -0.0), + (0.1, 0.0), + (0.0, 0.0) + ) + @ddt.unpack + def test_round_away_from_zero(self, precise, expected_rounded_number, rounding_precision=0): + assert round_away_from_zero(precise, rounding_precision) == expected_rounded_number