Skip to content

Commit 8ee95c1

Browse files
GostmeaperTenType
andauthored
feat: AP credit calculator command (#62)
* feat: jsons and command file * feat: base UX * feat: consolidate jsons and correct ap-sheet to reflect accurately * feat: form class for credit calculator * feat: implement credit calculator form class to have dropdowns * fix: calculate credits (incomplete) * fix: string select menus both can be entered * fix: get course name correctly * fix: use right json.... * fix: everything * fix: remove console logs * feat: add gened jsons * feat: add ap courses * feat: functionality complete * fix: stuff ai didn't like * fix: Chemistry entry * fix: LOL i forgor tepper * fix: Tentype bot comments * fix: json school data * fix: school logic * fix: CI Bug * fix: let the war be over * fix: not ephemeral * fix: note ambiguity --------- Co-authored-by: Max Wen <55125103+TenType@users.noreply.github.com>
1 parent ebd4643 commit 8ee95c1

File tree

11 files changed

+11573
-19
lines changed

11 files changed

+11573
-19
lines changed
Lines changed: 353 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,353 @@
1+
import {
2+
ContainerBuilder,
3+
MessageFlags,
4+
SeparatorBuilder,
5+
SlashCommandBuilder,
6+
} from "discord.js";
7+
import { DEFAULT_EMBED_COLOR, SCHOOLS } from "../constants.js";
8+
import apCoursesData from "../data/ap-courses.json" with { type: "json" };
9+
import apCreditData from "../data/ap-credit.json" with { type: "json" };
10+
import CoursesData from "../data/finalCourseJSON.json" with { type: "json" };
11+
import CITGenedData from "../data/geneds/CITgeneds.json" with { type: "json" };
12+
import DCGenedData from "../data/geneds/DCgeneds.json" with { type: "json" };
13+
import MCSGenedData from "../data/geneds/MCSgeneds.json" with { type: "json" };
14+
import SCSGenedData from "../data/geneds/SCSgeneds.json" with { type: "json" };
15+
import type { SlashCommand } from "../types.d.ts";
16+
import {
17+
type SetupField,
18+
SetupForm,
19+
type SetupSchema,
20+
} from "../utils/creditCalculatorForm.ts";
21+
import { Course, GenEd } from "../utils/index.ts";
22+
23+
type School = "DC" | "CIT" | "SCS" | "TEP" | "MCS" | "CFA";
24+
25+
type Exam = {
26+
name: string;
27+
subject: "STEM" | "Arts" | "Humanities" | "N/A";
28+
school: School[];
29+
info: string;
30+
scores: {
31+
score: number;
32+
courses: Course[];
33+
}[];
34+
};
35+
36+
type ApCourse = {
37+
id: string;
38+
name: string;
39+
units: string;
40+
};
41+
42+
async function loadApCreditData(): Promise<Exam[]> {
43+
const exams: Exam[] = [];
44+
45+
const courses = CoursesData as Record<string, Course>;
46+
const apCoursesIndex: Record<string, Course> = Object.fromEntries(
47+
(apCoursesData as ApCourse[]).map((course) => [
48+
course.id,
49+
{
50+
id: course.id,
51+
name: course.name,
52+
units: course.units,
53+
syllabi: [],
54+
desc: "",
55+
prereqs: [],
56+
prereqString: "",
57+
coreqs: [],
58+
crosslisted: [],
59+
department: "",
60+
},
61+
]),
62+
);
63+
64+
for (const entry of apCreditData) {
65+
for (const exam of entry.exams) {
66+
const scoreCourses: Course[] = entry.courses.map((id) => {
67+
const course = courses[id] ?? apCoursesIndex[id];
68+
69+
if (!course) {
70+
return {
71+
id,
72+
name: exam.name,
73+
syllabi: [],
74+
desc: "",
75+
prereqs: [],
76+
prereqString: "",
77+
coreqs: [],
78+
crosslisted: [],
79+
units: "",
80+
department: "",
81+
};
82+
}
83+
84+
return course;
85+
});
86+
87+
exams.push({
88+
name: exam.name,
89+
subject: entry.subject as
90+
| "STEM"
91+
| "Arts"
92+
| "Humanities"
93+
| "N/A",
94+
school: entry.school as School[],
95+
info: entry.info?.trim() || "",
96+
scores: [
97+
{
98+
score: exam.score,
99+
courses: scoreCourses,
100+
},
101+
],
102+
});
103+
}
104+
}
105+
return exams;
106+
}
107+
108+
function getGenedsForCourse(courseId: string, geneds: GenEd[]): string[] {
109+
return geneds.filter((g) => g.courseID === courseId).flatMap((g) => g.tags);
110+
}
111+
112+
const command: SlashCommand = {
113+
data: new SlashCommandBuilder()
114+
.setName("credit-calculator")
115+
.setDescription("Credit calculator for CMU courses")
116+
.addSubcommand((subcommand) =>
117+
subcommand
118+
.setName("ap")
119+
.setDescription(
120+
"Calculate units and courses waived through your APs",
121+
)
122+
.addStringOption((option) =>
123+
option
124+
.setName("school")
125+
.setDescription(
126+
"Enter College (DC, CIT, SCS, TEP, MCS, CFA)",
127+
)
128+
.setRequired(true),
129+
),
130+
),
131+
132+
async execute(interaction) {
133+
if (interaction.options.getSubcommand() === "ap") {
134+
const userSchool = interaction.options.getString("school");
135+
136+
if (!userSchool || !SCHOOLS.includes(userSchool)) {
137+
console.log(userSchool, SCHOOLS);
138+
return interaction.reply({
139+
content: "Acceptable Colleges DC, CIT, SCS, TEP, MCS, CFA",
140+
flags: MessageFlags.Ephemeral,
141+
});
142+
}
143+
const exams = await loadApCreditData();
144+
145+
const stemExams = exams.filter((e) => e.subject === "STEM");
146+
const artsExams = exams.filter((e) => e.subject === "Arts");
147+
const humanitiesExams = exams.filter(
148+
(e) => e.subject === "Humanities",
149+
);
150+
151+
const stemExamsUnique = Array.from(
152+
new Map(stemExams.map((e) => [e.name, e])).values(),
153+
);
154+
155+
const artsExamsUnique = Array.from(
156+
new Map(artsExams.map((e) => [e.name, e])).values(),
157+
);
158+
159+
const humanitiesExamsUnique = Array.from(
160+
new Map(humanitiesExams.map((e) => [e.name, e])).values(),
161+
);
162+
163+
const artsSciencesUnique = Array.from(
164+
new Map(
165+
[...artsExamsUnique, ...stemExamsUnique].map((e) => [
166+
e.name,
167+
e,
168+
]),
169+
).values(),
170+
);
171+
172+
const fields: SetupField[] = [
173+
{
174+
key: "stem-arts",
175+
label: "Arts and Sciences AP Exams",
176+
required: false,
177+
multiple: false,
178+
type: "string",
179+
options: artsSciencesUnique.map((e) => ({
180+
label: e.name,
181+
value: e.name,
182+
})),
183+
modal: {
184+
title: "Enter AP Score",
185+
input: {
186+
key: "score",
187+
label: "Score (1-5)",
188+
min: 1,
189+
max: 5,
190+
},
191+
},
192+
},
193+
{
194+
key: "humanities",
195+
label: "Humanities AP Exams",
196+
required: false,
197+
multiple: false,
198+
type: "string",
199+
options: humanitiesExamsUnique.map((e) => ({
200+
label: e.name,
201+
value: e.name,
202+
})),
203+
modal: {
204+
title: "Enter AP Score",
205+
input: {
206+
key: "score",
207+
label: "Score (1-5)",
208+
min: 1,
209+
max: 5,
210+
},
211+
},
212+
},
213+
];
214+
215+
const apExamSetup: SetupSchema = {
216+
name: "AP Credit Calculator",
217+
fields,
218+
onComplete: async (data) => {
219+
const courses = CoursesData as Record<string, Course>;
220+
const awarded: { exam: Exam; courses: Course[] }[] = [];
221+
222+
const processCategory = (
223+
entries: { examName: string; score: number }[],
224+
) => {
225+
entries.forEach(({ examName, score }) => {
226+
const sameName = exams.filter(
227+
(e) => e.name === examName,
228+
);
229+
230+
const chosenExams = (() => {
231+
const specific: typeof sameName = [];
232+
const general: typeof sameName = [];
233+
234+
for (const e of sameName) {
235+
if (
236+
e.school?.includes(userSchool as School)
237+
)
238+
specific.push(e);
239+
else if (!e.school || e.school.length === 0)
240+
general.push(e);
241+
}
242+
243+
return specific.length > 0 ? specific : general;
244+
})();
245+
246+
const results = chosenExams.flatMap((exam) => {
247+
const courses = exam.scores
248+
.filter((s) => s.score === score)
249+
.flatMap((s) => s.courses);
250+
251+
return courses.length
252+
? [{ exam, courses }]
253+
: [];
254+
});
255+
256+
awarded.push(...results);
257+
});
258+
};
259+
260+
processCategory(data["stem-arts"] ?? []);
261+
processCategory(data["humanities"] ?? []);
262+
263+
const container = new ContainerBuilder()
264+
.setAccentColor(DEFAULT_EMBED_COLOR)
265+
.addTextDisplayComponents((t) =>
266+
t.setContent("Awarded CMU Credit"),
267+
);
268+
269+
if (awarded.length === 0) {
270+
container.addTextDisplayComponents((t) =>
271+
t.setContent(
272+
"No credit awarded based on the selected exams.",
273+
),
274+
);
275+
return container;
276+
}
277+
278+
let genedCreditTotal = 0;
279+
280+
for (const { exam, courses: awardedCourses } of awarded) {
281+
container.addSeparatorComponents(
282+
new SeparatorBuilder(),
283+
);
284+
285+
container.addTextDisplayComponents((t) =>
286+
t.setContent(`### ${exam.name}`),
287+
);
288+
289+
let geneds: GenEd[] = [];
290+
291+
if (userSchool == "DC") {
292+
geneds = DCGenedData as GenEd[];
293+
} else if (userSchool == "CIT") {
294+
geneds = CITGenedData as GenEd[];
295+
} else if (userSchool == "MCS") {
296+
geneds = MCSGenedData as GenEd[];
297+
} else if (userSchool == "SCS") {
298+
geneds = SCSGenedData as GenEd[];
299+
} else if (userSchool == "CFA" || userSchool == "TEP") {
300+
container.addTextDisplayComponents((t) =>
301+
t.setContent(
302+
`Gened data not available for ${userSchool}`,
303+
),
304+
);
305+
}
306+
307+
for (const course of awardedCourses) {
308+
const units = Number(course.units) || 0;
309+
genedCreditTotal += units;
310+
311+
const courseName =
312+
courses[course.id]?.name ?? course.name;
313+
314+
const genedList =
315+
geneds && course.id
316+
? getGenedsForCourse(course.id, geneds)
317+
: [];
318+
319+
const genedTags = genedList.length
320+
? genedList.map((g) => `[${g}]`).join(" ")
321+
: "_None_";
322+
323+
container.addTextDisplayComponents((t) =>
324+
t.setContent(
325+
[
326+
`> **${course.id}** — ${courseName}`,
327+
`> **${units} units** · GenEds: ${genedTags}`,
328+
].join("\n"),
329+
),
330+
);
331+
}
332+
if (exam.info !== "") {
333+
container.addTextDisplayComponents((t) =>
334+
t.setContent(`${exam.info}`),
335+
);
336+
}
337+
}
338+
339+
container.addTextDisplayComponents((t) =>
340+
t.setContent(
341+
`**GenEd Unit Total:** ${genedCreditTotal}`,
342+
),
343+
);
344+
return container;
345+
},
346+
};
347+
348+
await new SetupForm(apExamSetup, interaction).start();
349+
}
350+
},
351+
};
352+
353+
export default command;

src/commands/courses.ts

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,27 +18,9 @@ import CoursesData from "../data/finalCourseJSON.json" with { type: "json" };
1818
import { parseAndEvaluate } from "../modules/operator-parser.ts";
1919
import type { SlashCommand } from "../types.d.ts";
2020
import { EmbedPaginator } from "../utils/EmbedPaginator.ts";
21-
22-
type Session = {
23-
term: string;
24-
section: string;
25-
instructors: string[];
26-
url: string;
27-
};
21+
import { Course } from "../utils/index.ts";
2822

2923
//TODO: many of these fields could be made into CourseCodes
30-
type Course = {
31-
id: string;
32-
name: string;
33-
syllabi: Session[];
34-
desc: string;
35-
prereqs: string[];
36-
prereqString: string;
37-
coreqs: string[];
38-
crosslisted: string[];
39-
units: string;
40-
department: string;
41-
};
4224

4325
type FCEData = {
4426
courseNum: string;

src/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export const SCOTTYLABS_URL = "https://www.courses.scottylabs.org";
22
export const FYW_MINIS = ["76-106", "76-107", "76-108"];
3+
export const SCHOOLS = ["DC", "CIT", "SCS", "TEP", "MCS", "CFA"];
34
export const DEFAULT_EMBED_COLOR = 0x5865f2;

0 commit comments

Comments
 (0)