Skip to content

Commit

Permalink
Merge pull request #1540 from AtCoder-NoviSteps/#1526
Browse files Browse the repository at this point in the history
:docs: Improve contest labels (#1526)
  • Loading branch information
KATO-Hiro authored Nov 29, 2024
2 parents d50fed5 + 41bba20 commit 729dceb
Show file tree
Hide file tree
Showing 5 changed files with 479 additions and 123 deletions.
207 changes: 202 additions & 5 deletions src/lib/utils/contest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,10 +302,18 @@ export const getContestNameLabel = (contestId: string) => {
return 'TDPC';
}

if (contestId.startsWith('past')) {
return getPastContestLabel(PAST_TRANSLATIONS, contestId);
}

if (contestId === 'practice2') {
return 'ACL Practice';
}

if (contestId.startsWith('joi')) {
return getJoiContestLabel(contestId);
}

if (contestId === 'tessoku-book') {
return '競技プログラミングの鉄則';
}
Expand All @@ -324,20 +332,203 @@ export const getContestNameLabel = (contestId: string) => {

// AIZU ONLINE JUDGE
if (aojCoursePrefixes.has(contestId)) {
return 'AOJ Courses';
return getAojContestLabel(AOJ_COURSES, contestId);
}

if (contestId.startsWith('PCK')) {
return getAojChallengeLabel(PCK_TRANSLATIONS, contestId);
return getAojContestLabel(PCK_TRANSLATIONS, contestId);
}

if (contestId.startsWith('JAG')) {
return getAojChallengeLabel(JAG_TRANSLATIONS, contestId);
return getAojContestLabel(JAG_TRANSLATIONS, contestId);
}

return contestId.toUpperCase();
};

/**
* A mapping of contest dates to their respective Japanese translations.
* Each key represents a date in the format 'YYYYMM', and the corresponding value
* is the Japanese translation indicating the contest number.
*
* Note:
* After the 15th contest, the URL includes the number of times the contest has been held
*
* See:
* https://atcoder.jp/contests/archive?ratedType=0&category=50
*
* Example:
* - '201912': ' 第 1 回' (The 1st contest in December 2019)
* - '202303': ' 第 14 回' (The 14th contest in March 2023)
*/
export const PAST_TRANSLATIONS = {
'201912': ' 第 1 回',
'202004': ' 第 2 回',
'202005': ' 第 3 回',
'202010': ' 第 4 回',
'202012': ' 第 5 回',
'202104': ' 第 6 回',
'202107': ' 第 7 回',
'202109': ' 第 8 回',
'202112': ' 第 9 回',
'202203': ' 第 10 回',
'202206': ' 第 11 回',
'202209': ' 第 12 回',
'202212': ' 第 13 回',
'202303': ' 第 14 回',
};

/**
* A regular expression to match strings that representing the 15th or later PAST contests.
* The string should start with "past" followed by exactly two digits and end with "-open".
* The matching is case-insensitive.
*
* Examples:
* - "past15-open" (matches)
* - "past16-open" (matches)
* - "past99-open" (matches)
*/
const regexForPast = /^past(\d+)-open$/i;

export function getPastContestLabel(
translations: Readonly<ContestLabelTranslations>,
contestId: string,
): string {
let label = contestId;

Object.entries(translations).forEach(([abbrEnglish, japanese]) => {
label = label.replace(abbrEnglish, japanese);
});

if (label == contestId) {
label = label.replace(regexForPast, (_, round) => {
return `PAST 第 ${round} 回`;
});
}

// Remove suffix
return label.replace('-open', '').toUpperCase();
}

/**
* Regular expression to match specific patterns in contest identifiers.
*
* The pattern matches strings that follow these rules:
* - Starts with "joi" (case insensitive).
* - Optionally followed by "g" or "open".
* - Optionally represents year (4-digit number).
* - Optionally followed by "yo", "ho", "sc", or "sp" (Qual, Final and Spring camp).
* - Optionally represents year (4-digit number).
* - Optionally followed by "1" or "2" (Qual 1st, 2nd).
* - Optionally followed by "a", "b", or "c" (Round 1, 2 and 3).
*
* Flags:
* - `i`: Case insensitive matching.
*
* Examples:
* - "joi2024yo1a" (matches)
* - "joi2023ho" (matches)
* - "joisc2022" (matches)
* - "joisp2021" (matches)
* - "joig2024-open" (matches)
* - "joisc2024" (matches)
* - "joisp2022" (matches)
* - "joi24yo3d" (does not match)
*/
const regexForJoi = /^(joi)(g|open)*(\d{4})*(yo|ho|sc|sp)*(\d{4})*(1|2)*(a|b|c)*/i;

/**
* Transforms a contest ID into a formatted contest label.
*
* This function processes the given contest ID by removing specific suffixes
* and applying various transformations to generate a human-readable contest label.
*
* @param contestId - The ID of the contest to be transformed.
* @returns The formatted contest label.
*/
export function getJoiContestLabel(contestId: string): string {
let label = contestId;
// Remove suffix
label = label.replace('-open', '');

label = label.replace(
regexForJoi,
(_, base, subType, yearPrefix, division, yearSuffix, qual, qualRound) => {
const SPACE = ' ';

let newLabel = base.toUpperCase();
newLabel += addJoiSubTypeIfNeeds(subType);

if (division !== undefined) {
newLabel += SPACE;
newLabel += addJoiDivisionNameIfNeeds(division, qual);
}

newLabel += SPACE;
newLabel += addJoiYear(yearSuffix, yearPrefix);

if (qualRound !== undefined) {
newLabel += SPACE;
newLabel += addJoiQualRoundNameIfNeeds(qualRound);
}

return newLabel;
},
);

return label;
}

function addJoiSubTypeIfNeeds(subType: string): string {
if (subType === 'g') {
return subType.toUpperCase();
} else if (subType === 'open') {
return ' Open';
}

return '';
}

function addJoiDivisionNameIfNeeds(division: string, qual: string): string {
if (division === 'yo') {
if (qual === undefined) {
return '予選';
} else if (qual === '1') {
return '一次予選';
} else if (qual === '2') {
return '二次予選';
}
} else if (division === 'ho') {
return '本選';
} else if (division === 'sc' || division === 'sp') {
return '春合宿';
}

return '';
}

function addJoiYear(yearSuffix: string, yearPrefix: string): string {
if (yearPrefix !== undefined) {
return yearPrefix;
} else if (yearSuffix !== undefined) {
return yearSuffix;
}

return '';
}

function addJoiQualRoundNameIfNeeds(qualRound: string): string {
if (qualRound === 'a') {
return '第 1 回';
} else if (qualRound === 'b') {
return '第 2 回';
} else if (qualRound === 'c') {
return '第 3 回';
}

return '';
}

/**
* Generates a formatted contest label for AtCoder University contests.
*
Expand All @@ -349,6 +540,10 @@ export const getContestNameLabel = (contestId: string) => {
* @returns The formatted contest label (ex: UTPC 2023).
*/
export function getAtCoderUniversityContestLabel(contestId: string): string {
if (!regexForAtCoderUniversity.test(contestId)) {
throw new Error(`Invalid university contest ID format: ${contestId}`);
}

return contestId.replace(
regexForAtCoderUniversity,
(_, contestType, common, contestYear) =>
Expand Down Expand Up @@ -386,7 +581,7 @@ const JAG_TRANSLATIONS = {
Regional: ' 模擬地区 ',
};

function getAojChallengeLabel(
export function getAojContestLabel(
translations: Readonly<ContestLabelTranslations>,
contestId: string,
): string {
Expand All @@ -410,5 +605,7 @@ export const addContestNameToTaskIndex = (contestId: string, taskTableIndex: str
};

function isAojContest(contestId: string): boolean {
return contestId.startsWith('PCK') || contestId.startsWith('JAG');
return (
aojCoursePrefixes.has(contestId) || contestId.startsWith('PCK') || contestId.startsWith('JAG')
);
}
68 changes: 44 additions & 24 deletions src/test/lib/utils/contest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
contestTypePriorities,
getContestNameLabel,
addContestNameToTaskIndex,
getAtCoderUniversityContestLabel,
} from '$lib/utils/contest';

describe('Contest', () => {
Expand Down Expand Up @@ -366,18 +367,6 @@ describe('Contest', () => {
});
});

// TODO(#issue): Skipped until notational inconsistencies are resolved.
// Current issues:
// 1. Contest names use inconsistent formats (e.g., "past201912-open" vs "past17-open")
// 2. Need to standardize naming conventions across all contests
describe.skip('when contest_id contains past', () => {
TestCasesForContestNameLabel.past.forEach(({ name, value }) => {
runTests(`${name}`, [value], ({ contestId, expected }: TestCaseForContestNameLabel) => {
expect(getContestNameLabel(contestId)).toEqual(expected);
});
});
});

describe('when contest_id is practice2 (ACL practice)', () => {
TestCasesForContestNameLabel.aclPractice.forEach(({ name, value }) => {
runTests(`${name}`, [value], ({ contestId, expected }: TestCaseForContestNameLabel) => {
Expand All @@ -386,18 +375,6 @@ describe('Contest', () => {
});
});

// TODO(#issue): Skipped until notational inconsistencies are resolved.
// Current issues:
// 1. Contest names use inconsistent formats
// 2. Need to standardize naming conventions across all contests
describe.skip('when contest_id contains joi', () => {
TestCasesForContestNameLabel.joi.forEach(({ name, value }) => {
runTests(`${name}`, [value], ({ contestId, expected }: TestCaseForContestNameLabel) => {
expect(getContestNameLabel(contestId)).toEqual(expected);
});
});
});

describe('when contest_id contains chokudai_S', () => {
TestCasesForContestNameLabel.atCoderOthers.forEach(({ name, value }) => {
runTests(`${name}`, [value], ({ contestId, expected }: TestCaseForContestNameLabel) => {
Expand Down Expand Up @@ -446,6 +423,30 @@ describe('Contest', () => {
});
});

describe('when contest_id contains past', () => {
TestCasesForContestNameAndTaskIndex.past.forEach(({ name, value }) => {
runTests(
`${name}`,
[value],
({ contestId, taskTableIndex, expected }: TestCaseForContestNameAndTaskIndex) => {
expect(addContestNameToTaskIndex(contestId, taskTableIndex)).toEqual(expected);
},
);
});
});

describe('when contest_id contains joi', () => {
TestCasesForContestNameAndTaskIndex.joi.forEach(({ name, value }) => {
runTests(
`${name}`,
[value],
({ contestId, taskTableIndex, expected }: TestCaseForContestNameAndTaskIndex) => {
expect(addContestNameToTaskIndex(contestId, taskTableIndex)).toEqual(expected);
},
);
});
});

describe('when contest_id is tessoku-book', () => {
TestCasesForContestNameAndTaskIndex.tessokuBook.forEach(({ name, value }) => {
runTests(
Expand Down Expand Up @@ -545,4 +546,23 @@ describe('Contest', () => {
});
});
});

describe('get AtCoder university contest label', () => {
describe('expected to return correct label for valid format', () => {
test.each([
['utpc2019', 'UTPC 2019'],
['ttpc2022', 'TTPC 2022'],
])('when %s is given', (input, expected) => {
expect(getAtCoderUniversityContestLabel(input)).toBe(expected);
});
});

describe('expected to be thrown an error if an invalid format is given', () => {
test.each(['utpc24', 'ttpc', 'tupc'])('when %s is given', (input) => {
expect(() => getAtCoderUniversityContestLabel(input)).toThrow(
`Invalid university contest ID format: ${input}`,
);
});
});
});
});
Loading

0 comments on commit 729dceb

Please sign in to comment.