diff --git a/__tests__/utils/channels.tests.ts b/__tests__/utils/channels.tests.ts index 5cf18d1..2e0f43c 100644 --- a/__tests__/utils/channels.tests.ts +++ b/__tests__/utils/channels.tests.ts @@ -2,12 +2,12 @@ import { cleanChannelString, concatCategoryName, moveChannel, - getCourseName, getTopic, } from "../../src/utils/channels"; import { expect, describe, it, beforeEach } from "@jest/globals"; import { Guild, GuildChannel } from "discord.js"; import { IClass } from "../../src/models/classModel"; +import { getCourseName } from "../../src/utils/course_cleaning"; //cleanChannelString describe("cleanChannelString", () => { diff --git a/package-lock.json b/package-lock.json index ec836e0..aef15eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,8 @@ "devDependencies": { "@jest/globals": "^29.3.1", "jest": "^29.3.1", - "ts-jest": "^29.0.3" + "ts-jest": "^29.0.3", + "typedoc": "^0.23.24" }, "engines": { "node": "17.x" @@ -3016,6 +3017,12 @@ "node": ">=6" } }, + "node_modules/jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "dev": true + }, "node_modules/kareem": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.4.1.tgz", @@ -3077,6 +3084,12 @@ "yallist": "^3.0.2" } }, + "node_modules/lunr": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", + "dev": true + }, "node_modules/make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -3106,6 +3119,18 @@ "tmpl": "1.0.5" } }, + "node_modules/marked": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.2.5.tgz", + "integrity": "sha512-jPueVhumq7idETHkb203WDD4fMA3yV9emQ5vLwop58lu8bTclMghBWcYAavlDqIEMaisADinV1TooIFCfqOsYQ==", + "dev": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, "node_modules/memory-pager": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", @@ -3666,6 +3691,17 @@ "node": ">=8" } }, + "node_modules/shiki": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.12.1.tgz", + "integrity": "sha512-aieaV1m349rZINEBkjxh2QbBvFFQOlgqYTNtCal82hHj4dDZ76oMlQIX+C7ryerBTDiga3e5NfH6smjdJ02BbQ==", + "dev": true, + "dependencies": { + "jsonc-parser": "^3.2.0", + "vscode-oniguruma": "^1.7.0", + "vscode-textmate": "^8.0.0" + } + }, "node_modules/sift": { "version": "16.0.0", "resolved": "https://registry.npmjs.org/sift/-/sift-16.0.0.tgz", @@ -4053,6 +4089,48 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/typedoc": { + "version": "0.23.24", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.23.24.tgz", + "integrity": "sha512-bfmy8lNQh+WrPYcJbtjQ6JEEsVl/ce1ZIXyXhyW+a1vFrjO39t6J8sL/d6FfAGrJTc7McCXgk9AanYBSNvLdIA==", + "dev": true, + "dependencies": { + "lunr": "^2.3.9", + "marked": "^4.2.5", + "minimatch": "^5.1.2", + "shiki": "^0.12.1" + }, + "bin": { + "typedoc": "bin/typedoc" + }, + "engines": { + "node": ">= 14.14" + }, + "peerDependencies": { + "typescript": "4.6.x || 4.7.x || 4.8.x || 4.9.x" + } + }, + "node_modules/typedoc/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/typedoc/node_modules/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-bNH9mmM9qsJ2X4r2Nat1B//1dJVcn3+iBLa3IgqJ7EbGaDNepL9QSHOxN4ng33s52VMMhhIfgCYDk3C4ZmlDAg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/typescript": { "version": "4.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.3.tgz", @@ -4134,6 +4212,18 @@ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", "dev": true }, + "node_modules/vscode-oniguruma": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz", + "integrity": "sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==", + "dev": true + }, + "node_modules/vscode-textmate": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-8.0.0.tgz", + "integrity": "sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==", + "dev": true + }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -6582,6 +6672,12 @@ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true }, + "jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "dev": true + }, "kareem": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.4.1.tgz", @@ -6634,6 +6730,12 @@ "yallist": "^3.0.2" } }, + "lunr": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", + "dev": true + }, "make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -6657,6 +6759,12 @@ "tmpl": "1.0.5" } }, + "marked": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.2.5.tgz", + "integrity": "sha512-jPueVhumq7idETHkb203WDD4fMA3yV9emQ5vLwop58lu8bTclMghBWcYAavlDqIEMaisADinV1TooIFCfqOsYQ==", + "dev": true + }, "memory-pager": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", @@ -7065,6 +7173,17 @@ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true }, + "shiki": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.12.1.tgz", + "integrity": "sha512-aieaV1m349rZINEBkjxh2QbBvFFQOlgqYTNtCal82hHj4dDZ76oMlQIX+C7ryerBTDiga3e5NfH6smjdJ02BbQ==", + "dev": true, + "requires": { + "jsonc-parser": "^3.2.0", + "vscode-oniguruma": "^1.7.0", + "vscode-textmate": "^8.0.0" + } + }, "sift": { "version": "16.0.0", "resolved": "https://registry.npmjs.org/sift/-/sift-16.0.0.tgz", @@ -7328,6 +7447,38 @@ "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", "dev": true }, + "typedoc": { + "version": "0.23.24", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.23.24.tgz", + "integrity": "sha512-bfmy8lNQh+WrPYcJbtjQ6JEEsVl/ce1ZIXyXhyW+a1vFrjO39t6J8sL/d6FfAGrJTc7McCXgk9AanYBSNvLdIA==", + "dev": true, + "requires": { + "lunr": "^2.3.9", + "marked": "^4.2.5", + "minimatch": "^5.1.2", + "shiki": "^0.12.1" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-bNH9mmM9qsJ2X4r2Nat1B//1dJVcn3+iBLa3IgqJ7EbGaDNepL9QSHOxN4ng33s52VMMhhIfgCYDk3C4ZmlDAg==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, "typescript": { "version": "4.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.3.tgz", @@ -7385,6 +7536,18 @@ } } }, + "vscode-oniguruma": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz", + "integrity": "sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==", + "dev": true + }, + "vscode-textmate": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-8.0.0.tgz", + "integrity": "sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==", + "dev": true + }, "walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", diff --git a/package.json b/package.json index 5b2a7a6..3a3b539 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,7 @@ "main": "index.ts", "scripts": { "run": "ts-node src/index.ts", - "format": "prettier --write .", - "test": "jest" + "format": "prettier --write ." }, "repository": { "type": "git", @@ -35,7 +34,6 @@ }, "devDependencies": { "@jest/globals": "^29.3.1", - "jest": "^29.3.1", - "ts-jest": "^29.0.3" + "jest": "^29.3.1" } -} \ No newline at end of file +} diff --git a/src/commands/owner/csClassPoll.ts b/src/commands/owner/csClassPoll.ts index d534358..f63f719 100644 --- a/src/commands/owner/csClassPoll.ts +++ b/src/commands/owner/csClassPoll.ts @@ -9,7 +9,7 @@ import { ICommand } from "wokcommands"; import { classModel, IClass } from "../../models/classModel"; import { checkForRoles, cleanRoleString } from "../../utils/roles"; import { sleep } from "../../utils/sleep"; -import { getCourseName } from "../../utils/channels"; +import { getCourseName } from "../../utils/course_cleaning"; // Splits any size list into lists of at most `max_list_len` /** diff --git a/src/commands/owner/csCreateChannels.ts b/src/commands/owner/csCreateChannels.ts index f0397a9..0684bd9 100644 --- a/src/commands/owner/csCreateChannels.ts +++ b/src/commands/owner/csCreateChannels.ts @@ -8,10 +8,11 @@ import { getCategory, moveChannel, concatCategoryName, + getTopic, } from "../../utils/channels"; import { create_default_embed } from "../../utils/embeds"; import { cleanRoleString } from "../../utils/roles"; -import { getCourseName, getTopic } from "../../utils/channels"; +import { getCourseName } from "../../utils/course_cleaning"; export default { name: "csCreateChannels", diff --git a/src/commands/owner/migrateDB.ts b/src/commands/owner/migrateDB.ts index d4a9884..68f6e2b 100644 --- a/src/commands/owner/migrateDB.ts +++ b/src/commands/owner/migrateDB.ts @@ -3,20 +3,15 @@ import { ICommand } from "wokcommands"; import { classModel, IClass } from "../../models/classModel"; import { create_default_embed } from "../../utils/embeds"; import { Schema, Types, Document } from "mongoose"; +import { cleanChannelString, getTopic } from "../../utils/channels"; +import { cleanRoleString } from "../../utils/roles"; import { getCourseName, - cleanChannelString, - getTopic, -} from "../../utils/channels"; -import { cleanRoleString } from "../../utils/roles"; + cleanCompSciTitle, + cleanCourseCode, +} from "../../utils/course_cleaning"; import Bottleneck from "bottleneck"; -function cleanCompSciString(s: string): string { - return s.toLowerCase().replace("compsci ", "cs").trim(); -} -function cleanCompSciTitle(s: string): string { - return s.replace(/(?:advanced )?topics in computer science:/gim, "").trim(); -} function isDupe(name: string): number { return name.search(/\(\d\)/); } @@ -76,7 +71,7 @@ export default { // Remove the number new_name = new_name.slice(0, is_dupe); } - new_name = cleanCompSciString(new_name); + new_name = cleanCourseCode(new_name); course.set("NAME", new_name); // Remove UUID console.log(chalk.yellow(`${code}\t${new_name}`)); diff --git a/src/commands/owner/updateDB.ts b/src/commands/owner/updateDB.ts new file mode 100644 index 0000000..214e260 --- /dev/null +++ b/src/commands/owner/updateDB.ts @@ -0,0 +1,194 @@ +import { ICommand } from "wokcommands"; +import DiscordJS from "discord.js"; +import chalk from "chalk"; +import axios from "axios"; +import { classModel, IClass } from "../../models/classModel"; +import { Types, Document } from "mongoose"; +import { + cleanCourseCode, + getCourseName, + removeHTML, +} from "../../utils/course_cleaning"; +import { cleanRoleString } from "../../utils/roles"; + +/** + * @description + * @author Nathen Goldsborough + * @interface course + */ +interface course { + code: string; + title: string; + description: string; +} + +/** + * @description - Gets all courses from the course catalog api + * @author Nathen Goldsborough + * @param srcdb + * @return - the response data (results field) + */ +async function getCourses(srcdb: string) { + const body = encodeURIComponent( + `{"other":{"srcdb":"${srcdb}"},"criteria":[{"field":"subject","value":"COMPSCI"}]}` + ); + const config = { + method: "post", + url: `https://catalog.uwm.edu/course-search/api/?page=fose&route=search&subject=COMPSCI`, + headers: { + "Content-Type": "text/plain", + }, + data: body, + }; + const response = await axios(config); + return response.data.results; +} + +/** + * @description - Gets the course description from the course catalog api + * @author Nathen Goldsborough + * @param course - the course to get the description for + * @param srcdb - the semester id to get the description for + * @return - the course description returned from the api call + */ +async function getDescription(course: any, srcdb: string) { + // skipcq: JS-0323 + const code = course.code; + const crn = course.crn; + const body = encodeURIComponent( + `{"group":"code:${code}","key":"crn:${crn}","srcdb":"${srcdb}","matched":"crn:${crn}"}` + ); + const config = { + method: "get", + url: `https://catalog.uwm.edu/course-search/api/?page=fose&route=details`, + headers: { + "Content-Type": "text/plain", + }, + data: body, + }; + const result = await axios(config); + return result.data.description; +} + +/** + * @description - Gets all course data from the course catalog api + * @author Nathen Goldsborough + * @param srcdb - the semester id to get the courses for + * @return - Array of course objects + */ +async function getCoursesWithDescription(srcdb: string) { + const courses = await getCourses(srcdb); + const info: course[] = []; + for (const course of courses) { + const description = await getDescription(course, srcdb); + const entry: course = { + code: cleanCourseCode(course.code), + title: course.title, + description: removeHTML(description), + }; + info.push(entry); + } + return info; +} +export default { + name: "updatedb", + category: "owner", + description: "Update the database with new classes", + slash: true, + expectedArgs: "", + minArgs: 1, + permissions: ["MANAGE_GUILD", "MANAGE_CHANNELS", "MANAGE_ROLES"], + guildOnly: true, + ownerOnly: true, + options: [ + { + name: "semester", + description: "Which semester to query", + type: DiscordJS.Constants.ApplicationCommandOptionTypes.STRING, + required: true, + choices: [ + //TODO: Make this dynamic by querying the catalog api for all available semesters when the bot starts and then updating this list when the bot is restarted + { + name: "Spring 2023", + value: "2232", + }, + { + name: "Summer 2023", + value: "2228", + }, + ], + }, + ], + + callback: async ({ interaction }) => { + interaction.deferReply(); // Defer the reply until all entries have been created or updated + const srcdb = interaction.options.getString("semester"); + const current_courses = await classModel.find({}); + const new_courses = await getCoursesWithDescription(srcdb!); // skipcq: JS-0339 + + // Set all current entries to inactive so when we update the db we can know the ones that are no longer active. they will get moved to the PAST CLASSES category + current_courses.forEach((course) => { + course.ACTIVE = false; + }); + + const courses_to_save: (Document & // skipcq: JS-0323 + IClass & { _id: Types.ObjectId })[] = []; + + // Loop through all the new classes info and update current entries or create new ones + for (const new_course of new_courses) { + const found_courses = await classModel.find({ NAME: new_course.code }); + if (found_courses === undefined || found_courses.length === 0) { + const newClass = new classModel({ + NAME: new_course.code, + TITLE: new_course.title, + INFO: new_course.description, + ACTIVE: true, + DUPE: false, + }); + newClass.ROLE_NAME = cleanRoleString(getCourseName(newClass)); + courses_to_save.push(newClass); + } else { + let found_dupe = false; + const multiple_results = found_courses.length > 1 ? true : false; // If there are multiple results for a class then it is a duplicate + for (const found_course of found_courses) { + found_course.DUPE = multiple_results; + if (found_course.TITLE === new_course.title) { + found_course.ACTIVE = true; + found_course.NAME = new_course.code; + found_course.INFO = new_course.description; + found_dupe = true; + } + } + if (found_dupe === false) { + const newClass = new classModel({ + NAME: new_course.code, + TITLE: new_course.title, + INFO: new_course.description, + ACTIVE: true, + DUPE: multiple_results, + }); + courses_to_save.push(newClass); + } + } + } + + await classModel.bulkSave(courses_to_save).then(async () => { + await classModel.bulkSave(current_courses).then(() => { + interaction.editReply({ content: "Database updated!" }); // Can't be ephemeral because the interaction is deferred and deferred edits can't be ephemeral + }); + }); + + //TODO: Reply to interaction when all classes are updated + + // Log the command usage + console.log( + chalk.blue( + `${chalk.green(`[COMMAND]`)} ${chalk.yellow( + interaction.user.tag + )} used the ${chalk.green(`/updateDB`)} command in ${chalk.yellow( + interaction.guild?.name + )}` + ) + ); + }, +} as ICommand; diff --git a/src/models/classModel.ts b/src/models/classModel.ts index 3163a5f..30d6afb 100644 --- a/src/models/classModel.ts +++ b/src/models/classModel.ts @@ -18,11 +18,11 @@ const ClassSchema = new Schema({ NAME: { type: String, required: true }, TITLE: { type: String, required: true }, INFO: { type: String, required: true }, - DUPE: { type: Boolean, required: true }, + DUPE: { type: Boolean, required: false }, ACTIVE: { type: Boolean, required: true }, - ROLE_NAME: { type: String, required: true }, - ROLE_ID: { type: String, required: true }, - CHANNEL_ID: { type: String }, + ROLE_NAME: { type: String, required: false }, + ROLE_ID: { type: String, required: false }, + CHANNEL_ID: { type: String, required: false }, }); const name = "class"; diff --git a/src/utils/channels.ts b/src/utils/channels.ts index 9bd0cf5..6449b6f 100644 --- a/src/utils/channels.ts +++ b/src/utils/channels.ts @@ -28,16 +28,6 @@ export function cleanChannelString(name: string): string { export function getTopic(course: IClass): string { return `${course.TITLE} | ${course.INFO}`.slice(0, 1024); } -/** - * @description - Gets the course name, if it is a duplicate, it adds the title to the end of the name - * @author John Schiltz - * @export - * @param course - * @return - The course name - */ -export function getCourseName(course: IClass) { - return course.DUPE === true ? `${course.NAME}-${course.TITLE}` : course.NAME; -} /** * @description - Checks if a channel exists in the guild first by id, then by name diff --git a/src/utils/course_cleaning.ts b/src/utils/course_cleaning.ts new file mode 100644 index 0000000..98b3640 --- /dev/null +++ b/src/utils/course_cleaning.ts @@ -0,0 +1,50 @@ +import { IClass } from "../models/classModel"; +const shorteningMap = new Map([["compsci", "cs"]]); + +/** + * @description - Cleans a course code by removing spaces and replacing matches in the shorteningMap + * @author John Schiltz + * @export + * @param course_code - the course code to clean + * @return {*} - the cleaned course code + */ +export function cleanCourseCode(course_code: string): string { + let clean_course_code = course_code.toLowerCase(); + shorteningMap.forEach((value, key) => { + clean_course_code = clean_course_code.replace(key, value); + }); + return clean_course_code.trim(); +} + +/** + * @description - Removes all html tags from a string and adds spaces after periods + * @author Nathen Goldsborough + * @export + * @param s + * @return {*} + */ +export function removeHTML(s: string): string { + return s.replace(/<\/?[^>]+(>|$)/g, "").replace(".", ". "); +} + +/** + * @description - Gets the course name, if it is a duplicate, it adds the title to the end of the name + * @author John Schiltz + * @export + * @param course + * @return - The course name + */ +export function getCourseName(course: IClass) { + return course.DUPE === true ? `${course.NAME}-${course.TITLE}` : course.NAME; +} + +/** + * @description - Cleans the course title string for computer science courses by removing the "advanced topics in computer science:" part of the title + * @author John Schiltz + * @export + * @param course + * @return - The cleaned string + */ +export function cleanCompSciTitle(s: string): string { + return s.replace(/(?:advanced )?topics in computer science:/gim, "").trim(); +} diff --git a/src/utils/sleep.ts b/src/utils/sleep.ts index 6d492e6..65e75ce 100644 --- a/src/utils/sleep.ts +++ b/src/utils/sleep.ts @@ -1,3 +1,10 @@ +/** + * @description Creates a promise that resolves itself after the given number of milliseconds + * @author John Schiltz + * @export + * @param ms + * @return {*} + */ export function sleep(ms: number) { // Create new promise that resolves itself after a delay of return new Promise((resolve: (args: void) => void) =>