@@ -302,10 +302,18 @@ export const getContestNameLabel = (contestId: string) => {
302
302
return 'TDPC' ;
303
303
}
304
304
305
+ if ( contestId . startsWith ( 'past' ) ) {
306
+ return getPastContestLabel ( PAST_TRANSLATIONS , contestId ) ;
307
+ }
308
+
305
309
if ( contestId === 'practice2' ) {
306
310
return 'ACL Practice' ;
307
311
}
308
312
313
+ if ( contestId . startsWith ( 'joi' ) ) {
314
+ return getJoiContestLabel ( contestId ) ;
315
+ }
316
+
309
317
if ( contestId === 'tessoku-book' ) {
310
318
return '競技プログラミングの鉄則' ;
311
319
}
@@ -324,20 +332,203 @@ export const getContestNameLabel = (contestId: string) => {
324
332
325
333
// AIZU ONLINE JUDGE
326
334
if ( aojCoursePrefixes . has ( contestId ) ) {
327
- return 'AOJ Courses' ;
335
+ return getAojContestLabel ( AOJ_COURSES , contestId ) ;
328
336
}
329
337
330
338
if ( contestId . startsWith ( 'PCK' ) ) {
331
- return getAojChallengeLabel ( PCK_TRANSLATIONS , contestId ) ;
339
+ return getAojContestLabel ( PCK_TRANSLATIONS , contestId ) ;
332
340
}
333
341
334
342
if ( contestId . startsWith ( 'JAG' ) ) {
335
- return getAojChallengeLabel ( JAG_TRANSLATIONS , contestId ) ;
343
+ return getAojContestLabel ( JAG_TRANSLATIONS , contestId ) ;
336
344
}
337
345
338
346
return contestId . toUpperCase ( ) ;
339
347
} ;
340
348
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
+
341
532
/**
342
533
* Generates a formatted contest label for AtCoder University contests.
343
534
*
@@ -349,6 +540,10 @@ export const getContestNameLabel = (contestId: string) => {
349
540
* @returns The formatted contest label (ex: UTPC 2023).
350
541
*/
351
542
export function getAtCoderUniversityContestLabel ( contestId : string ) : string {
543
+ if ( ! regexForAtCoderUniversity . test ( contestId ) ) {
544
+ throw new Error ( `Invalid university contest ID format: ${ contestId } ` ) ;
545
+ }
546
+
352
547
return contestId . replace (
353
548
regexForAtCoderUniversity ,
354
549
( _ , contestType , common , contestYear ) =>
@@ -386,7 +581,7 @@ const JAG_TRANSLATIONS = {
386
581
Regional : ' 模擬地区 ' ,
387
582
} ;
388
583
389
- function getAojChallengeLabel (
584
+ export function getAojContestLabel (
390
585
translations : Readonly < ContestLabelTranslations > ,
391
586
contestId : string ,
392
587
) : string {
@@ -410,5 +605,7 @@ export const addContestNameToTaskIndex = (contestId: string, taskTableIndex: str
410
605
} ;
411
606
412
607
function 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
+ ) ;
414
611
}
0 commit comments