Skip to content

Commit

Permalink
feat: adds ira calculator with unit tests (#229)
Browse files Browse the repository at this point in the history
* feat: adds ira calculator with unit tests

* refac: removes magic numbers

* refac: adds typing to discipline dict

* refac: adds messages to captured exceptions

* fix: changes math for ira calculation

* fix: fixes math again

* feat: adds testing for some disciplines during first semester

* feat: adds more input validation

* refac: adds comments that explain the calculation

* refac: changes variable names to english and return value

* refac: corrects error handling
  • Loading branch information
lucasqueiroz23 authored Aug 22, 2024
1 parent b3e8235 commit 8442bd6
Show file tree
Hide file tree
Showing 2 changed files with 216 additions and 0 deletions.
93 changes: 93 additions & 0 deletions api/utils/ira_calculator.py
Original file line number Diff line number Diff line change
@@ -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)



123 changes: 123 additions & 0 deletions api/utils/tests/test_ira_calculator.py
Original file line number Diff line number Diff line change
@@ -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)

0 comments on commit 8442bd6

Please sign in to comment.