diff --git a/api/utils/ira_calculator.py b/api/utils/ira_calculator.py new file mode 100644 index 0000000..000f3a8 --- /dev/null +++ b/api/utils/ira_calculator.py @@ -0,0 +1,93 @@ +from typing import TypedDict + +class Discipline(TypedDict): + """ + TypedDict que define uma disciplina. + + Attributes: + grade: a menção obtida pelo aluno na disciplina. + number_of_credits: a quantidade de créditos que a disciplina tem. + semester: qual o semestre em que o aluno realizou a disciplina. O valor mínimo é 1, e o máximo é 6. + """ + grade: str + number_of_credits: int + semester: int + +class IraCalculator: + """ + Classe que calcula o valor do IRA a partir de um conjunto de disciplinas. + + Atualmente, o cálculo está sendo baseado com base no + recurso do seguinte link: 'https://deg.unb.br/images/legislacao/resolucao_ceg_0001_2020.pdf' + + Para uma disciplina, nos interessam as seguintes variáveis: + E -> Equivalência da menção de disciplina (isto é, SS=5, MS=4,..., SR=0); + C -> Número de créditos daquela disciplina; + S -> Semestre em que aquela disciplina foi cursada, sendo 6 o seu valor máximo. + + Realiza-se o somatório de E*C*S para cada disciplina, e depois divide-se pelo somatório de C*S para cada uma delas. + """ + + def __init__(self) -> None: + self.grade_map = { + 'SS': 5, + 'MS': 4, + 'MM': 3, + 'MI': 2, + 'II': 1, + 'SR': 0, + } + + self.semester_range = { + 'min': 1, + 'max': 6, + } + + + def get_ira_value(self, disciplines: list[Discipline]) -> float: + """ + Obter o valor do IRA a partir de um conjunto de menções. + :param disciplines: A lista de disciplinas que um aluno pegou. + + :returns: Um float com o valor calculado do IRA. + """ + + numerator: int = 0 + denominator: int = 0 + + # para o cálculo do IRA, o maior valor possível para semestre é 6, mesmo + # que o estudante esteja num semestre maior que esse + MAX_SEMESTER_NUMBER: int = self.semester_range['max'] + + for discipline in disciplines: + + ## validação da entrada + try: + if discipline['number_of_credits'] <= 0: + raise ValueError("O número de créditos da disciplina é menor ou igual a 0.") + + discipline['semester'] = min(discipline['semester'], MAX_SEMESTER_NUMBER) + + if not (self.semester_range['min'] <= discipline['semester'] <= self.semester_range['max']): + raise ValueError( + f"O semestre está fora do intervalo delimitado entre {self.semester_range['min']} e {self.semester_range['max']}." + ) + + if discipline['grade'].upper() not in self.grade_map.keys(): + raise ValueError(f"A menção {discipline['grade']} não existe.") + + except TypeError: + raise TypeError("O tipo de dado passado como parâmetro está incorreto.") + + + ## cálculo do IRA + numerator += self.grade_map[discipline['grade'].upper()] * \ + discipline['number_of_credits'] * \ + discipline['semester'] + + denominator += discipline['number_of_credits'] * discipline['semester'] + + return float(numerator / denominator) + + + diff --git a/api/utils/tests/test_ira_calculator.py b/api/utils/tests/test_ira_calculator.py new file mode 100644 index 0000000..2f90cde --- /dev/null +++ b/api/utils/tests/test_ira_calculator.py @@ -0,0 +1,123 @@ +from django.test import TestCase +from utils.ira_calculator import IraCalculator, Discipline + + +class IraTestCase(TestCase): + def setUp(self): + self.ira_calc = IraCalculator() + + def test_one_discipline_with_MM(self): + args: list[Discipline] = [ + { + 'grade': 'MM', + 'semester': 1, + 'number_of_credits': 2, + } + ] + + self.assertEqual(self.ira_calc.get_ira_value(args), 3) + + def test_discipline_with_left_out_of_bounds_semester_value(self): + args: list[Discipline] = [ + { + 'grade': 'MM', + 'semester': 0, + 'number_of_credits': 2, + }, + ] + + self.assertRaises(ValueError, self.ira_calc.get_ira_value, args) + + def test_discipline_with_incorrect_semester_type(self): + args: list[Discipline] = [ + { + 'grade': 'MM', + 'semester': '0', + 'number_of_credits': 2, + }, + ] + + self.assertRaises(TypeError, self.ira_calc.get_ira_value, args) + + + def test_inexistent_grade(self): + args: list[Discipline] = [ + { + 'grade': 'NE', + 'semester': 3, + 'number_of_credits': 2, + }, + ] + self.assertRaises(ValueError, self.ira_calc.get_ira_value, args) + + def test_multiple_disciplines_during_first_semester(self): + args: list[Discipline] = [ + { + 'grade': 'MM', + 'semester': 1, + 'number_of_credits': 4 + }, + { + 'grade': 'MS', + 'semester': 1, + 'number_of_credits': 6 + }, + { + 'grade': 'SS', + 'semester': 1, + 'number_of_credits': 6 + }, + { + 'grade': 'SS', + 'semester': 1, + 'number_of_credits': 4 + }, + { + 'grade': 'MS', + 'semester': 1, + 'number_of_credits': 4 + }, + ] + + self.assertEqual(self.ira_calc.get_ira_value(args), 4.25) + + def test_negative_number_of_credits(self): + args: list[Discipline] = [ + { + 'grade': 'MM', + 'semester': 3, + 'number_of_credits': -1, + }, + ] + self.assertRaises(ValueError, self.ira_calc.get_ira_value, args) + + def test_null_number_of_credits(self): + args: list[Discipline] = [ + { + 'grade': 'MM', + 'semester': 3, + 'number_of_credits': -1, + }, + ] + self.assertRaises(ValueError, self.ira_calc.get_ira_value, args) + + def test_none_number_of_credits(self): + args: list[Discipline] = [ + { + 'grade': 'MM', + 'semester': 2, + 'number_of_credits': None, + }, + ] + self.assertRaises(TypeError, self.ira_calc.get_ira_value, args) + + def tests_discipline_with_lowercase_grade_value(self): + args: list[Discipline] = [ + { + 'grade': 'mm', + 'semester': 1, + 'number_of_credits': 2, + } + ] + + self.assertEqual(self.ira_calc.get_ira_value(args), 3)