-
Notifications
You must be signed in to change notification settings - Fork 0
/
unigrade
executable file
·210 lines (176 loc) · 7.22 KB
/
unigrade
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
#!/usr/bin/env python3
import click
import pandas as pd
from pathlib import Path
from tabulate import tabulate
from dataclasses import dataclass
from csv import writer
FILEPATH = Path(__file__).parent / "grades.csv"
accepted_grades = ['A', 'B', 'C', 'D', 'E', 'Pass']
format_of_CSV = """The file needs to have the following format:
Subject,Grade,Credits
ExampleSubjectGradePass,Pass,5
ExampleSubjectGradeA,A,10
In which the grades have to be element of the subset {A,B,C,D,E,Pass}
and the credits have to be a number of type integer"""
@dataclass
class GradeInformation:
grade: str
credits: float
@dataclass
class Subject:
name: str
info: GradeInformation
def __init__(self, name: str, grade: str, credits: float):
try:
assert any(grade.lower() == accepted_grade.lower() for accepted_grade in accepted_grades), "The grade is not accepted"
credits = float(credits)
except AssertionError as grade_error:
print(grade_error)
exit(1)
except:
exit(1)
self.name = name
self.info = GradeInformation(grade, credits)
def write_to_subject_csv(subject: Subject):
if subject.info.credits % 1 == 0:
subject.info.credits = int(subject.info.credits)
with open(FILEPATH, 'a+') as file:
csv_writer = writer(file)
csv_writer.writerow([subject.name, subject.info.grade.capitalize(), subject.info.credits])
def read_subject_dict(path):
try:
subject_dict = dict()
with open(path) as file:
list = file.readlines()[1:]
for line in list:
if line.strip(): # If line is not empty
subject, grade, credits = line.strip().split(',')
subject_dict[subject] = GradeInformation(grade, credits)
return subject_dict
except ValueError:
print("The file containing grades is empty")
def calulate_grade(predict=None):
grade_dict = read_subject_dict(FILEPATH)
if predict is not None:
prediction_dict = read_subject_dict(predict)
grade_dict = {**grade_dict, **prediction_dict}
try:
letter_to_number_grade = {
'A': 5,
'B': 4,
'C': 3,
'D': 2,
'E': 1
}
sum_of_credits = 0
sum_of_credits_x_grade = 0
sum_of_credits_from_passed_subjects = 0
for key in grade_dict:
grade = grade_dict.get(key).grade
if grade.capitalize() == 'Pass':
sum_of_credits_from_passed_subjects += float(grade_dict.get(key).credits)
continue
grade = letter_to_number_grade[grade]
credits = float(grade_dict.get(key).credits)
sum_of_credits += credits
sum_of_credits_x_grade += (credits * grade)
return round(sum_of_credits_x_grade / sum_of_credits, 2), sum_of_credits + sum_of_credits_from_passed_subjects
except ZeroDivisionError:
print("You have to insert a grade!")
exit(1)
def do_append():
"""Appends a new subject to the list of grades"""
try:
write_to_subject_csv(Subject(name=input("Subject: "), grade=input(
"Grade: "), credits=input("Credits: ")))
except AssertionError as format_error:
print(format_error)
def do_show(dataframe=pd.read_csv(FILEPATH), table=True, predict_path=None):
"""Shows a table of your grades, and your weighted average."""
if table:
print(tabulate(dataframe.to_numpy(), headers=["Subject", "Grade", "Credits"]))
grade, credits = calulate_grade(predict=predict_path)
grade = int(grade) if grade % 1 == 0 else grade # Convert to int if no decimals
credits = int(credits) if credits % 1 == 0 else credits # Convert int float if no decimals
print("\n" + f"Your weighted average grade is {grade}, with a total of {credits} credits.")
def do_predict(predict_path: str):
try:
assert predict_path.split(".")[-1].lower() == "csv", "The file needs to be of format csv"
with open(predict_path) as file:
header = file.readline()
assert header.strip() == 'Subject,Grade,Credits', format_of_CSV
li_of_df = []
li_of_df.append(pd.read_csv(FILEPATH, index_col=None))
li_of_df.append(pd.read_csv(predict_path, header=0, index_col=None))
dataframe = pd.concat(li_of_df, axis=0, ignore_index=True).drop_duplicates(subset='Subject', keep="last")
do_show(dataframe=dataframe, table=True, predict_path=predict_path)
except AssertionError as format_error:
print(format_error)
exit(1)
except FileNotFoundError:
print("No such file or directory")
exit(1)
# Function to translate numerical grade to letter grade
def numerical_to_ects_grade(numerical_grade):
# Dictionary mapping ECTS grades to their numerical ranges
grade_definitions = {
'A': (5, 5),
'A-': (4.5, 4.9999999999),
'B+': (4, 4.4999999999),
'B': (4, 4),
'B-': (3.5, 3.9999999999),
'C+': (3, 3.4999999999),
'C': (3, 3),
'C-': (2.5, 2.9999999999),
'D+': (2, 2.4999999999),
'D': (2, 2),
'D-': (1.5, 1.9999999999),
'E+': (1, 1.4999999999),
'E': (1, 1),
# Lower than E is fail in ECTS
}
# Iterate through the grade definitions to find where the value falls
for grade, (lower_bound, upper_bound) in grade_definitions.items():
if lower_bound <= numerical_grade <= upper_bound:
return grade
return "Incorrect grade" if numerical_grade > 5 or numerical_grade < 0 else "Fail"
def do_info():
""" Show information."""
print("""ECTS grades are in the range from 1-5, E to A, where A is 5.00, E is 1.00 and F is below 1.""")
def do_stats(dataframe=pd.read_csv(FILEPATH), predict_path=None):
"""Show statistics."""
# Get stats
grade, credits = calulate_grade(predict=predict_path)
grade = int(grade) if grade % 1 == 0 else grade # Convert to int if no decimals
credits = int(credits) if credits % 1 == 0 else credits # Convert int float if no decimals
grouped_grades = "\n - ".join(f"{grade}: {count}" for grade, count in dataframe["Grade"].value_counts().sort_index().items())
percentage = grade / 0.05
# Print
print("Stats")
print("-"*20)
print("Average numerical grade (ECTS): ", grade)
print("Equivalent letter grade (ECTS): ", numerical_to_ects_grade(grade))
print("Overall percentage:", str(percentage) + "%")
print("Total credits: ", credits)
print("Grouped grades: ", "\n -", grouped_grades)
print("-"*20)
@click.command()
@click.option('-a', '--append', is_flag=True, help='Append a new grade.')
@click.option('-p', '--predict', help='Predict grade with path to predict.csv.')
@click.option('-s', '--stats', is_flag=True, help='Show stats.')
@click.option('-i', '--info', is_flag=True, help='Show information.')
def main(append: bool, predict: str, stats: bool, info: bool):
"""A command line interface for displaying and keeping track of your grades and weighted average."""
if append:
do_append()
elif stats:
do_stats()
elif info:
do_info()
elif predict is not None:
do_predict(predict)
else:
do_show()
if __name__ == "__main__":
main()