diff --git a/prisma/migrations/20260531130053_add_aoj_university_to_contest_type/migration.sql b/prisma/migrations/20260531130053_add_aoj_university_to_contest_type/migration.sql new file mode 100644 index 000000000..5f0b86460 --- /dev/null +++ b/prisma/migrations/20260531130053_add_aoj_university_to_contest_type/migration.sql @@ -0,0 +1,2 @@ +-- AlterEnum +ALTER TYPE "ContestType" ADD VALUE 'AOJ_UNIVERSITY'; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e6b28ba97..75e189678 100755 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -323,6 +323,7 @@ enum ContestType { AOJ_PCK // All-Japan High School Programming Contest (PCK) AOJ_ICPC // ICPC (International Collegiate Programming Contest) AOJ_JAG // ACM-ICPC Japan Alumni Group Contest (JAG) + AOJ_UNIVERSITY // University Programming Contest (RUPC, HUPC, UAPC) } // 11Q(最も簡単)〜6D(最難関)。 diff --git a/prisma/tasks.ts b/prisma/tasks.ts index b2f9534d2..b728a0d75 100755 --- a/prisma/tasks.ts +++ b/prisma/tasks.ts @@ -11973,4 +11973,25 @@ export const tasks = [ name: 'World Trip', title: '2349. World Trip', }, + { + id: '3058', + contest_id: 'AOJ-UAPC2019-in-RUPC2019-day2', + problem_index: '3058', + name: 'Ghost', + title: '3058. Ghost', + }, + { + id: '2903', + contest_id: 'AOJ-RUPC2018-in-ACPC2018-day1', + problem_index: '2903', + name: 'Board', + title: '2903. Board', + }, + { + id: '3171', + contest_id: 'AOJ-HUPC2020-in-HUPC2020-day1', + problem_index: '3171', + name: 'Traditional Company', + title: '3171. Traditional Company', + }, ]; diff --git a/src/lib/types/contest.ts b/src/lib/types/contest.ts index e2758739b..d60f9a20b 100644 --- a/src/lib/types/contest.ts +++ b/src/lib/types/contest.ts @@ -53,6 +53,7 @@ export const ContestType: { [key in ContestTypeOrigin]: key } = { AOJ_PCK: 'AOJ_PCK', // All-Japan High School Programming Contest (PCK) AOJ_ICPC: 'AOJ_ICPC', // ICPC (International Collegiate Programming Contest) AOJ_JAG: 'AOJ_JAG', // ACM-ICPC Japan Alumni Group Contest (JAG) + AOJ_UNIVERSITY: 'AOJ_UNIVERSITY', // University Programming Contest (RUPC, HUPC, UAPC) } as const; // Re-exporting the original type with the original name. diff --git a/src/lib/utils/contest.ts b/src/lib/utils/contest.ts index 803c5d045..70d975c7a 100644 --- a/src/lib/utils/contest.ts +++ b/src/lib/utils/contest.ts @@ -1,6 +1,7 @@ import { ContestType, type ContestPrefix, type ContestLabelTranslations } from '$lib/types/contest'; -const regexForJag = /^JAG(Prelim|Regional|Summer|Winter|Spring)\d{4}(-day\d+[A-Z]?)?$/; +export const regexForJag = /^JAG(Prelim|Regional|Summer|Winter|Spring)\d{4}(-day\d+[A-Z]?)?$/; +export const regexForAojUniversity = /^AOJ-[A-Z]+PC\d{4}/; // See: // https://github.com/kenkoooo/AtCoderProblems/blob/master/atcoder-problems-frontend/src/utils/ContestClassifier.ts @@ -107,6 +108,10 @@ export const classifyContest = (contest_id: string) => { return ContestType.AOJ_JAG; } + if (regexForAojUniversity.test(contest_id)) { + return ContestType.AOJ_UNIVERSITY; + } + return null; }; @@ -271,13 +276,13 @@ export function getContestPrefixes(contestPrefixes: Record) { } /** - * Contest type priorities (0 = Highest, 24 = Lowest) + * Contest type priorities (0 = Highest, 25 = Lowest) * * Priority assignment rationale: * - Educational contests (0-11, 17): ABS, ABC, APG4B and AWC etc. * - Contests for genius (12-16): ARC, AGC, and their variants * - Special contests (18-20): UNIVERSITY, FPS_24, OTHERS - * - External platforms (21-24): AOJ_COURSES, AOJ_PCK, AOJ_ICPC, AOJ_JAG + * - External platforms (21-25): AOJ_COURSES, AOJ_PCK, AOJ_ICPC, AOJ_JAG, AOJ_UNIVERSITY * * @remarks * HACK: The priorities for ARC, AGC, UNIVERSITY, AOJ_COURSES, and AOJ_PCK are temporary @@ -312,6 +317,7 @@ export const contestTypePriorities: Map = new Map([ [ContestType.AOJ_PCK, 22], [ContestType.AOJ_ICPC, 23], [ContestType.AOJ_JAG, 24], + [ContestType.AOJ_UNIVERSITY, 25], ]); export function getContestPriority(contestId: string): number { @@ -462,6 +468,10 @@ export const getContestNameLabel = (contestId: string) => { return getAojContestLabel(JAG_TRANSLATIONS, contestId); } + if (regexForAojUniversity.test(contestId)) { + return getAojUniversityContestLabel(contestId); + } + return contestId.toUpperCase(); }; @@ -689,6 +699,17 @@ const PCK_TRANSLATIONS = { Final: ' 本選 ', }; +function getAojUniversityContestLabel(contestId: string): string { + const label = contestId + .replace(/^AOJ-/, '') + .replace(/UAPC/g, 'ACPC') + .replace(/([A-Z]{2,})(\d{4})/g, '$1 $2') + .replace(/-in-/, ' in ') + .replace(/-day(\d+)/, ' Day$1') + .replace(/-summer/, ' Summer'); + return '(' + label + ')'; +} + /** * Maps JAG contest type abbreviations to their Japanese translations. * @@ -740,6 +761,7 @@ function isAojContest(contestId: string): boolean { aojCoursePrefixes.has(contestId) || contestId.startsWith('PCK') || regexForJag.test(contestId) || - contestId.startsWith('ICPC') + contestId.startsWith('ICPC') || + regexForAojUniversity.test(contestId) ); } diff --git a/src/lib/utils/task.ts b/src/lib/utils/task.ts index 3f384e5a4..3827cf94b 100644 --- a/src/lib/utils/task.ts +++ b/src/lib/utils/task.ts @@ -5,7 +5,12 @@ import type { UrlGenerator, UrlGenerators } from '$lib/types/url'; import { type WorkBookTaskBase } from '$features/workbooks/types/workbook'; import { ATCODER_BASE_CONTEST_URL, AOJ_TASKS_URL } from '$lib/constants/urls'; -import { getPrefixForAojCourses, getContestPriority } from '$lib/utils/contest'; +import { + getPrefixForAojCourses, + getContestPriority, + regexForAojUniversity, + regexForJag, +} from '$lib/utils/contest'; // TODO: Codeforces、yukicoder、BOJなどに対応できるようにする /** @@ -36,8 +41,9 @@ class AojGenerator implements UrlGenerator { return ( getPrefixForAojCourses().includes(contestId) || contestId.startsWith('PCK') || - contestId.startsWith('JAG') || - contestId.startsWith('ICPC') + regexForJag.test(contestId) || + contestId.startsWith('ICPC') || + regexForAojUniversity.test(contestId) ); } diff --git a/src/test/lib/utils/contest.test.ts b/src/test/lib/utils/contest.test.ts index dea8e29dd..85404122d 100644 --- a/src/test/lib/utils/contest.test.ts +++ b/src/test/lib/utils/contest.test.ts @@ -229,6 +229,14 @@ describe('Contest', () => { }); }); }); + + describe('when contest_id means AOJ University (RUPC, HUPC, UAPC)', () => { + TestCasesForContestType.aojUniversity.forEach(({ name, value }) => { + runTests(`${name}`, [value], ({ contestId, expected }: TestCaseForContestType) => { + expect(classifyContest(contestId)).toEqual(expected); + }); + }); + }); }); }); @@ -435,6 +443,14 @@ describe('Contest', () => { }); }); }); + + describe('when contest_id means AOJ University (RUPC, HUPC, UAPC)', () => { + TestCasesForContestType.aojUniversity.forEach(({ name, value }) => { + runTests(`${name}`, [value], ({ contestId, expected }: TestCaseForContestType) => { + expect(getContestPriority(contestId)).toEqual(contestTypePriorities.get(expected)); + }); + }); + }); }); }); @@ -530,6 +546,14 @@ describe('Contest', () => { }); }); }); + + describe('when contest_id means AOJ University (RUPC, HUPC, UAPC)', () => { + TestCasesForContestNameLabel.aojUniversity.forEach(({ name, value }) => { + runTests(`${name}`, [value], ({ contestId, expected }: TestCaseForContestNameLabel) => { + expect(getContestNameLabel(contestId)).toEqual(expected); + }); + }); + }); }); }); @@ -725,6 +749,18 @@ describe('Contest', () => { ); }); }); + + describe('when contest_id means AOJ University (RUPC, HUPC, UAPC)', () => { + TestCasesForContestNameAndTaskIndex.aojUniversity.forEach(({ name, value }) => { + runTests( + `${name}`, + [value], + ({ contestId, taskTableIndex, expected }: TestCaseForContestNameAndTaskIndex) => { + expect(addContestNameToTaskIndex(contestId, taskTableIndex)).toEqual(expected); + }, + ); + }); + }); }); }); diff --git a/src/test/lib/utils/task.test.ts b/src/test/lib/utils/task.test.ts index ee6984523..f069db06a 100644 --- a/src/test/lib/utils/task.test.ts +++ b/src/test/lib/utils/task.test.ts @@ -107,6 +107,14 @@ describe('Task', () => { }); }); }); + + describe('when contest ids and task ids for AOJ University (RUPC, HUPC, UAPC) are given', () => { + TestCasesForTaskUrl.aojUniversity.forEach(({ name, value }) => { + runTests(`${name}`, [value], ({ contestId, taskId, expected }: TestCaseForTaskUrl) => { + expect(getTaskUrl(contestId, taskId)).toBe(expected); + }); + }); + }); }); describe('count accepted tasks', () => { diff --git a/src/test/lib/utils/test_cases/contest_name_and_task_index.ts b/src/test/lib/utils/test_cases/contest_name_and_task_index.ts index b877863ce..dd7366111 100644 --- a/src/test/lib/utils/test_cases/contest_name_and_task_index.ts +++ b/src/test/lib/utils/test_cases/contest_name_and_task_index.ts @@ -888,3 +888,41 @@ const generateAojIcpcTestCases = ( export const aojIcpc = Object.entries(AOJ_ICPC_TEST_DATA).flatMap(([contestId, tasks]) => generateAojIcpcTestCases(Array(tasks.tasks.length).fill(contestId), tasks.tasks), ); + +const AOJ_UNIVERSITY_TEST_DATA = [ + { + contestId: 'AOJ-RUPC2018-in-ACPC2018-day1', + taskTableIndex: '2903', + expected: 'AOJ 2903(RUPC 2018 in ACPC 2018 Day1)', + }, + { + contestId: 'AOJ-UAPC2019-in-RUPC2019-day2', + taskTableIndex: '3058', + expected: 'AOJ 3058(ACPC 2019 in RUPC 2019 Day2)', + }, + { + contestId: 'AOJ-HUPC2020-in-HUPC2020-day1', + taskTableIndex: '3171', + expected: 'AOJ 3171(HUPC 2020 in HUPC 2020 Day1)', + }, + { contestId: 'AOJ-UAPC2003', taskTableIndex: '1000', expected: 'AOJ 1000(ACPC 2003)' }, + { + contestId: 'AOJ-UAPC2011-summer', + taskTableIndex: '1001', + expected: 'AOJ 1001(ACPC 2011 Summer)', + }, + { + contestId: 'AOJ-UAPC2012-day1', + taskTableIndex: '1002', + expected: 'AOJ 1002(ACPC 2012 Day1)', + }, +]; + +export const aojUniversity = AOJ_UNIVERSITY_TEST_DATA.map( + ({ contestId, taskTableIndex, expected }) => + createTestCaseForContestNameAndTaskIndex(`AOJ, ${contestId} - ${taskTableIndex}`)({ + contestId, + taskTableIndex, + expected, + }), +); diff --git a/src/test/lib/utils/test_cases/contest_name_labels.ts b/src/test/lib/utils/test_cases/contest_name_labels.ts index 7e1681ea3..fb7bd7f58 100644 --- a/src/test/lib/utils/test_cases/contest_name_labels.ts +++ b/src/test/lib/utils/test_cases/contest_name_labels.ts @@ -124,3 +124,30 @@ export const aojIcpc = [ expected: '(ICPC 地区予選 2023)', }), ]; + +export const aojUniversity = [ + createTestCaseForContestNameLabel('AOJ, RUPC 2018 in ACPC 2018 Day1')({ + contestId: 'AOJ-RUPC2018-in-ACPC2018-day1', + expected: '(RUPC 2018 in ACPC 2018 Day1)', + }), + createTestCaseForContestNameLabel('AOJ, HUPC 2020 in HUPC 2020 Day1')({ + contestId: 'AOJ-HUPC2020-in-HUPC2020-day1', + expected: '(HUPC 2020 in HUPC 2020 Day1)', + }), + createTestCaseForContestNameLabel('AOJ, UAPC 2019 in RUPC 2019 Day2')({ + contestId: 'AOJ-UAPC2019-in-RUPC2019-day2', + expected: '(ACPC 2019 in RUPC 2019 Day2)', + }), + createTestCaseForContestNameLabel('AOJ, UAPC 2003')({ + contestId: 'AOJ-UAPC2003', + expected: '(ACPC 2003)', + }), + createTestCaseForContestNameLabel('AOJ, UAPC 2011 Summer')({ + contestId: 'AOJ-UAPC2011-summer', + expected: '(ACPC 2011 Summer)', + }), + createTestCaseForContestNameLabel('AOJ, UAPC 2012 Day1')({ + contestId: 'AOJ-UAPC2012-day1', + expected: '(ACPC 2012 Day1)', + }), +]; diff --git a/src/test/lib/utils/test_cases/contest_type.ts b/src/test/lib/utils/test_cases/contest_type.ts index 6dfdc8e20..7463da6ff 100644 --- a/src/test/lib/utils/test_cases/contest_type.ts +++ b/src/test/lib/utils/test_cases/contest_type.ts @@ -665,3 +665,19 @@ export const aojIcpc = aojIcpcContestData.map(({ name, contestId }) => expected: ContestType.AOJ_ICPC, }), ); + +const aojUniversityContestData = [ + { name: 'AOJ, RUPC 2018 in ACPC 2018 Day1', contestId: 'AOJ-RUPC2018-in-ACPC2018-day1' }, + { name: 'AOJ, HUPC 2020 in HUPC 2020 Day1', contestId: 'AOJ-HUPC2020-in-HUPC2020-day1' }, + { name: 'AOJ, UAPC 2019 in RUPC 2019 Day2', contestId: 'AOJ-UAPC2019-in-RUPC2019-day2' }, + { name: 'AOJ, UAPC 2003', contestId: 'AOJ-UAPC2003' }, + { name: 'AOJ, UAPC 2011 Summer', contestId: 'AOJ-UAPC2011-summer' }, + { name: 'AOJ, UAPC 2012 Day1', contestId: 'AOJ-UAPC2012-day1' }, +]; + +export const aojUniversity = aojUniversityContestData.map(({ name, contestId }) => + createTestCaseForContestType(name)({ + contestId, + expected: ContestType.AOJ_UNIVERSITY, + }), +); diff --git a/src/test/lib/utils/test_cases/task_url.ts b/src/test/lib/utils/test_cases/task_url.ts index 49f518d51..abbbb8758 100644 --- a/src/test/lib/utils/test_cases/task_url.ts +++ b/src/test/lib/utils/test_cases/task_url.ts @@ -195,3 +195,20 @@ export const aojIcpc = icpcContests.flatMap((icpc) => }); }), ); + +// AOJ University contests: contest ID = AOJ-{NAME}PC{YEAR}[-...], task ID = numeric problem ID +const aojUniversityContests = [ + { contestId: 'AOJ-RUPC2018-in-ACPC2018-day1', tasks: ['2903', '2904'] }, + { contestId: 'AOJ-UAPC2019-in-RUPC2019-day2', tasks: ['3058', '3059'] }, + { contestId: 'AOJ-HUPC2020-in-HUPC2020-day1', tasks: ['3171', '3172'] }, +]; + +export const aojUniversity = aojUniversityContests.flatMap((contest) => + contest.tasks.map((task) => { + return createTestCaseForTaskUrl(`AOJ University, ${contest.contestId} ${task}`)({ + contestId: contest.contestId, + taskId: task, + expected: `${AOJ_TASKS_URL}/${task}`, + }); + }), +);