@@ -302,10 +302,18 @@ export const getContestNameLabel = (contestId: string) => {
302302 return 'TDPC' ;
303303 }
304304
305+ if ( contestId . startsWith ( 'past' ) ) {
306+ return getPastContestLabel ( PAST_TRANSLATIONS , contestId ) ;
307+ }
308+
305309 if ( contestId === 'practice2' ) {
306310 return 'ACL Practice' ;
307311 }
308312
313+ if ( contestId . startsWith ( 'joi' ) ) {
314+ return getJoiContestLabel ( contestId ) ;
315+ }
316+
309317 if ( contestId === 'tessoku-book' ) {
310318 return '競技プログラミングの鉄則' ;
311319 }
@@ -324,20 +332,203 @@ export const getContestNameLabel = (contestId: string) => {
324332
325333 // AIZU ONLINE JUDGE
326334 if ( aojCoursePrefixes . has ( contestId ) ) {
327- return 'AOJ Courses' ;
335+ return getAojContestLabel ( AOJ_COURSES , contestId ) ;
328336 }
329337
330338 if ( contestId . startsWith ( 'PCK' ) ) {
331- return getAojChallengeLabel ( PCK_TRANSLATIONS , contestId ) ;
339+ return getAojContestLabel ( PCK_TRANSLATIONS , contestId ) ;
332340 }
333341
334342 if ( contestId . startsWith ( 'JAG' ) ) {
335- return getAojChallengeLabel ( JAG_TRANSLATIONS , contestId ) ;
343+ return getAojContestLabel ( JAG_TRANSLATIONS , contestId ) ;
336344 }
337345
338346 return contestId . toUpperCase ( ) ;
339347} ;
340348
349+ /**
350+ * A mapping of contest dates to their respective Japanese translations.
351+ * Each key represents a date in the format 'YYYYMM', and the corresponding value
352+ * is the Japanese translation indicating the contest number.
353+ *
354+ * Note:
355+ * After the 15th contest, the URL includes the number of times the contest has been held
356+ *
357+ * See:
358+ * https://atcoder.jp/contests/archive?ratedType=0&category=50
359+ *
360+ * Example:
361+ * - '201912': ' 第 1 回' (The 1st contest in December 2019)
362+ * - '202303': ' 第 14 回' (The 14th contest in March 2023)
363+ */
364+ export const PAST_TRANSLATIONS = {
365+ '201912' : ' 第 1 回' ,
366+ '202004' : ' 第 2 回' ,
367+ '202005' : ' 第 3 回' ,
368+ '202010' : ' 第 4 回' ,
369+ '202012' : ' 第 5 回' ,
370+ '202104' : ' 第 6 回' ,
371+ '202107' : ' 第 7 回' ,
372+ '202109' : ' 第 8 回' ,
373+ '202112' : ' 第 9 回' ,
374+ '202203' : ' 第 10 回' ,
375+ '202206' : ' 第 11 回' ,
376+ '202209' : ' 第 12 回' ,
377+ '202212' : ' 第 13 回' ,
378+ '202303' : ' 第 14 回' ,
379+ } ;
380+
381+ /**
382+ * A regular expression to match strings that representing the 15th or later PAST contests.
383+ * The string should start with "past" followed by exactly two digits and end with "-open".
384+ * The matching is case-insensitive.
385+ *
386+ * Examples:
387+ * - "past15-open" (matches)
388+ * - "past16-open" (matches)
389+ * - "past99-open" (matches)
390+ */
391+ const regexForPast = / ^ p a s t ( \d + ) - o p e n $ / i;
392+
393+ export function getPastContestLabel (
394+ translations : Readonly < ContestLabelTranslations > ,
395+ contestId : string ,
396+ ) : string {
397+ let label = contestId ;
398+
399+ Object . entries ( translations ) . forEach ( ( [ abbrEnglish , japanese ] ) => {
400+ label = label . replace ( abbrEnglish , japanese ) ;
401+ } ) ;
402+
403+ if ( label == contestId ) {
404+ label = label . replace ( regexForPast , ( _ , round ) => {
405+ return `PAST 第 ${ round } 回` ;
406+ } ) ;
407+ }
408+
409+ // Remove suffix
410+ return label . replace ( '-open' , '' ) . toUpperCase ( ) ;
411+ }
412+
413+ /**
414+ * Regular expression to match specific patterns in contest identifiers.
415+ *
416+ * The pattern matches strings that follow these rules:
417+ * - Starts with "joi" (case insensitive).
418+ * - Optionally followed by "g" or "open".
419+ * - Optionally represents year (4-digit number).
420+ * - Optionally followed by "yo", "ho", "sc", or "sp" (Qual, Final and Spring camp).
421+ * - Optionally represents year (4-digit number).
422+ * - Optionally followed by "1" or "2" (Qual 1st, 2nd).
423+ * - Optionally followed by "a", "b", or "c" (Round 1, 2 and 3).
424+ *
425+ * Flags:
426+ * - `i`: Case insensitive matching.
427+ *
428+ * Examples:
429+ * - "joi2024yo1a" (matches)
430+ * - "joi2023ho" (matches)
431+ * - "joisc2022" (matches)
432+ * - "joisp2021" (matches)
433+ * - "joig2024-open" (matches)
434+ * - "joisc2024" (matches)
435+ * - "joisp2022" (matches)
436+ * - "joi24yo3d" (does not match)
437+ */
438+ const regexForJoi = / ^ ( j o i ) ( g | o p e n ) * ( \d { 4 } ) * ( y o | h o | s c | s p ) * ( \d { 4 } ) * ( 1 | 2 ) * ( a | b | c ) * / i;
439+
440+ /**
441+ * Transforms a contest ID into a formatted contest label.
442+ *
443+ * This function processes the given contest ID by removing specific suffixes
444+ * and applying various transformations to generate a human-readable contest label.
445+ *
446+ * @param contestId - The ID of the contest to be transformed.
447+ * @returns The formatted contest label.
448+ */
449+ export function getJoiContestLabel ( contestId : string ) : string {
450+ let label = contestId ;
451+ // Remove suffix
452+ label = label . replace ( '-open' , '' ) ;
453+
454+ label = label . replace (
455+ regexForJoi ,
456+ ( _ , base , subType , yearPrefix , division , yearSuffix , qual , qualRound ) => {
457+ const SPACE = ' ' ;
458+
459+ let newLabel = base . toUpperCase ( ) ;
460+ newLabel += addJoiSubTypeIfNeeds ( subType ) ;
461+
462+ if ( division !== undefined ) {
463+ newLabel += SPACE ;
464+ newLabel += addJoiDivisionNameIfNeeds ( division , qual ) ;
465+ }
466+
467+ newLabel += SPACE ;
468+ newLabel += addJoiYear ( yearSuffix , yearPrefix ) ;
469+
470+ if ( qualRound !== undefined ) {
471+ newLabel += SPACE ;
472+ newLabel += addJoiQualRoundNameIfNeeds ( qualRound ) ;
473+ }
474+
475+ return newLabel ;
476+ } ,
477+ ) ;
478+
479+ return label ;
480+ }
481+
482+ function addJoiSubTypeIfNeeds ( subType : string ) : string {
483+ if ( subType === 'g' ) {
484+ return subType . toUpperCase ( ) ;
485+ } else if ( subType === 'open' ) {
486+ return ' Open' ;
487+ }
488+
489+ return '' ;
490+ }
491+
492+ function addJoiDivisionNameIfNeeds ( division : string , qual : string ) : string {
493+ if ( division === 'yo' ) {
494+ if ( qual === undefined ) {
495+ return '予選' ;
496+ } else if ( qual === '1' ) {
497+ return '一次予選' ;
498+ } else if ( qual === '2' ) {
499+ return '二次予選' ;
500+ }
501+ } else if ( division === 'ho' ) {
502+ return '本選' ;
503+ } else if ( division === 'sc' || division === 'sp' ) {
504+ return '春合宿' ;
505+ }
506+
507+ return '' ;
508+ }
509+
510+ function addJoiYear ( yearSuffix : string , yearPrefix : string ) : string {
511+ if ( yearPrefix !== undefined ) {
512+ return yearPrefix ;
513+ } else if ( yearSuffix !== undefined ) {
514+ return yearSuffix ;
515+ }
516+
517+ return '' ;
518+ }
519+
520+ function addJoiQualRoundNameIfNeeds ( qualRound : string ) : string {
521+ if ( qualRound === 'a' ) {
522+ return '第 1 回' ;
523+ } else if ( qualRound === 'b' ) {
524+ return '第 2 回' ;
525+ } else if ( qualRound === 'c' ) {
526+ return '第 3 回' ;
527+ }
528+
529+ return '' ;
530+ }
531+
341532/**
342533 * Generates a formatted contest label for AtCoder University contests.
343534 *
@@ -349,6 +540,10 @@ export const getContestNameLabel = (contestId: string) => {
349540 * @returns The formatted contest label (ex: UTPC 2023).
350541 */
351542export function getAtCoderUniversityContestLabel ( contestId : string ) : string {
543+ if ( ! regexForAtCoderUniversity . test ( contestId ) ) {
544+ throw new Error ( `Invalid university contest ID format: ${ contestId } ` ) ;
545+ }
546+
352547 return contestId . replace (
353548 regexForAtCoderUniversity ,
354549 ( _ , contestType , common , contestYear ) =>
@@ -386,7 +581,7 @@ const JAG_TRANSLATIONS = {
386581 Regional : ' 模擬地区 ' ,
387582} ;
388583
389- function getAojChallengeLabel (
584+ export function getAojContestLabel (
390585 translations : Readonly < ContestLabelTranslations > ,
391586 contestId : string ,
392587) : string {
@@ -410,5 +605,7 @@ export const addContestNameToTaskIndex = (contestId: string, taskTableIndex: str
410605} ;
411606
412607function isAojContest ( contestId : string ) : boolean {
413- return contestId . startsWith ( 'PCK' ) || contestId . startsWith ( 'JAG' ) ;
608+ return (
609+ aojCoursePrefixes . has ( contestId ) || contestId . startsWith ( 'PCK' ) || contestId . startsWith ( 'JAG' )
610+ ) ;
414611}
0 commit comments