-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcalculations.js
More file actions
157 lines (138 loc) · 5.54 KB
/
Copy pathcalculations.js
File metadata and controls
157 lines (138 loc) · 5.54 KB
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
// ══════════════════════════════════════════════════
// CALCULATIONS
// ══════════════════════════════════════════════════
function pct(scored, max) {
if (!max) return 0;
return Math.round((scored / max) * 100 * 10) / 10;
}
function grade(p) {
if (p >= 90) return 'O';
if (p >= 80) return 'A+';
if (p >= 70) return 'A';
if (p >= 60) return 'B+';
if (p >= 50) return 'B';
if (p >= 40) return 'C';
return 'F';
}
function pctClass(p) {
if (p >= 70) return 'pct-high';
if (p >= 50) return 'pct-mid';
return 'pct-low';
}
// Formats a number as a fixed two-decimal float, e.g. 85 -> "85.00"
function fmt2(n) {
return (+n || 0).toFixed(2);
}
// Normalizes subj.internal into an array of components ({name, obtained}).
// Supports:
// - the new format: { min, max, components:[{name,obtained},...] }
// - the legacy array form: [{name,min,max,obtained}, ...]
// - the legacy single-object form: {min,max,obtained}
function internalComponents(subj) {
const i = subj?.internal;
if (i && Array.isArray(i.components)) return i.components;
if (Array.isArray(i)) return i;
if (i && ((+i.max) || (+i.obtained) || (+i.min))) return [{ name: 'Internal', obtained: i.obtained }];
return [];
}
// Final IA (internal assessment) min/max for a subject.
// New format stores min/max directly on subj.internal; legacy data derives
// them by summing each component's own min/max.
function internalMinMax(subj) {
const i = subj?.internal;
if (i && !Array.isArray(i) && (i.min !== undefined || i.max !== undefined)) {
return { min: +i.min || 0, max: +i.max || 0 };
}
const comps = Array.isArray(i) ? i : [];
return {
min: comps.reduce((s, c) => s + (+c.min || 0), 0),
max: comps.reduce((s, c) => s + (+c.max || 0), 0),
};
}
// Totals for one subject (internal + external)
function calcSubjectTotal(subj) {
const comps = internalComponents(subj);
const intObtained = comps.reduce((s, c) => s + (+c.obtained || 0), 0);
const { min: intMin, max: intMax } = internalMinMax(subj);
const e = subj.external || { min: 0, max: 0, obtained: 0 };
const extObtained = +e.obtained || 0;
const extMax = +e.max || 0;
return {
scored: intObtained + extObtained,
max: intMax + extMax,
intObtained, intMax, extObtained, extMax,
intMin, extMin: +e.min || 0,
};
}
// Whether a subject clears both internal & external minimums
function subjectPass(subj) {
const t = calcSubjectTotal(subj);
const intOk = t.intMax ? t.intObtained >= t.intMin : true;
const extOk = t.extMax ? t.extObtained >= t.extMin : true;
return intOk && extOk;
}
function stageSubjects(stageId) {
return state.subjects.filter(s => s.stageId === stageId);
}
function termSubjects(stageId, termId) {
return state.subjects.filter(s => s.stageId === stageId && s.termId === termId);
}
// Average %, excluding audit subjects (audit = pass/fail only, not counted in %)
function stageAvg(subjects) {
const graded = subjects.filter(s => s.subjectType !== 'audit');
if (!graded.length) return 0;
let ts = 0, tm = 0;
graded.forEach(s => { const t = calcSubjectTotal(s); ts += t.scored; tm += t.max; });
return tm ? pct(ts, tm) : 0;
}
function termAvg(stageId, termId) {
return stageAvg(termSubjects(stageId, termId));
}
// ══════════════════════════════════════════════════
// TOTALS & GPA/CGPA
// ══════════════════════════════════════════════════
// Marks total (scored/max), excluding audit subjects (pass/fail only, not
// counted toward marks totals or GPA).
function subjectsTotal(subjects) {
const graded = subjects.filter(s => s.subjectType !== 'audit');
let scored = 0, max = 0;
graded.forEach(s => { const t = calcSubjectTotal(s); scored += t.scored; max += t.max; });
return { scored, max, count: graded.length };
}
// Grade point on a 10-point scale, mirroring the same percentage bands used
// by grade() (O=10, A+=9, A=8, B+=7, B=6, C=5, F=0).
function gradePoint(p) {
if (p >= 90) return 10;
if (p >= 80) return 9;
if (p >= 70) return 8;
if (p >= 60) return 7;
if (p >= 50) return 6;
if (p >= 40) return 5;
return 0;
}
// GPA for a set of subjects: the average grade point across graded subjects
// (audit subjects excluded). Returned to 2 decimal places.
function subjectsGPA(subjects) {
const graded = subjects.filter(s => s.subjectType !== 'audit');
if (!graded.length) return 0;
const sum = graded.reduce((acc, s) => {
const t = calcSubjectTotal(s);
return acc + gradePoint(pct(t.scored, t.max));
}, 0);
return Math.round((sum / graded.length) * 100) / 100;
}
function stageTotal(stageId) { return subjectsTotal(stageSubjects(stageId)); }
function termTotal(stageId, termId) { return subjectsTotal(termSubjects(stageId, termId)); }
function stageGPA(stageId) { return subjectsGPA(stageSubjects(stageId)); }
function termGPA(stageId, termId) { return subjectsGPA(termSubjects(stageId, termId)); }
// Overall CGPA across every stage (average grade point over all graded subjects)
function overallCGPA() {
return subjectsGPA(state.subjects);
}
function getStage(stageId) {
return state.stages.find(s => s.id === stageId);
}
function stageLabel(stage) {
if (!stage) return '';
return stage.label || (STAGE_TYPES[stage.type] || STAGE_TYPES.custom).label;
}