From 3f5c1dbacea16bfd779f824b11c515733f127ed3 Mon Sep 17 00:00:00 2001 From: "k.hiro1818" Date: Sun, 6 Sep 2026 05:02:24 +0000 Subject: [PATCH 1/5] refactor: move contest types to src/lib/contests/types/ Co-Authored-By: Claude Opus 4.6 --- src/lib/contests/types/contest.ts | 89 ++++++++++++++++++++++++++++ src/lib/types/contest.ts | 98 +++---------------------------- 2 files changed, 98 insertions(+), 89 deletions(-) create mode 100644 src/lib/contests/types/contest.ts diff --git a/src/lib/contests/types/contest.ts b/src/lib/contests/types/contest.ts new file mode 100644 index 000000000..7f59933f1 --- /dev/null +++ b/src/lib/contests/types/contest.ts @@ -0,0 +1,89 @@ +import type { TasksForImport } from '$lib/types/task'; + +export interface Contest { + id: string; + start_epoch_second: number; + duration_second: number; + title: string; + tasks: TasksForImport; +} + +export type Contests = Contest[]; + +export interface ContestForImport { + id: string; + start_epoch_second: number; + duration_second: number; + title: string; + rate_change: string; +} + +export type ContestsForImport = ContestForImport[]; + +// Import original enum as type. +import type { ContestType as ContestTypeOrigin } from '@prisma/client'; + +/** + * An object representing various types of programming contests. + * Each key is a contest type identifier, and the value is the same identifier as a string. + */ +export const ContestType: { [key in ContestTypeOrigin]: key } = { + ABC: 'ABC', // AtCoder Beginner Contest + APG4B: 'APG4B', // C++入門 AtCoder Programming Guide for beginners + ABS: 'ABS', // AtCoder Beginners Selection + ACL_PRACTICE: 'ACL_PRACTICE', // AtCoder Library Practice Contest + PAST: 'PAST', // Practical Algorithm Skill Test (アルゴリズム実技検定) + EDPC: 'EDPC', // Educational DP Contest / DP まとめコンテスト + TDPC: 'TDPC', // Typical DP Contest + NDPC: 'NDPC', // Next DP Contest + JOI: 'JOI', // Japanese Olympiad in Informatics + TYPICAL90: 'TYPICAL90', // 競プロ典型 90 問 + TESSOKU_BOOK: 'TESSOKU_BOOK', // 競技プログラミングの鉄則 + MATH_AND_ALGORITHM: 'MATH_AND_ALGORITHM', // アルゴリズムと数学 + ARC: 'ARC', // AtCoder Regular Contest + AGC: 'AGC', // AtCoder Grand Contest + ABC_LIKE: 'ABC_LIKE', // AtCoder Beginner Contest (ABC) 相当のコンテスト + ARC_LIKE: 'ARC_LIKE', // AtCoder Regular Contest (ARC) 相当のコンテスト + AGC_LIKE: 'AGC_LIKE', // AtCoder Grand Contest (AGC) 相当のコンテスト + AWC: 'AWC', // AtCoder Weekday Contest + UNIVERSITY: 'UNIVERSITY', // University Programming Contests in AtCoder (e.g., UTPC) + FPS_24: 'FPS_24', // 24 Problems on Formal Power Series + ATCODER_MAIN_OFFICIAL_ONSITE: 'ATCODER_MAIN_OFFICIAL_ONSITE', // AtCoder World Tour Finals (official onsite finals) + OTHERS: 'OTHERS', // AtCoder (その他) + AOJ_COURSES: 'AOJ_COURSES', // AIZU ONLINE JUDGE Courses + 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. +export type ContestType = ContestTypeOrigin; + +/** + * Represents a mapping of contest IDs to contest names. + * + * @interface ContestPrefix + * @property {string} [key] - The contest ID. + * @property {string} [key: string] - The contest name associated with the contest ID. + * @example + * { + * "abc001": "AtCoder Beginner Contest 001", + * "arc123": "AtCoder Regular Contest 123", + * "utpc2023": "University of Tokyo Programming Contest 2023" + * } + */ +export interface ContestPrefix { + [key: string]: string; +} + +/** + * Represents a collection of translations for contest labels. + * The keys are language codes or identifiers, and the values are the translated strings. + * + * @property {string} [key] - The abbr contest type in English. + * @property {string} [key: string] - The contest name in Japanese. + */ +export type ContestLabelTranslations = { + [key: string]: string; +}; diff --git a/src/lib/types/contest.ts b/src/lib/types/contest.ts index 7f59933f1..5eee3f07f 100644 --- a/src/lib/types/contest.ts +++ b/src/lib/types/contest.ts @@ -1,89 +1,9 @@ -import type { TasksForImport } from '$lib/types/task'; - -export interface Contest { - id: string; - start_epoch_second: number; - duration_second: number; - title: string; - tasks: TasksForImport; -} - -export type Contests = Contest[]; - -export interface ContestForImport { - id: string; - start_epoch_second: number; - duration_second: number; - title: string; - rate_change: string; -} - -export type ContestsForImport = ContestForImport[]; - -// Import original enum as type. -import type { ContestType as ContestTypeOrigin } from '@prisma/client'; - -/** - * An object representing various types of programming contests. - * Each key is a contest type identifier, and the value is the same identifier as a string. - */ -export const ContestType: { [key in ContestTypeOrigin]: key } = { - ABC: 'ABC', // AtCoder Beginner Contest - APG4B: 'APG4B', // C++入門 AtCoder Programming Guide for beginners - ABS: 'ABS', // AtCoder Beginners Selection - ACL_PRACTICE: 'ACL_PRACTICE', // AtCoder Library Practice Contest - PAST: 'PAST', // Practical Algorithm Skill Test (アルゴリズム実技検定) - EDPC: 'EDPC', // Educational DP Contest / DP まとめコンテスト - TDPC: 'TDPC', // Typical DP Contest - NDPC: 'NDPC', // Next DP Contest - JOI: 'JOI', // Japanese Olympiad in Informatics - TYPICAL90: 'TYPICAL90', // 競プロ典型 90 問 - TESSOKU_BOOK: 'TESSOKU_BOOK', // 競技プログラミングの鉄則 - MATH_AND_ALGORITHM: 'MATH_AND_ALGORITHM', // アルゴリズムと数学 - ARC: 'ARC', // AtCoder Regular Contest - AGC: 'AGC', // AtCoder Grand Contest - ABC_LIKE: 'ABC_LIKE', // AtCoder Beginner Contest (ABC) 相当のコンテスト - ARC_LIKE: 'ARC_LIKE', // AtCoder Regular Contest (ARC) 相当のコンテスト - AGC_LIKE: 'AGC_LIKE', // AtCoder Grand Contest (AGC) 相当のコンテスト - AWC: 'AWC', // AtCoder Weekday Contest - UNIVERSITY: 'UNIVERSITY', // University Programming Contests in AtCoder (e.g., UTPC) - FPS_24: 'FPS_24', // 24 Problems on Formal Power Series - ATCODER_MAIN_OFFICIAL_ONSITE: 'ATCODER_MAIN_OFFICIAL_ONSITE', // AtCoder World Tour Finals (official onsite finals) - OTHERS: 'OTHERS', // AtCoder (その他) - AOJ_COURSES: 'AOJ_COURSES', // AIZU ONLINE JUDGE Courses - 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. -export type ContestType = ContestTypeOrigin; - -/** - * Represents a mapping of contest IDs to contest names. - * - * @interface ContestPrefix - * @property {string} [key] - The contest ID. - * @property {string} [key: string] - The contest name associated with the contest ID. - * @example - * { - * "abc001": "AtCoder Beginner Contest 001", - * "arc123": "AtCoder Regular Contest 123", - * "utpc2023": "University of Tokyo Programming Contest 2023" - * } - */ -export interface ContestPrefix { - [key: string]: string; -} - -/** - * Represents a collection of translations for contest labels. - * The keys are language codes or identifiers, and the values are the translated strings. - * - * @property {string} [key] - The abbr contest type in English. - * @property {string} [key: string] - The contest name in Japanese. - */ -export type ContestLabelTranslations = { - [key: string]: string; -}; +export { + type Contest, + type Contests, + type ContestForImport, + type ContestsForImport, + ContestType, + type ContestPrefix, + type ContestLabelTranslations, +} from '$lib/contests/types/contest'; From b58e0a638da9579a98430a8fb500e303e669c31b Mon Sep 17 00:00:00 2001 From: "k.hiro1818" Date: Sun, 6 Sep 2026 05:03:37 +0000 Subject: [PATCH 2/5] refactor: move contest utils to src/lib/contests/utils/ Co-Authored-By: Claude Opus 4.6 --- src/lib/contests/utils/contest.ts | 830 +++++++++++++++++++++++++++++ src/lib/utils/contest.ts | 845 +----------------------------- 2 files changed, 849 insertions(+), 826 deletions(-) create mode 100644 src/lib/contests/utils/contest.ts diff --git a/src/lib/contests/utils/contest.ts b/src/lib/contests/utils/contest.ts new file mode 100644 index 000000000..5ec61cbef --- /dev/null +++ b/src/lib/contests/utils/contest.ts @@ -0,0 +1,830 @@ +import { + ContestType, + type ContestPrefix, + type ContestLabelTranslations, +} from '$lib/contests/types/contest'; + +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 +export const classifyContest = (contest_id: string) => { + // AtCoder + if (/^abc\d{3}$/.exec(contest_id)) { + return ContestType.ABC; + } + + if (/^arc\d{3}$/.exec(contest_id)) { + return ContestType.ARC; + } + + if (/^agc\d{3}$/.exec(contest_id)) { + return ContestType.AGC; + } + + if (/^awc\d{4}$/.exec(contest_id)) { + return ContestType.AWC; + } + + if (contest_id.startsWith('APG4b')) { + return ContestType.APG4B; + } + + if (contest_id === 'abs') { + return ContestType.ABS; + } + + if (contest_id === 'typical90') { + return ContestType.TYPICAL90; + } + + if (contest_id === 'dp') { + return ContestType.EDPC; + } + + if (contest_id === 'tdpc') { + return ContestType.TDPC; + } + + if (contest_id === 'ndpc') { + return ContestType.NDPC; + } + + if (contest_id.startsWith('past')) { + return ContestType.PAST; + } + + if (contest_id === 'practice2') { + return ContestType.ACL_PRACTICE; + } + + if (contest_id.startsWith('joi')) { + return ContestType.JOI; + } + + if (contest_id === 'tessoku-book') { + return ContestType.TESSOKU_BOOK; + } + + if (contest_id === 'math-and-algorithm') { + return ContestType.MATH_AND_ALGORITHM; + } + + if (contest_id === 'fps-24') { + return ContestType.FPS_24; + } + + if (isWorldTourFinals(contest_id)) { + return ContestType.ATCODER_MAIN_OFFICIAL_ONSITE; + } + + if (abcLikePrefixes.has(contest_id)) { + return ContestType.ABC_LIKE; + } + + if (arcLikePrefixes.has(contest_id)) { + return ContestType.ARC_LIKE; + } + + if (agcLikePrefixes.some((prefix) => contest_id.startsWith(prefix))) { + return ContestType.AGC_LIKE; + } + + if (atCoderUniversityPrefixes.some((prefix) => contest_id.startsWith(prefix))) { + return ContestType.UNIVERSITY; + } + + if (atCoderOthersPrefixes.some((prefix) => contest_id.startsWith(prefix))) { + return ContestType.OTHERS; + } + + // AIZU ONLINE JUDGE + if (aojCoursePrefixes.has(contest_id)) { + return ContestType.AOJ_COURSES; + } + + if (/^PCK(Prelim|Final)\d*$/.exec(contest_id)) { + return ContestType.AOJ_PCK; + } + + if (/^ICPC(Prelim|Regional)\d*$/.exec(contest_id)) { + return ContestType.AOJ_ICPC; + } + + if (regexForJag.exec(contest_id)) { + return ContestType.AOJ_JAG; + } + + if (regexForAojUniversity.test(contest_id)) { + return ContestType.AOJ_UNIVERSITY; + } + + return null; +}; + +// HACK: As of December 2025, the following contests are applicable. +// Note: The classification logic may need to be revised when new contests are added. +const ABC_LIKE: ContestPrefix = { + 'tenka1-2017-beginner': 'Tenka1 Programmer Beginner Contest 2017', + abl: 'ACL Beginner Contest', + caddi2018b: 'CADDi 2018 for Beginners', + 'soundhound2018-summer-qual': 'SoundHound Inc. Programming Contest 2018 -Masters Tournament-', + 'tenka1-2018-beginner': 'Tenka1 Programmer Beginner Contest 2018', + aising2019: 'エイシング プログラミング コンテスト 2019', + sumitrust2019: '三井住友信託銀行プログラミングコンテスト2019', + 'tenka1-2019-beginner': 'Tenka1 Programmer Beginner Contest 2019', + aising2020: 'エイシング プログラミング コンテスト 2020', + hhkb2020: 'HHKB プログラミングコンテスト 2020', + 'm-solutions2020': 'M-SOLUTIONS プロコンオープン 2020', + panasonic2020: 'パナソニックプログラミングコンテスト 2020', + jsc2021: '第二回日本最強プログラマー学生選手権', + zone2021: 'ZONeエナジー プログラミングコンテスト “HELLO SPACE”', + 'jsc2025advance-final': '日本最強プログラマー学生選手権~Advance~', +} as const; +const abcLikePrefixes = new Set(getContestPrefixes(ABC_LIKE)); + +const ARC_LIKE: ContestPrefix = { + 'tenka1-2017': 'Tenka1 Programmer Contest 2017', + 'tenka1-2018': 'Tenka1 Programmer Contest 2018', + 'tenka1-2019': 'Tenka1 Programmer Contest 2019', + caddi2018: 'CADDi 2018', + 'dwacon5th-prelims': '第5回 ドワンゴからの挑戦状 予選', + 'dwacon6th-prelims': '第6回 ドワンゴからの挑戦状 予選', + diverta2019: 'diverta 2019 Programming Contest', + keyence2019: 'キーエンス プログラミング コンテスト 2019', + keyence2020: 'キーエンス プログラミング コンテスト 2020', + keyence2021: 'キーエンス プログラミング コンテスト 2021', + 'jsc2019-qual': '第一回日本最強プログラマー学生選手権-予選-', + 'nikkei2019-qual': '全国統一プログラミング王決定戦予選', + acl1: 'ACL Contest 1', +} as const; +const arcLikePrefixes = new Set(getContestPrefixes(ARC_LIKE)); + +const AGC_LIKE: ContestPrefix = { + 'code-festival-2016-qual': 'CODE FESTIVAL 2016 qual', + 'code-festival-2017-qual': 'CODE FESTIVAL 2017 qual', + 'cf16-final': 'CODE FESTIVAL 2016 final', + 'cf17-final': 'CODE FESTIVAL 2017 final', +} as const; +const agcLikePrefixes = getContestPrefixes(AGC_LIKE); + +// HACK: As of September 2025, KUPC, QUPC, UTPC, TTPC and TUPC are included. +// More university contests may be added in the future. +/** + * Maps university contest ID prefixes to their display names. + * + * @example + * { + * kupc: 'KUPC' // Kyoto University Programming Contest + * qupc: 'QUPC' // Kyushu University Programming Contest + * utpc: 'UTPC' // University of Tokyo Programming Contest + * ttpc: 'TTPC' // Tokyo Institute of Technology Programming Contest + * tupc: 'TUPC' // Tohoku University Programming Contest + * wupc: 'WUPC' // Waseda University Programming Contest + * } + * + * @remarks + * When adding new university contests: + * 1. Use lowercase prefix as key + * 2. Use official contest name as value + * 3. Ensure prefix doesn't conflict with existing contest types + */ +const ATCODER_UNIVERSITIES: ContestPrefix = { + kupc: 'KUPC', + qupc: 'QUPC', + utpc: 'UTPC', + ttpc: 'TTPC', + tupc: 'TUPC', + wupc: 'WUPC', +} as const; + +const atCoderUniversityPrefixes = getContestPrefixes(ATCODER_UNIVERSITIES); + +// World Tour Finals (AtCoder official onsite finals), Algorithm division only. +// +// Seeded contest_id values carry a trailing "-open" (e.g. wtf19-open), except +// wtf22-day2 which AtCoder Problems records without it; strip it before matching +// so both forms work and it never leaks into the display label. +// +// From 2025 the id gains an "algo" infix (awtf2025algo-open) to disambiguate +// from the new Heuristic division (awtf2025heuristic), which stays out of scope. +const regexForWorldTourFinals = /^(wtf19|wtf22-day[12]|awtf2024|awtf20\d{2}algo)$/; + +export const isWorldTourFinals = (contestId: string): boolean => + regexForWorldTourFinals.test(stripOpenSuffix(contestId)); + +export const getWorldTourFinalsLabel = (contestId: string): string => { + const base = 'World Tour Finals'; + const id = stripOpenSuffix(contestId); + + if (id === 'wtf19') { + return `${base} 2019`; + } + + const dayMatch = /^wtf22-day([12])$/.exec(id); + + if (dayMatch) { + return `${base} 2022 Day${dayMatch[1]}`; + } + + if (id === 'awtf2024') { + return `${base} 2024`; + } + + const algoMatch = /^awtf(20\d{2})algo$/.exec(id); + + if (algoMatch) { + // "Algorithm" distinguishes from the Heuristic division, introduced in 2025. + return `${base} ${algoMatch[1]} Algorithm`; + } + + return contestId.toUpperCase(); +}; + +const stripOpenSuffix = (contestId: string): string => + contestId.endsWith('-open') ? contestId.slice(0, -'-open'.length) : contestId; + +/** + * Maps other AtCoder contest ID prefixes to their display names. + * Includes special, corporate, and promotional contests that don't fit other categories. + * + * @example + * { + * 'mujin-pc-2018': 'Mujin Programming Challenge 2018', + * 'discovery2016': 'DISCO presents ディスカバリーチャンネル プログラミングコンテスト2016' + * } + * + * @remarks + * When adding new contests: + * 1. Use kebab-case for contest ID prefix as key + * 2. Use official contest name in English or Japanese as value + * 3. Ensure the contest doesn't belong to other specific categories + */ +const ATCODER_OTHERS: ContestPrefix = { + chokudai_S: 'Chokudai SpeedRun', + atc001: 'AtCoder Typical Contest 001', + geocon2013: '幾何コンテスト2013', + 's8pc-3': 'square869120Contest #3', + 's8pc-4': 'square869120Contest #4', + 'maximum-cup-2013': 'Maximum-Cup 2013', + 'maximum-cup-2018': 'Maximum-Cup 2018', + 'code-festival-2014-quala': 'Code Festival 2014 予選 A', + 'code-festival-2014-qualb': 'Code Festival 2014 予選 B', + 'code-festival-2014-final': 'Code Festival 2014 決勝', + 'code-festival-2014-china-open': 'Code Festival 2014 上海', + 'code-festival-2015-qualb': 'Code Festival 2015 予選 B', + 'code-festival-2015-morning-middle': 'CODE FESTIVAL 2015 あさぷろ Middle', + 'code-festival-2015-exhibition': 'CODE FESTIVAL 2015 エキシビション', + 'code-thanks-festival': 'CODE THANKS FESTIVAL', + donuts: 'Donutsプロコンチャレンジ', + indeednow: 'Indeedなう', + 'tkppc4-2': '技術室奥プログラミングコンテスト#4 Day2', + 'dwango2016-prelims': '第2回 ドワンゴからの挑戦状 予選', + 'dwacon2017-prelims': '第3回 ドワンゴからの挑戦状 予選', + 'mujin-pc-2016': 'Mujin Programming Challenge 2016', + 'mujin-pc-2018': 'Mujin Programming Challenge 2018', + 'bitflyer2018-qual': 'codeFlyer (bitFlyer Programming Contest)', + soundhound2018: 'SoundHound Inc. Programming Contest 2018 (春)', + 'pakencamp-2018-day3': 'パ研合宿コンペティション 3日目', + 'pakencamp-2024-day1': 'パ研合宿2024 第1日「SpeedRun」', + 'tenka1-2012-qualB': '天下一プログラマーコンテスト2012予選B', + 'tenka1-2015-quala': '天下一プログラマーコンテスト2015予選A', + 'tenka1-2015-qualb': '天下一プログラマーコンテスト2015予選B', + 'tenka1-2016-final': '天下一プログラマーコンテスト2016本戦', + // Discovery Channel contest featuring algorithm problems + discovery2016: 'DISCO presents ディスカバリーチャンネル プログラミングコンテスト2016', + colopl: 'COLOCON', + gigacode: 'GigaCode', + cpsco2019: 'CPSCO 2019', + 'iroha2019-day4': 'いろはちゃんコンテスト Day4', + 'nikkei2019-final': '全国統一プログラミング王決定戦本戦', + 'jsc2019-final': '第一回日本最強プログラマー学生選手権決勝', + 'jsc2025-final': '第六回日本最強プログラマー学生選手権 -決勝-', + DEGwer2023: 'DEGwer さんの D 論応援コンテスト', + xmascon19: 'Xmas Contest 2019', +} as const; +const atCoderOthersPrefixes = getContestPrefixes(ATCODER_OTHERS); + +// AIZU ONLINE JUDGE AOJ Courses +export const AOJ_COURSES: ContestPrefix = { + ITP1: 'プログラミング入門', + ALDS1: 'アルゴリズムとデータ構造入門', + ITP2: 'プログラミング応用', + DPL: '組み合わせ最適化', + GRL: 'グラフ', + DSL: 'データ構造', + CGL: '計算幾何学', + NTL: '整数論', +} as const; + +export function getPrefixForAojCourses() { + return getContestPrefixes(AOJ_COURSES); +} + +const aojCoursePrefixes = new Set(getPrefixForAojCourses()); // For O(1) lookups + +/** + * Extracts contest prefixes (keys) from a contest prefix object. + * @param contestPrefixes - Object mapping contest IDs to their display names + * @returns Array of contest prefix strings + */ +export function getContestPrefixes(contestPrefixes: Record) { + return Object.keys(contestPrefixes); +} + +/** + * Contest type priorities (0 = Highest, 26 = 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-21): UNIVERSITY, FPS_24, ATCODER_MAIN_OFFICIAL_ONSITE, OTHERS + * - External platforms (22-26): 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 + * and may be adjusted based on future requirements. + * + * See: + * https://jsprimer.net/basic/map-and-set/ + */ +export const contestTypePriorities: Map = new Map([ + [ContestType.ABS, 0], + [ContestType.ABC, 1], + [ContestType.APG4B, 2], + [ContestType.TYPICAL90, 3], + [ContestType.EDPC, 4], + [ContestType.TDPC, 5], + [ContestType.NDPC, 6], + [ContestType.PAST, 7], + [ContestType.ACL_PRACTICE, 8], + [ContestType.JOI, 9], + [ContestType.TESSOKU_BOOK, 10], + [ContestType.MATH_AND_ALGORITHM, 11], + [ContestType.ARC, 12], + [ContestType.AGC, 13], + [ContestType.ABC_LIKE, 14], + [ContestType.ARC_LIKE, 15], + [ContestType.AGC_LIKE, 16], + [ContestType.AWC, 17], + [ContestType.UNIVERSITY, 18], + [ContestType.FPS_24, 19], + [ContestType.ATCODER_MAIN_OFFICIAL_ONSITE, 20], + [ContestType.OTHERS, 21], // AtCoder (その他) + [ContestType.AOJ_COURSES, 22], + [ContestType.AOJ_PCK, 23], + [ContestType.AOJ_ICPC, 24], + [ContestType.AOJ_JAG, 25], + [ContestType.AOJ_UNIVERSITY, 26], +]); + +export function getContestPriority(contestId: string): number { + const contestType = classifyContest(contestId); + const INF: number = 10 ** 3; + + if (contestType === null || contestType === undefined) { + return INF; + } else { + return contestTypePriorities.get(contestType) as number; + } +} + +/** + * Regular expression to match contest codes. + * + * This regex matches strings that start with one of the following prefixes: + * - "abc" + * - "arc" + * - "agc" + * - "awc" + * + * followed by exactly three or four digits. The matching is case-insensitive. + * + * Example matches: + * - "abc376" + * - "ARC128" + * - "agc045" + * - "atc001" + * - "awc0001" + * + * Example non-matches: + * - "xyz123" + * - "abc12" + * - "abc1234" + * - "atc1234" + * - "awc12345" + */ +const regexForAxc = /^(abc|arc|agc|atc)(\d{3})$/i; +const regexForAwc = /^(awc)(\d{4})$/i; + +/** + * Regular expression to match AtCoder University contest identifiers. + * + * The pattern matches strings that: + * - Start with either "ku", "qu", "ut", "tt","tu", or "wu" + * - Followed by "pc" + * - End with exactly year (four digits) + * + * Example matches: + * - "kupc2024" + * - "qupc2018" + * - "utpc2014" + * - "ttpc2022" + * - "tupc2023" + * - "wupc2019" + */ +const regexForAtCoderUniversity = /^(ku|qu|ut|tt|tu|wu)(pc)(\d{4})$/i; + +export const getContestNameLabel = (contestId: string) => { + // AtCoder + if (regexForAxc.exec(contestId)) { + return contestId.replace( + regexForAxc, + (_, contestType, contestNumber) => `${contestType.toUpperCase()} ${contestNumber}`, + ); + } + + if (regexForAwc.exec(contestId)) { + return contestId.replace( + regexForAwc, + (_, contestType, contestNumber) => `${contestType.toUpperCase()} ${contestNumber}`, + ); + } + + if (contestId === 'APG4b' || contestId === 'APG4bPython') { + return contestId; + } + + if (contestId === 'typical90') { + return '競プロ典型 90 問'; + } + + if (contestId === 'dp') { + return 'EDPC'; + } + + if (contestId === 'tdpc') { + return 'TDPC'; + } + + if (contestId === 'ndpc') { + return 'NDPC'; + } + + 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 '競技プログラミングの鉄則'; + } + + if (contestId === 'math-and-algorithm') { + return 'アルゴリズムと数学'; + } + + if (contestId === 'fps-24') { + return 'FPS 24 題'; + } + + if (isWorldTourFinals(contestId)) { + return getWorldTourFinalsLabel(contestId); + } + + if (regexForAtCoderUniversity.exec(contestId)) { + return getAtCoderUniversityContestLabel(contestId); + } + + const othersLabel = ATCODER_OTHERS[contestId as keyof typeof ATCODER_OTHERS]; + + if (othersLabel) { + return othersLabel; + } + + if (contestId.startsWith('chokudai_S')) { + return contestId.replace('chokudai_S', 'Chokudai SpeedRun '); + } + + // AIZU ONLINE JUDGE + if (aojCoursePrefixes.has(contestId)) { + return getAojContestLabel(AOJ_COURSES, contestId); + } + + if (contestId.startsWith('PCK')) { + return getAojContestLabel(PCK_TRANSLATIONS, contestId); + } + + if (contestId.startsWith('ICPC')) { + return getAojContestLabel(ICPC_TRANSLATIONS, contestId); + } + + if (regexForJag.exec(contestId)) { + return getAojContestLabel(JAG_TRANSLATIONS, contestId); + } + + if (regexForAojUniversity.test(contestId)) { + return getAojUniversityContestLabel(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, + 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) + * - "joi2026sf" (matches) + */ +const regexForJoi = /^(joi)(g|open)*(\d{4})*(yo|ho|sc|sp|sf)*(\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 === 'sf') { + 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. + * + * This function takes a contest ID string and replaces parts of it using a regular expression + * to generate a formatted label. The label is constructed by converting the contest type and + * common part to uppercase and appending the contest year. + * + * @param contestId - The ID of the contest to format (ex: utpc2023). + * @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) => + `${(contestType + common).toUpperCase()} ${contestYear}`, + ); +} + +/** + * Maps PCK contest type abbreviations to their Japanese translations. + * + * @example + * { + * PCK: 'パソコン甲子園', + * Prelim: '予選', + * Final: '本選' + * } + */ +const PCK_TRANSLATIONS = { + PCK: 'パソコン甲子園', + Prelim: ' 予選 ', + 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. + * + * @example + * { + * Prelim: '模擬国内', + * Regional: '模擬地区' + * } + */ +const JAG_TRANSLATIONS = { + Prelim: ' 模擬国内 ', + Regional: ' 模擬地区 ', + Summer: ' 夏合宿 ', + Winter: ' 冬合宿 ', + Spring: ' 春合宿 ', + '-day': ' Day', +}; + +const ICPC_TRANSLATIONS = { + Prelim: ' 国内予選 ', + Regional: ' アジア地区 ', +}; + +export function getAojContestLabel( + translations: Readonly, + contestId: string, +): string { + let label = contestId; + + Object.entries(translations).forEach(([abbrEnglish, japanese]) => { + label = label.replace(abbrEnglish, japanese); + }); + + return '(' + label + ')'; +} + +export const addContestNameToTaskIndex = (contestId: string, taskTableIndex: string): string => { + const contestName = getContestNameLabel(contestId); + + if (isAojContest(contestId)) { + return `AOJ ${taskTableIndex}${contestName}`; + } + + return `${contestName} - ${taskTableIndex}`; +}; + +function isAojContest(contestId: string): boolean { + return ( + aojCoursePrefixes.has(contestId) || + contestId.startsWith('PCK') || + regexForJag.test(contestId) || + contestId.startsWith('ICPC') || + regexForAojUniversity.test(contestId) + ); +} diff --git a/src/lib/utils/contest.ts b/src/lib/utils/contest.ts index 3da6df5a2..52c8df748 100644 --- a/src/lib/utils/contest.ts +++ b/src/lib/utils/contest.ts @@ -1,826 +1,19 @@ -import { ContestType, type ContestPrefix, type ContestLabelTranslations } from '$lib/types/contest'; - -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 -export const classifyContest = (contest_id: string) => { - // AtCoder - if (/^abc\d{3}$/.exec(contest_id)) { - return ContestType.ABC; - } - - if (/^arc\d{3}$/.exec(contest_id)) { - return ContestType.ARC; - } - - if (/^agc\d{3}$/.exec(contest_id)) { - return ContestType.AGC; - } - - if (/^awc\d{4}$/.exec(contest_id)) { - return ContestType.AWC; - } - - if (contest_id.startsWith('APG4b')) { - return ContestType.APG4B; - } - - if (contest_id === 'abs') { - return ContestType.ABS; - } - - if (contest_id === 'typical90') { - return ContestType.TYPICAL90; - } - - if (contest_id === 'dp') { - return ContestType.EDPC; - } - - if (contest_id === 'tdpc') { - return ContestType.TDPC; - } - - if (contest_id === 'ndpc') { - return ContestType.NDPC; - } - - if (contest_id.startsWith('past')) { - return ContestType.PAST; - } - - if (contest_id === 'practice2') { - return ContestType.ACL_PRACTICE; - } - - if (contest_id.startsWith('joi')) { - return ContestType.JOI; - } - - if (contest_id === 'tessoku-book') { - return ContestType.TESSOKU_BOOK; - } - - if (contest_id === 'math-and-algorithm') { - return ContestType.MATH_AND_ALGORITHM; - } - - if (contest_id === 'fps-24') { - return ContestType.FPS_24; - } - - if (isWorldTourFinals(contest_id)) { - return ContestType.ATCODER_MAIN_OFFICIAL_ONSITE; - } - - if (abcLikePrefixes.has(contest_id)) { - return ContestType.ABC_LIKE; - } - - if (arcLikePrefixes.has(contest_id)) { - return ContestType.ARC_LIKE; - } - - if (agcLikePrefixes.some((prefix) => contest_id.startsWith(prefix))) { - return ContestType.AGC_LIKE; - } - - if (atCoderUniversityPrefixes.some((prefix) => contest_id.startsWith(prefix))) { - return ContestType.UNIVERSITY; - } - - if (atCoderOthersPrefixes.some((prefix) => contest_id.startsWith(prefix))) { - return ContestType.OTHERS; - } - - // AIZU ONLINE JUDGE - if (aojCoursePrefixes.has(contest_id)) { - return ContestType.AOJ_COURSES; - } - - if (/^PCK(Prelim|Final)\d*$/.exec(contest_id)) { - return ContestType.AOJ_PCK; - } - - if (/^ICPC(Prelim|Regional)\d*$/.exec(contest_id)) { - return ContestType.AOJ_ICPC; - } - - if (regexForJag.exec(contest_id)) { - return ContestType.AOJ_JAG; - } - - if (regexForAojUniversity.test(contest_id)) { - return ContestType.AOJ_UNIVERSITY; - } - - return null; -}; - -// HACK: As of December 2025, the following contests are applicable. -// Note: The classification logic may need to be revised when new contests are added. -const ABC_LIKE: ContestPrefix = { - 'tenka1-2017-beginner': 'Tenka1 Programmer Beginner Contest 2017', - abl: 'ACL Beginner Contest', - caddi2018b: 'CADDi 2018 for Beginners', - 'soundhound2018-summer-qual': 'SoundHound Inc. Programming Contest 2018 -Masters Tournament-', - 'tenka1-2018-beginner': 'Tenka1 Programmer Beginner Contest 2018', - aising2019: 'エイシング プログラミング コンテスト 2019', - sumitrust2019: '三井住友信託銀行プログラミングコンテスト2019', - 'tenka1-2019-beginner': 'Tenka1 Programmer Beginner Contest 2019', - aising2020: 'エイシング プログラミング コンテスト 2020', - hhkb2020: 'HHKB プログラミングコンテスト 2020', - 'm-solutions2020': 'M-SOLUTIONS プロコンオープン 2020', - panasonic2020: 'パナソニックプログラミングコンテスト 2020', - jsc2021: '第二回日本最強プログラマー学生選手権', - zone2021: 'ZONeエナジー プログラミングコンテスト “HELLO SPACE”', - 'jsc2025advance-final': '日本最強プログラマー学生選手権~Advance~', -} as const; -const abcLikePrefixes = new Set(getContestPrefixes(ABC_LIKE)); - -const ARC_LIKE: ContestPrefix = { - 'tenka1-2017': 'Tenka1 Programmer Contest 2017', - 'tenka1-2018': 'Tenka1 Programmer Contest 2018', - 'tenka1-2019': 'Tenka1 Programmer Contest 2019', - caddi2018: 'CADDi 2018', - 'dwacon5th-prelims': '第5回 ドワンゴからの挑戦状 予選', - 'dwacon6th-prelims': '第6回 ドワンゴからの挑戦状 予選', - diverta2019: 'diverta 2019 Programming Contest', - keyence2019: 'キーエンス プログラミング コンテスト 2019', - keyence2020: 'キーエンス プログラミング コンテスト 2020', - keyence2021: 'キーエンス プログラミング コンテスト 2021', - 'jsc2019-qual': '第一回日本最強プログラマー学生選手権-予選-', - 'nikkei2019-qual': '全国統一プログラミング王決定戦予選', - acl1: 'ACL Contest 1', -} as const; -const arcLikePrefixes = new Set(getContestPrefixes(ARC_LIKE)); - -const AGC_LIKE: ContestPrefix = { - 'code-festival-2016-qual': 'CODE FESTIVAL 2016 qual', - 'code-festival-2017-qual': 'CODE FESTIVAL 2017 qual', - 'cf16-final': 'CODE FESTIVAL 2016 final', - 'cf17-final': 'CODE FESTIVAL 2017 final', -} as const; -const agcLikePrefixes = getContestPrefixes(AGC_LIKE); - -// HACK: As of September 2025, KUPC, QUPC, UTPC, TTPC and TUPC are included. -// More university contests may be added in the future. -/** - * Maps university contest ID prefixes to their display names. - * - * @example - * { - * kupc: 'KUPC' // Kyoto University Programming Contest - * qupc: 'QUPC' // Kyushu University Programming Contest - * utpc: 'UTPC' // University of Tokyo Programming Contest - * ttpc: 'TTPC' // Tokyo Institute of Technology Programming Contest - * tupc: 'TUPC' // Tohoku University Programming Contest - * wupc: 'WUPC' // Waseda University Programming Contest - * } - * - * @remarks - * When adding new university contests: - * 1. Use lowercase prefix as key - * 2. Use official contest name as value - * 3. Ensure prefix doesn't conflict with existing contest types - */ -const ATCODER_UNIVERSITIES: ContestPrefix = { - kupc: 'KUPC', - qupc: 'QUPC', - utpc: 'UTPC', - ttpc: 'TTPC', - tupc: 'TUPC', - wupc: 'WUPC', -} as const; - -const atCoderUniversityPrefixes = getContestPrefixes(ATCODER_UNIVERSITIES); - -// World Tour Finals (AtCoder official onsite finals), Algorithm division only. -// -// Seeded contest_id values carry a trailing "-open" (e.g. wtf19-open), except -// wtf22-day2 which AtCoder Problems records without it; strip it before matching -// so both forms work and it never leaks into the display label. -// -// From 2025 the id gains an "algo" infix (awtf2025algo-open) to disambiguate -// from the new Heuristic division (awtf2025heuristic), which stays out of scope. -const regexForWorldTourFinals = /^(wtf19|wtf22-day[12]|awtf2024|awtf20\d{2}algo)$/; - -export const isWorldTourFinals = (contestId: string): boolean => - regexForWorldTourFinals.test(stripOpenSuffix(contestId)); - -export const getWorldTourFinalsLabel = (contestId: string): string => { - const base = 'World Tour Finals'; - const id = stripOpenSuffix(contestId); - - if (id === 'wtf19') { - return `${base} 2019`; - } - - const dayMatch = /^wtf22-day([12])$/.exec(id); - - if (dayMatch) { - return `${base} 2022 Day${dayMatch[1]}`; - } - - if (id === 'awtf2024') { - return `${base} 2024`; - } - - const algoMatch = /^awtf(20\d{2})algo$/.exec(id); - - if (algoMatch) { - // "Algorithm" distinguishes from the Heuristic division, introduced in 2025. - return `${base} ${algoMatch[1]} Algorithm`; - } - - return contestId.toUpperCase(); -}; - -const stripOpenSuffix = (contestId: string): string => - contestId.endsWith('-open') ? contestId.slice(0, -'-open'.length) : contestId; - -/** - * Maps other AtCoder contest ID prefixes to their display names. - * Includes special, corporate, and promotional contests that don't fit other categories. - * - * @example - * { - * 'mujin-pc-2018': 'Mujin Programming Challenge 2018', - * 'discovery2016': 'DISCO presents ディスカバリーチャンネル プログラミングコンテスト2016' - * } - * - * @remarks - * When adding new contests: - * 1. Use kebab-case for contest ID prefix as key - * 2. Use official contest name in English or Japanese as value - * 3. Ensure the contest doesn't belong to other specific categories - */ -const ATCODER_OTHERS: ContestPrefix = { - chokudai_S: 'Chokudai SpeedRun', - atc001: 'AtCoder Typical Contest 001', - geocon2013: '幾何コンテスト2013', - 's8pc-3': 'square869120Contest #3', - 's8pc-4': 'square869120Contest #4', - 'maximum-cup-2013': 'Maximum-Cup 2013', - 'maximum-cup-2018': 'Maximum-Cup 2018', - 'code-festival-2014-quala': 'Code Festival 2014 予選 A', - 'code-festival-2014-qualb': 'Code Festival 2014 予選 B', - 'code-festival-2014-final': 'Code Festival 2014 決勝', - 'code-festival-2014-china-open': 'Code Festival 2014 上海', - 'code-festival-2015-qualb': 'Code Festival 2015 予選 B', - 'code-festival-2015-morning-middle': 'CODE FESTIVAL 2015 あさぷろ Middle', - 'code-festival-2015-exhibition': 'CODE FESTIVAL 2015 エキシビション', - 'code-thanks-festival': 'CODE THANKS FESTIVAL', - donuts: 'Donutsプロコンチャレンジ', - indeednow: 'Indeedなう', - 'tkppc4-2': '技術室奥プログラミングコンテスト#4 Day2', - 'dwango2016-prelims': '第2回 ドワンゴからの挑戦状 予選', - 'dwacon2017-prelims': '第3回 ドワンゴからの挑戦状 予選', - 'mujin-pc-2016': 'Mujin Programming Challenge 2016', - 'mujin-pc-2018': 'Mujin Programming Challenge 2018', - 'bitflyer2018-qual': 'codeFlyer (bitFlyer Programming Contest)', - soundhound2018: 'SoundHound Inc. Programming Contest 2018 (春)', - 'pakencamp-2018-day3': 'パ研合宿コンペティション 3日目', - 'pakencamp-2024-day1': 'パ研合宿2024 第1日「SpeedRun」', - 'tenka1-2012-qualB': '天下一プログラマーコンテスト2012予選B', - 'tenka1-2015-quala': '天下一プログラマーコンテスト2015予選A', - 'tenka1-2015-qualb': '天下一プログラマーコンテスト2015予選B', - 'tenka1-2016-final': '天下一プログラマーコンテスト2016本戦', - // Discovery Channel contest featuring algorithm problems - discovery2016: 'DISCO presents ディスカバリーチャンネル プログラミングコンテスト2016', - colopl: 'COLOCON', - gigacode: 'GigaCode', - cpsco2019: 'CPSCO 2019', - 'iroha2019-day4': 'いろはちゃんコンテスト Day4', - 'nikkei2019-final': '全国統一プログラミング王決定戦本戦', - 'jsc2019-final': '第一回日本最強プログラマー学生選手権決勝', - 'jsc2025-final': '第六回日本最強プログラマー学生選手権 -決勝-', - DEGwer2023: 'DEGwer さんの D 論応援コンテスト', - xmascon19: 'Xmas Contest 2019', -} as const; -const atCoderOthersPrefixes = getContestPrefixes(ATCODER_OTHERS); - -// AIZU ONLINE JUDGE AOJ Courses -export const AOJ_COURSES: ContestPrefix = { - ITP1: 'プログラミング入門', - ALDS1: 'アルゴリズムとデータ構造入門', - ITP2: 'プログラミング応用', - DPL: '組み合わせ最適化', - GRL: 'グラフ', - DSL: 'データ構造', - CGL: '計算幾何学', - NTL: '整数論', -} as const; - -export function getPrefixForAojCourses() { - return getContestPrefixes(AOJ_COURSES); -} - -const aojCoursePrefixes = new Set(getPrefixForAojCourses()); // For O(1) lookups - -/** - * Extracts contest prefixes (keys) from a contest prefix object. - * @param contestPrefixes - Object mapping contest IDs to their display names - * @returns Array of contest prefix strings - */ -export function getContestPrefixes(contestPrefixes: Record) { - return Object.keys(contestPrefixes); -} - -/** - * Contest type priorities (0 = Highest, 26 = 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-21): UNIVERSITY, FPS_24, ATCODER_MAIN_OFFICIAL_ONSITE, OTHERS - * - External platforms (22-26): 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 - * and may be adjusted based on future requirements. - * - * See: - * https://jsprimer.net/basic/map-and-set/ - */ -export const contestTypePriorities: Map = new Map([ - [ContestType.ABS, 0], - [ContestType.ABC, 1], - [ContestType.APG4B, 2], - [ContestType.TYPICAL90, 3], - [ContestType.EDPC, 4], - [ContestType.TDPC, 5], - [ContestType.NDPC, 6], - [ContestType.PAST, 7], - [ContestType.ACL_PRACTICE, 8], - [ContestType.JOI, 9], - [ContestType.TESSOKU_BOOK, 10], - [ContestType.MATH_AND_ALGORITHM, 11], - [ContestType.ARC, 12], - [ContestType.AGC, 13], - [ContestType.ABC_LIKE, 14], - [ContestType.ARC_LIKE, 15], - [ContestType.AGC_LIKE, 16], - [ContestType.AWC, 17], - [ContestType.UNIVERSITY, 18], - [ContestType.FPS_24, 19], - [ContestType.ATCODER_MAIN_OFFICIAL_ONSITE, 20], - [ContestType.OTHERS, 21], // AtCoder (その他) - [ContestType.AOJ_COURSES, 22], - [ContestType.AOJ_PCK, 23], - [ContestType.AOJ_ICPC, 24], - [ContestType.AOJ_JAG, 25], - [ContestType.AOJ_UNIVERSITY, 26], -]); - -export function getContestPriority(contestId: string): number { - const contestType = classifyContest(contestId); - const INF: number = 10 ** 3; - - if (contestType === null || contestType === undefined) { - return INF; - } else { - return contestTypePriorities.get(contestType) as number; - } -} - -/** - * Regular expression to match contest codes. - * - * This regex matches strings that start with one of the following prefixes: - * - "abc" - * - "arc" - * - "agc" - * - "awc" - * - * followed by exactly three or four digits. The matching is case-insensitive. - * - * Example matches: - * - "abc376" - * - "ARC128" - * - "agc045" - * - "atc001" - * - "awc0001" - * - * Example non-matches: - * - "xyz123" - * - "abc12" - * - "abc1234" - * - "atc1234" - * - "awc12345" - */ -const regexForAxc = /^(abc|arc|agc|atc)(\d{3})$/i; -const regexForAwc = /^(awc)(\d{4})$/i; - -/** - * Regular expression to match AtCoder University contest identifiers. - * - * The pattern matches strings that: - * - Start with either "ku", "qu", "ut", "tt","tu", or "wu" - * - Followed by "pc" - * - End with exactly year (four digits) - * - * Example matches: - * - "kupc2024" - * - "qupc2018" - * - "utpc2014" - * - "ttpc2022" - * - "tupc2023" - * - "wupc2019" - */ -const regexForAtCoderUniversity = /^(ku|qu|ut|tt|tu|wu)(pc)(\d{4})$/i; - -export const getContestNameLabel = (contestId: string) => { - // AtCoder - if (regexForAxc.exec(contestId)) { - return contestId.replace( - regexForAxc, - (_, contestType, contestNumber) => `${contestType.toUpperCase()} ${contestNumber}`, - ); - } - - if (regexForAwc.exec(contestId)) { - return contestId.replace( - regexForAwc, - (_, contestType, contestNumber) => `${contestType.toUpperCase()} ${contestNumber}`, - ); - } - - if (contestId === 'APG4b' || contestId === 'APG4bPython') { - return contestId; - } - - if (contestId === 'typical90') { - return '競プロ典型 90 問'; - } - - if (contestId === 'dp') { - return 'EDPC'; - } - - if (contestId === 'tdpc') { - return 'TDPC'; - } - - if (contestId === 'ndpc') { - return 'NDPC'; - } - - 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 '競技プログラミングの鉄則'; - } - - if (contestId === 'math-and-algorithm') { - return 'アルゴリズムと数学'; - } - - if (contestId === 'fps-24') { - return 'FPS 24 題'; - } - - if (isWorldTourFinals(contestId)) { - return getWorldTourFinalsLabel(contestId); - } - - if (regexForAtCoderUniversity.exec(contestId)) { - return getAtCoderUniversityContestLabel(contestId); - } - - const othersLabel = ATCODER_OTHERS[contestId as keyof typeof ATCODER_OTHERS]; - - if (othersLabel) { - return othersLabel; - } - - if (contestId.startsWith('chokudai_S')) { - return contestId.replace('chokudai_S', 'Chokudai SpeedRun '); - } - - // AIZU ONLINE JUDGE - if (aojCoursePrefixes.has(contestId)) { - return getAojContestLabel(AOJ_COURSES, contestId); - } - - if (contestId.startsWith('PCK')) { - return getAojContestLabel(PCK_TRANSLATIONS, contestId); - } - - if (contestId.startsWith('ICPC')) { - return getAojContestLabel(ICPC_TRANSLATIONS, contestId); - } - - if (regexForJag.exec(contestId)) { - return getAojContestLabel(JAG_TRANSLATIONS, contestId); - } - - if (regexForAojUniversity.test(contestId)) { - return getAojUniversityContestLabel(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, - 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) - * - "joi2026sf" (matches) - */ -const regexForJoi = /^(joi)(g|open)*(\d{4})*(yo|ho|sc|sp|sf)*(\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 === 'sf') { - 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. - * - * This function takes a contest ID string and replaces parts of it using a regular expression - * to generate a formatted label. The label is constructed by converting the contest type and - * common part to uppercase and appending the contest year. - * - * @param contestId - The ID of the contest to format (ex: utpc2023). - * @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) => - `${(contestType + common).toUpperCase()} ${contestYear}`, - ); -} - -/** - * Maps PCK contest type abbreviations to their Japanese translations. - * - * @example - * { - * PCK: 'パソコン甲子園', - * Prelim: '予選', - * Final: '本選' - * } - */ -const PCK_TRANSLATIONS = { - PCK: 'パソコン甲子園', - Prelim: ' 予選 ', - 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. - * - * @example - * { - * Prelim: '模擬国内', - * Regional: '模擬地区' - * } - */ -const JAG_TRANSLATIONS = { - Prelim: ' 模擬国内 ', - Regional: ' 模擬地区 ', - Summer: ' 夏合宿 ', - Winter: ' 冬合宿 ', - Spring: ' 春合宿 ', - '-day': ' Day', -}; - -const ICPC_TRANSLATIONS = { - Prelim: ' 国内予選 ', - Regional: ' アジア地区 ', -}; - -export function getAojContestLabel( - translations: Readonly, - contestId: string, -): string { - let label = contestId; - - Object.entries(translations).forEach(([abbrEnglish, japanese]) => { - label = label.replace(abbrEnglish, japanese); - }); - - return '(' + label + ')'; -} - -export const addContestNameToTaskIndex = (contestId: string, taskTableIndex: string): string => { - const contestName = getContestNameLabel(contestId); - - if (isAojContest(contestId)) { - return `AOJ ${taskTableIndex}${contestName}`; - } - - return `${contestName} - ${taskTableIndex}`; -}; - -function isAojContest(contestId: string): boolean { - return ( - aojCoursePrefixes.has(contestId) || - contestId.startsWith('PCK') || - regexForJag.test(contestId) || - contestId.startsWith('ICPC') || - regexForAojUniversity.test(contestId) - ); -} +export { + classifyContest, + getContestNameLabel, + getContestPriority, + contestTypePriorities, + addContestNameToTaskIndex, + isWorldTourFinals, + getWorldTourFinalsLabel, + getPastContestLabel, + getJoiContestLabel, + getAtCoderUniversityContestLabel, + getAojContestLabel, + AOJ_COURSES, + getPrefixForAojCourses, + getContestPrefixes, + regexForJag, + regexForAojUniversity, + PAST_TRANSLATIONS, +} from '$lib/contests/utils/contest'; From aa9c735d611deb1b2f804e1509ee150465ef3cc2 Mon Sep 17 00:00:00 2001 From: "k.hiro1818" Date: Sun, 6 Sep 2026 05:06:59 +0000 Subject: [PATCH 3/5] refactor: co-locate contest tests and rename test_cases to fixtures Co-Authored-By: Claude Opus 4.6 --- .../fixtures}/contest_name_and_task_index.ts | 4 ++-- .../contests/fixtures}/contest_name_labels.ts | 2 +- .../contests/fixtures}/contest_type.ts | 4 ++-- .../lib => lib/contests}/utils/contest.test.ts | 18 +++++++++--------- 4 files changed, 14 insertions(+), 14 deletions(-) rename src/{test/lib/utils/test_cases => lib/contests/fixtures}/contest_name_and_task_index.ts (99%) rename src/{test/lib/utils/test_cases => lib/contests/fixtures}/contest_name_labels.ts (99%) rename src/{test/lib/utils/test_cases => lib/contests/fixtures}/contest_type.ts (99%) rename src/{test/lib => lib/contests}/utils/contest.test.ts (97%) diff --git a/src/test/lib/utils/test_cases/contest_name_and_task_index.ts b/src/lib/contests/fixtures/contest_name_and_task_index.ts similarity index 99% rename from src/test/lib/utils/test_cases/contest_name_and_task_index.ts rename to src/lib/contests/fixtures/contest_name_and_task_index.ts index df459adc5..23d75007e 100644 --- a/src/test/lib/utils/test_cases/contest_name_and_task_index.ts +++ b/src/lib/contests/fixtures/contest_name_and_task_index.ts @@ -1,4 +1,4 @@ -import { createTestCase, zip } from '../../common/test_helpers'; +import { createTestCase, zip } from '../../../test/lib/common/test_helpers'; import { getPastContestLabel, getJoiContestLabel, @@ -6,7 +6,7 @@ import { getAojContestLabel, PAST_TRANSLATIONS, AOJ_COURSES, -} from '$lib/utils/contest'; +} from '$lib/contests/utils/contest'; export type TestCaseForContestNameAndTaskIndex = { contestId: string; diff --git a/src/test/lib/utils/test_cases/contest_name_labels.ts b/src/lib/contests/fixtures/contest_name_labels.ts similarity index 99% rename from src/test/lib/utils/test_cases/contest_name_labels.ts rename to src/lib/contests/fixtures/contest_name_labels.ts index 766b93ef6..fc7dad54f 100644 --- a/src/test/lib/utils/test_cases/contest_name_labels.ts +++ b/src/lib/contests/fixtures/contest_name_labels.ts @@ -1,4 +1,4 @@ -import { createTestCase } from '../../common/test_helpers'; +import { createTestCase } from '../../../test/lib/common/test_helpers'; export type TestCaseForContestNameLabel = { contestId: string; diff --git a/src/test/lib/utils/test_cases/contest_type.ts b/src/lib/contests/fixtures/contest_type.ts similarity index 99% rename from src/test/lib/utils/test_cases/contest_type.ts rename to src/lib/contests/fixtures/contest_type.ts index cb5118fd8..beb144430 100644 --- a/src/test/lib/utils/test_cases/contest_type.ts +++ b/src/lib/contests/fixtures/contest_type.ts @@ -1,5 +1,5 @@ -import { createTestCase } from '../../common/test_helpers'; -import { ContestType } from '$lib/types/contest'; +import { createTestCase } from '../../../test/lib/common/test_helpers'; +import { ContestType } from '$lib/contests/types/contest'; export type TestCaseForContestType = { contestId: string; diff --git a/src/test/lib/utils/contest.test.ts b/src/lib/contests/utils/contest.test.ts similarity index 97% rename from src/test/lib/utils/contest.test.ts rename to src/lib/contests/utils/contest.test.ts index f22d1e5a2..eb1600212 100644 --- a/src/test/lib/utils/contest.test.ts +++ b/src/lib/contests/utils/contest.test.ts @@ -1,13 +1,13 @@ import { expect } from 'vitest'; -import { ContestType } from '$lib/types/contest'; -import { runTests } from '../common/test_helpers'; -import * as TestCasesForContestType from './test_cases/contest_type'; -import { type TestCaseForContestType } from './test_cases/contest_type'; -import * as TestCasesForContestNameLabel from './test_cases/contest_name_labels'; -import { type TestCaseForContestNameLabel } from './test_cases/contest_name_labels'; -import * as TestCasesForContestNameAndTaskIndex from './test_cases/contest_name_and_task_index'; -import { type TestCaseForContestNameAndTaskIndex } from './test_cases/contest_name_and_task_index'; +import { ContestType } from '$lib/contests/types/contest'; +import { runTests } from '../../../test/lib/common/test_helpers'; +import * as TestCasesForContestType from '$lib/contests/fixtures/contest_type'; +import { type TestCaseForContestType } from '$lib/contests/fixtures/contest_type'; +import * as TestCasesForContestNameLabel from '$lib/contests/fixtures/contest_name_labels'; +import { type TestCaseForContestNameLabel } from '$lib/contests/fixtures/contest_name_labels'; +import * as TestCasesForContestNameAndTaskIndex from '$lib/contests/fixtures/contest_name_and_task_index'; +import { type TestCaseForContestNameAndTaskIndex } from '$lib/contests/fixtures/contest_name_and_task_index'; import { classifyContest, getContestPriority, @@ -15,7 +15,7 @@ import { getContestNameLabel, addContestNameToTaskIndex, getAtCoderUniversityContestLabel, -} from '$lib/utils/contest'; +} from '$lib/contests/utils/contest'; describe('Contest', () => { describe('classify contest', () => { From 90973dd54bcd5aaa3052b57b5911a198db067b65 Mon Sep 17 00:00:00 2001 From: "k.hiro1818" Date: Sun, 6 Sep 2026 05:17:57 +0000 Subject: [PATCH 4/5] refactor: remove contest type/util re-export shims and update imports Delete src/lib/types/contest.ts and src/lib/utils/contest.ts re-export files. Update all 70 importing files to reference $lib/contests/ directly. Co-Authored-By: Claude Opus 4.6 Claude-Session: https://claude.ai/code/session_01LpBu3jvGWLNEeyWVL2PQZT --- prisma/seed.ts | 2 +- .../contest-table/contest_table_provider.ts | 2 +- .../utils/contest-table/abc_providers.test.ts | 2 +- .../utils/contest-table/abc_providers.ts | 2 +- .../utils/contest-table/abs_provider.test.ts | 2 +- .../tasks/utils/contest-table/abs_provider.ts | 2 +- .../utils/contest-table/acl_providers.test.ts | 2 +- .../utils/contest-table/acl_providers.ts | 2 +- .../utils/contest-table/agc_provider.test.ts | 2 +- .../tasks/utils/contest-table/agc_provider.ts | 2 +- .../contest-table/aoj_icpc_providers.test.ts | 2 +- .../utils/contest-table/aoj_icpc_providers.ts | 2 +- .../contest-table/aoj_jag_providers.test.ts | 2 +- .../utils/contest-table/aoj_jag_providers.ts | 2 +- .../utils/contest-table/arc_providers.test.ts | 2 +- .../utils/contest-table/arc_providers.ts | 2 +- .../utils/contest-table/awc_provider.test.ts | 2 +- .../tasks/utils/contest-table/awc_provider.ts | 4 ++-- .../contest-table/axc_like_provider.test.ts | 2 +- .../utils/contest-table/axc_like_provider.ts | 2 +- .../contest_table_provider_base.test.ts | 2 +- .../contest_table_provider_base.ts | 2 +- .../contest_table_provider_group.test.ts | 2 +- .../contest_table_provider_group.ts | 2 +- .../contest_table_provider_groups.test.ts | 2 +- .../contest_table_provider_groups.ts | 2 +- .../utils/contest-table/dp_providers.test.ts | 2 +- .../tasks/utils/contest-table/dp_providers.ts | 2 +- .../contest-table/fps24_provider.test.ts | 2 +- .../utils/contest-table/fps24_provider.ts | 2 +- .../utils/contest-table/joi_providers.test.ts | 4 ++-- .../utils/contest-table/joi_providers.ts | 4 ++-- .../math_and_algorithm_provider.test.ts | 2 +- .../math_and_algorithm_provider.ts | 2 +- .../tessoku_book_providers.test.ts | 2 +- .../contest-table/tessoku_book_providers.ts | 4 ++-- .../contest-table/typical90_provider.test.ts | 2 +- .../utils/contest-table/typical90_provider.ts | 2 +- .../detail/WorkBookTasksTable.svelte | 2 +- .../clients/aizu_online_judge/clients.test.ts | 2 +- src/lib/clients/aizu_online_judge/clients.ts | 2 +- src/lib/clients/aizu_online_judge/utils.ts | 2 +- .../clients/atcoder/atcoder_problems.test.ts | 2 +- src/lib/clients/atcoder/atcoder_problems.ts | 2 +- src/lib/clients/cache_strategy.ts | 2 +- src/lib/clients/contest_task_fetcher.ts | 2 +- src/lib/clients/http_client.ts | 2 +- src/lib/clients/index.ts | 2 +- .../SubmissionStatus/UpdatingModal.svelte | 2 +- src/lib/components/TagForm.svelte | 2 +- src/lib/components/TaskForm.svelte | 2 +- src/lib/components/TaskList.svelte | 2 +- src/lib/components/TaskListSorted.svelte | 2 +- src/lib/contests/fixtures/contest_type.ts | 2 +- src/lib/services/tasks.ts | 4 ++-- src/lib/types/contest.ts | 9 --------- src/lib/types/task.ts | 2 +- src/lib/utils/contest.ts | 19 ------------------- src/lib/utils/task.ts | 6 +++--- src/lib/utils/task_filter.ts | 2 +- src/routes/(admin)/tasks/+page.server.ts | 4 ++-- src/routes/(admin)/tasks/+page.svelte | 2 +- .../_components/TaskTableForImport.svelte | 4 ++-- .../(admin)/tasks/_utils/contests.test.ts | 2 +- src/routes/(admin)/tasks/_utils/contests.ts | 2 +- .../grade/_components/TaskGradeTable.svelte | 2 +- src/routes/votes/+page.svelte | 2 +- src/routes/workbooks/[slug]/+page.svelte | 2 +- .../lib/services/fixtures/task_results.ts | 2 +- src/test/lib/utils/task.test.ts | 2 +- 70 files changed, 77 insertions(+), 105 deletions(-) delete mode 100644 src/lib/types/contest.ts delete mode 100644 src/lib/utils/contest.ts diff --git a/prisma/seed.ts b/prisma/seed.ts index 9574a68ce..68a498fa3 100755 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -24,7 +24,7 @@ import { hashPassword } from '../src/features/auth/server/password'; import { getTaskGrade } from '../src/lib/types/task'; import type { PlacementCreate } from '../src/features/workbooks/types/workbook_placement'; -import { classifyContest } from '../src/lib/utils/contest'; +import { classifyContest } from '../src/lib/contests/utils/contest'; import { users, USER_PASSWORD_FOR_SEED } from './users'; import { tasks } from './tasks'; diff --git a/src/features/tasks/types/contest-table/contest_table_provider.ts b/src/features/tasks/types/contest-table/contest_table_provider.ts index 54b3ff251..83cc676e7 100644 --- a/src/features/tasks/types/contest-table/contest_table_provider.ts +++ b/src/features/tasks/types/contest-table/contest_table_provider.ts @@ -1,4 +1,4 @@ -import type { ContestType } from '$lib/types/contest'; +import type { ContestType } from '$lib/contests/types/contest'; import type { TaskResults, TaskResult } from '$lib/types/task'; /** diff --git a/src/features/tasks/utils/contest-table/abc_providers.test.ts b/src/features/tasks/utils/contest-table/abc_providers.test.ts index ab0f1594f..651e00df5 100644 --- a/src/features/tasks/utils/contest-table/abc_providers.test.ts +++ b/src/features/tasks/utils/contest-table/abc_providers.test.ts @@ -1,6 +1,6 @@ import { describe, test, expect } from 'vitest'; -import { ContestType } from '$lib/types/contest'; +import { ContestType } from '$lib/contests/types/contest'; import type { TaskResult, TaskResults } from '$lib/types/task'; import { diff --git a/src/features/tasks/utils/contest-table/abc_providers.ts b/src/features/tasks/utils/contest-table/abc_providers.ts index 2dea3fbc6..6dd8d795f 100644 --- a/src/features/tasks/utils/contest-table/abc_providers.ts +++ b/src/features/tasks/utils/contest-table/abc_providers.ts @@ -4,7 +4,7 @@ import { } from '$features/tasks/types/contest-table/contest_table_provider'; import type { TaskResult } from '$lib/types/task'; -import { classifyContest, getContestNameLabel } from '$lib/utils/contest'; +import { classifyContest, getContestNameLabel } from '$lib/contests/utils/contest'; import { ContestTableProviderBase, parseContestRound } from './contest_table_provider_base'; diff --git a/src/features/tasks/utils/contest-table/abs_provider.test.ts b/src/features/tasks/utils/contest-table/abs_provider.test.ts index e86775812..54def4c19 100644 --- a/src/features/tasks/utils/contest-table/abs_provider.test.ts +++ b/src/features/tasks/utils/contest-table/abs_provider.test.ts @@ -1,6 +1,6 @@ import { describe, test, expect } from 'vitest'; -import { ContestType } from '$lib/types/contest'; +import { ContestType } from '$lib/contests/types/contest'; import type { TaskResults } from '$lib/types/task'; import { ABSProvider } from './abs_provider'; diff --git a/src/features/tasks/utils/contest-table/abs_provider.ts b/src/features/tasks/utils/contest-table/abs_provider.ts index cf527e21a..12b9669e4 100644 --- a/src/features/tasks/utils/contest-table/abs_provider.ts +++ b/src/features/tasks/utils/contest-table/abs_provider.ts @@ -4,7 +4,7 @@ import { } from '$features/tasks/types/contest-table/contest_table_provider'; import type { TaskResult } from '$lib/types/task'; -import { classifyContest } from '$lib/utils/contest'; +import { classifyContest } from '$lib/contests/utils/contest'; import { ContestTableProviderBase } from './contest_table_provider_base'; diff --git a/src/features/tasks/utils/contest-table/acl_providers.test.ts b/src/features/tasks/utils/contest-table/acl_providers.test.ts index 6cdc37388..c2c4b4826 100644 --- a/src/features/tasks/utils/contest-table/acl_providers.test.ts +++ b/src/features/tasks/utils/contest-table/acl_providers.test.ts @@ -1,6 +1,6 @@ import { describe, test, expect } from 'vitest'; -import { ContestType } from '$lib/types/contest'; +import { ContestType } from '$lib/contests/types/contest'; import type { TaskResults } from '$lib/types/task'; import { ACLPracticeProvider, ACLBeginnerProvider, ACLProvider } from './acl_providers'; diff --git a/src/features/tasks/utils/contest-table/acl_providers.ts b/src/features/tasks/utils/contest-table/acl_providers.ts index 2235e0f9d..86ae9572f 100644 --- a/src/features/tasks/utils/contest-table/acl_providers.ts +++ b/src/features/tasks/utils/contest-table/acl_providers.ts @@ -4,7 +4,7 @@ import { } from '$features/tasks/types/contest-table/contest_table_provider'; import type { TaskResult } from '$lib/types/task'; -import { classifyContest } from '$lib/utils/contest'; +import { classifyContest } from '$lib/contests/utils/contest'; import { ContestTableProviderBase } from './contest_table_provider_base'; diff --git a/src/features/tasks/utils/contest-table/agc_provider.test.ts b/src/features/tasks/utils/contest-table/agc_provider.test.ts index 83c91d730..c11600c68 100644 --- a/src/features/tasks/utils/contest-table/agc_provider.test.ts +++ b/src/features/tasks/utils/contest-table/agc_provider.test.ts @@ -1,6 +1,6 @@ import { describe, test, expect } from 'vitest'; -import { ContestType } from '$lib/types/contest'; +import { ContestType } from '$lib/contests/types/contest'; import type { TaskResults } from '$lib/types/task'; import { AGC001OnwardsProvider } from './agc_provider'; diff --git a/src/features/tasks/utils/contest-table/agc_provider.ts b/src/features/tasks/utils/contest-table/agc_provider.ts index 93e0e0922..efb7703af 100644 --- a/src/features/tasks/utils/contest-table/agc_provider.ts +++ b/src/features/tasks/utils/contest-table/agc_provider.ts @@ -1,7 +1,7 @@ import { type ContestTableMetaData } from '$features/tasks/types/contest-table/contest_table_provider'; import type { TaskResult } from '$lib/types/task'; -import { classifyContest, getContestNameLabel } from '$lib/utils/contest'; +import { classifyContest, getContestNameLabel } from '$lib/contests/utils/contest'; import { ContestTableProviderBase, parseContestRound } from './contest_table_provider_base'; diff --git a/src/features/tasks/utils/contest-table/aoj_icpc_providers.test.ts b/src/features/tasks/utils/contest-table/aoj_icpc_providers.test.ts index ad709e1a2..046db0b88 100644 --- a/src/features/tasks/utils/contest-table/aoj_icpc_providers.test.ts +++ b/src/features/tasks/utils/contest-table/aoj_icpc_providers.test.ts @@ -1,6 +1,6 @@ import { describe, test, expect, beforeEach, afterEach } from 'vitest'; -import { ContestType } from '$lib/types/contest'; +import { ContestType } from '$lib/contests/types/contest'; import type { TaskResults } from '$lib/types/task'; import { AOJ_LABEL_OVERRIDES } from './aoj_labels'; diff --git a/src/features/tasks/utils/contest-table/aoj_icpc_providers.ts b/src/features/tasks/utils/contest-table/aoj_icpc_providers.ts index acd295bbf..92df296e4 100644 --- a/src/features/tasks/utils/contest-table/aoj_icpc_providers.ts +++ b/src/features/tasks/utils/contest-table/aoj_icpc_providers.ts @@ -1,4 +1,4 @@ -import { ContestType } from '$lib/types/contest'; +import { ContestType } from '$lib/contests/types/contest'; import type { TaskResult, TaskResults } from '$lib/types/task'; import { type ContestTableMetaData, diff --git a/src/features/tasks/utils/contest-table/aoj_jag_providers.test.ts b/src/features/tasks/utils/contest-table/aoj_jag_providers.test.ts index d92f82509..5828d6025 100644 --- a/src/features/tasks/utils/contest-table/aoj_jag_providers.test.ts +++ b/src/features/tasks/utils/contest-table/aoj_jag_providers.test.ts @@ -1,6 +1,6 @@ import { describe, test, expect } from 'vitest'; -import { ContestType } from '$lib/types/contest'; +import { ContestType } from '$lib/contests/types/contest'; import type { TaskResults } from '$lib/types/task'; import { JagPrelimProvider } from './aoj_jag_providers'; diff --git a/src/features/tasks/utils/contest-table/aoj_jag_providers.ts b/src/features/tasks/utils/contest-table/aoj_jag_providers.ts index 55bbfaf9b..e004f7193 100644 --- a/src/features/tasks/utils/contest-table/aoj_jag_providers.ts +++ b/src/features/tasks/utils/contest-table/aoj_jag_providers.ts @@ -1,4 +1,4 @@ -import { ContestType } from '$lib/types/contest'; +import { ContestType } from '$lib/contests/types/contest'; import type { TaskResult, TaskResults } from '$lib/types/task'; import { type ContestTableMetaData, diff --git a/src/features/tasks/utils/contest-table/arc_providers.test.ts b/src/features/tasks/utils/contest-table/arc_providers.test.ts index 4a5ac472f..4acb3938e 100644 --- a/src/features/tasks/utils/contest-table/arc_providers.test.ts +++ b/src/features/tasks/utils/contest-table/arc_providers.test.ts @@ -1,6 +1,6 @@ import { describe, test, expect } from 'vitest'; -import { ContestType } from '$lib/types/contest'; +import { ContestType } from '$lib/contests/types/contest'; import type { TaskResult, TaskResults } from '$lib/types/task'; import { diff --git a/src/features/tasks/utils/contest-table/arc_providers.ts b/src/features/tasks/utils/contest-table/arc_providers.ts index 2de4b2490..4751de367 100644 --- a/src/features/tasks/utils/contest-table/arc_providers.ts +++ b/src/features/tasks/utils/contest-table/arc_providers.ts @@ -4,7 +4,7 @@ import { } from '$features/tasks/types/contest-table/contest_table_provider'; import type { TaskResult } from '$lib/types/task'; -import { classifyContest, getContestNameLabel } from '$lib/utils/contest'; +import { classifyContest, getContestNameLabel } from '$lib/contests/utils/contest'; import { ContestTableProviderBase, parseContestRound } from './contest_table_provider_base'; diff --git a/src/features/tasks/utils/contest-table/awc_provider.test.ts b/src/features/tasks/utils/contest-table/awc_provider.test.ts index 6306f96fa..99bd4e4b8 100644 --- a/src/features/tasks/utils/contest-table/awc_provider.test.ts +++ b/src/features/tasks/utils/contest-table/awc_provider.test.ts @@ -1,6 +1,6 @@ import { describe, test, expect } from 'vitest'; -import { ContestType } from '$lib/types/contest'; +import { ContestType } from '$lib/contests/types/contest'; import type { TaskResults } from '$lib/types/task'; import { AWCRangeProvider, AWCSpecialContestProvider } from './awc_provider'; diff --git a/src/features/tasks/utils/contest-table/awc_provider.ts b/src/features/tasks/utils/contest-table/awc_provider.ts index 0a32cc731..0c9293ff6 100644 --- a/src/features/tasks/utils/contest-table/awc_provider.ts +++ b/src/features/tasks/utils/contest-table/awc_provider.ts @@ -2,10 +2,10 @@ import { type ContestTableMetaData, type ContestTableDisplayConfig, } from '$features/tasks/types/contest-table/contest_table_provider'; -import { ContestType } from '$lib/types/contest'; +import { ContestType } from '$lib/contests/types/contest'; import type { TaskResult } from '$lib/types/task'; -import { classifyContest, getContestNameLabel } from '$lib/utils/contest'; +import { classifyContest, getContestNameLabel } from '$lib/contests/utils/contest'; import { ContestTableProviderBase, parseContestRound } from './contest_table_provider_base'; diff --git a/src/features/tasks/utils/contest-table/axc_like_provider.test.ts b/src/features/tasks/utils/contest-table/axc_like_provider.test.ts index 845bcaad6..3fba7b855 100644 --- a/src/features/tasks/utils/contest-table/axc_like_provider.test.ts +++ b/src/features/tasks/utils/contest-table/axc_like_provider.test.ts @@ -1,6 +1,6 @@ import { describe, test, expect } from 'vitest'; -import { ContestType } from '$lib/types/contest'; +import { ContestType } from '$lib/contests/types/contest'; import type { TaskResults } from '$lib/types/task'; import { ABCLikeProvider } from './axc_like_provider'; diff --git a/src/features/tasks/utils/contest-table/axc_like_provider.ts b/src/features/tasks/utils/contest-table/axc_like_provider.ts index a46980471..d411d8be5 100644 --- a/src/features/tasks/utils/contest-table/axc_like_provider.ts +++ b/src/features/tasks/utils/contest-table/axc_like_provider.ts @@ -4,7 +4,7 @@ import { } from '$features/tasks/types/contest-table/contest_table_provider'; import type { TaskResult } from '$lib/types/task'; -import { classifyContest } from '$lib/utils/contest'; +import { classifyContest } from '$lib/contests/utils/contest'; import { ContestTableProviderBase } from './contest_table_provider_base'; diff --git a/src/features/tasks/utils/contest-table/contest_table_provider_base.test.ts b/src/features/tasks/utils/contest-table/contest_table_provider_base.test.ts index c30191607..5dbdb797d 100644 --- a/src/features/tasks/utils/contest-table/contest_table_provider_base.test.ts +++ b/src/features/tasks/utils/contest-table/contest_table_provider_base.test.ts @@ -1,6 +1,6 @@ import { describe, test, expect } from 'vitest'; -import { ContestType } from '$lib/types/contest'; +import { ContestType } from '$lib/contests/types/contest'; import { ABSProvider } from './contest_table_provider'; import { taskResultsForABS } from '$features/tasks/fixtures/contest-table/contest_table_provider'; diff --git a/src/features/tasks/utils/contest-table/contest_table_provider_base.ts b/src/features/tasks/utils/contest-table/contest_table_provider_base.ts index ef41ae617..fb36f1545 100644 --- a/src/features/tasks/utils/contest-table/contest_table_provider_base.ts +++ b/src/features/tasks/utils/contest-table/contest_table_provider_base.ts @@ -5,7 +5,7 @@ import { type ContestTableDisplayConfig, type ProviderKey, } from '$features/tasks/types/contest-table/contest_table_provider'; -import { ContestType } from '$lib/types/contest'; +import { ContestType } from '$lib/contests/types/contest'; import type { TaskResults, TaskResult } from '$lib/types/task'; import { getTaskTableHeaderName } from '$lib/utils/task'; diff --git a/src/features/tasks/utils/contest-table/contest_table_provider_group.test.ts b/src/features/tasks/utils/contest-table/contest_table_provider_group.test.ts index 46ce4d599..ac7aaeded 100644 --- a/src/features/tasks/utils/contest-table/contest_table_provider_group.test.ts +++ b/src/features/tasks/utils/contest-table/contest_table_provider_group.test.ts @@ -1,6 +1,6 @@ import { describe, test, expect } from 'vitest'; -import { ContestType } from '$lib/types/contest'; +import { ContestType } from '$lib/contests/types/contest'; import { ABSProvider, diff --git a/src/features/tasks/utils/contest-table/contest_table_provider_group.ts b/src/features/tasks/utils/contest-table/contest_table_provider_group.ts index e16580a12..781b812bf 100644 --- a/src/features/tasks/utils/contest-table/contest_table_provider_group.ts +++ b/src/features/tasks/utils/contest-table/contest_table_provider_group.ts @@ -1,5 +1,5 @@ import { type ContestTablesMetaData } from '$features/tasks/types/contest-table/contest_table_provider'; -import { ContestType } from '$lib/types/contest'; +import { ContestType } from '$lib/contests/types/contest'; import { ContestTableProviderBase } from './contest_table_provider_base'; diff --git a/src/features/tasks/utils/contest-table/contest_table_provider_groups.test.ts b/src/features/tasks/utils/contest-table/contest_table_provider_groups.test.ts index 77372acb4..f879b1efd 100644 --- a/src/features/tasks/utils/contest-table/contest_table_provider_groups.test.ts +++ b/src/features/tasks/utils/contest-table/contest_table_provider_groups.test.ts @@ -1,6 +1,6 @@ import { describe, test, expect } from 'vitest'; -import { ContestType } from '$lib/types/contest'; +import { ContestType } from '$lib/contests/types/contest'; import { TESSOKU_SECTIONS } from '$features/tasks/types/contest-table/contest_table_provider'; diff --git a/src/features/tasks/utils/contest-table/contest_table_provider_groups.ts b/src/features/tasks/utils/contest-table/contest_table_provider_groups.ts index ea311d973..98e7da299 100644 --- a/src/features/tasks/utils/contest-table/contest_table_provider_groups.ts +++ b/src/features/tasks/utils/contest-table/contest_table_provider_groups.ts @@ -1,4 +1,4 @@ -import { ContestType } from '$lib/types/contest'; +import { ContestType } from '$lib/contests/types/contest'; import { ABSProvider } from './abs_provider'; import { diff --git a/src/features/tasks/utils/contest-table/dp_providers.test.ts b/src/features/tasks/utils/contest-table/dp_providers.test.ts index 92cc9407c..c88cd189c 100644 --- a/src/features/tasks/utils/contest-table/dp_providers.test.ts +++ b/src/features/tasks/utils/contest-table/dp_providers.test.ts @@ -1,6 +1,6 @@ import { describe, test, expect } from 'vitest'; -import { ContestType } from '$lib/types/contest'; +import { ContestType } from '$lib/contests/types/contest'; import type { TaskResults } from '$lib/types/task'; import { EDPCProvider, TDPCProvider, NDPCProvider } from './dp_providers'; diff --git a/src/features/tasks/utils/contest-table/dp_providers.ts b/src/features/tasks/utils/contest-table/dp_providers.ts index 8ee49b01f..9936ec050 100644 --- a/src/features/tasks/utils/contest-table/dp_providers.ts +++ b/src/features/tasks/utils/contest-table/dp_providers.ts @@ -4,7 +4,7 @@ import { } from '$features/tasks/types/contest-table/contest_table_provider'; import type { TaskResult } from '$lib/types/task'; -import { classifyContest } from '$lib/utils/contest'; +import { classifyContest } from '$lib/contests/utils/contest'; import { ContestTableProviderBase } from './contest_table_provider_base'; diff --git a/src/features/tasks/utils/contest-table/fps24_provider.test.ts b/src/features/tasks/utils/contest-table/fps24_provider.test.ts index b1aeb2f0a..a520a86ae 100644 --- a/src/features/tasks/utils/contest-table/fps24_provider.test.ts +++ b/src/features/tasks/utils/contest-table/fps24_provider.test.ts @@ -1,6 +1,6 @@ import { describe, test, expect } from 'vitest'; -import { ContestType } from '$lib/types/contest'; +import { ContestType } from '$lib/contests/types/contest'; import type { TaskResults } from '$lib/types/task'; import { FPS24Provider } from './fps24_provider'; diff --git a/src/features/tasks/utils/contest-table/fps24_provider.ts b/src/features/tasks/utils/contest-table/fps24_provider.ts index 48b5decee..488669c77 100644 --- a/src/features/tasks/utils/contest-table/fps24_provider.ts +++ b/src/features/tasks/utils/contest-table/fps24_provider.ts @@ -4,7 +4,7 @@ import { } from '$features/tasks/types/contest-table/contest_table_provider'; import type { TaskResult } from '$lib/types/task'; -import { classifyContest } from '$lib/utils/contest'; +import { classifyContest } from '$lib/contests/utils/contest'; import { ContestTableProviderBase } from './contest_table_provider_base'; diff --git a/src/features/tasks/utils/contest-table/joi_providers.test.ts b/src/features/tasks/utils/contest-table/joi_providers.test.ts index 16e18ae21..46e7cfd99 100644 --- a/src/features/tasks/utils/contest-table/joi_providers.test.ts +++ b/src/features/tasks/utils/contest-table/joi_providers.test.ts @@ -1,6 +1,6 @@ import { describe, test, expect } from 'vitest'; -import { ContestType } from '$lib/types/contest'; +import { ContestType } from '$lib/contests/types/contest'; import type { TaskResults } from '$lib/types/task'; import { @@ -81,7 +81,7 @@ describe('JOIFirstQualRoundProvider', () => { test('expects to handle invalid contest IDs gracefully', () => { const provider = new JOIFirstQualRoundProvider(ContestType.JOI); - expect(provider.getContestRoundLabel('invalid-id')).toBe('INVALID-ID'); // See: getContestNameLabel() in src/lib/utils/contest.ts + expect(provider.getContestRoundLabel('invalid-id')).toBe('INVALID-ID'); // See: getContestNameLabel() in src/lib/contests/utils/contest.ts expect(provider.getContestRoundLabel('joi2024yo1d')).toBe('2024d'); // 'd' doesn't match valid round (a|b|c) }); diff --git a/src/features/tasks/utils/contest-table/joi_providers.ts b/src/features/tasks/utils/contest-table/joi_providers.ts index 9e78c1257..950ae6841 100644 --- a/src/features/tasks/utils/contest-table/joi_providers.ts +++ b/src/features/tasks/utils/contest-table/joi_providers.ts @@ -4,10 +4,10 @@ import { JOI_SECOND_QUAL_ROUND_SECTIONS, JOI_FINAL_ROUND_SECTIONS, } from '$features/tasks/types/contest-table/contest_table_provider'; -import { ContestType } from '$lib/types/contest'; +import { ContestType } from '$lib/contests/types/contest'; import type { TaskResult } from '$lib/types/task'; -import { classifyContest, getContestNameLabel } from '$lib/utils/contest'; +import { classifyContest, getContestNameLabel } from '$lib/contests/utils/contest'; import { ContestTableProviderBase } from './contest_table_provider_base'; diff --git a/src/features/tasks/utils/contest-table/math_and_algorithm_provider.test.ts b/src/features/tasks/utils/contest-table/math_and_algorithm_provider.test.ts index 2471ddae6..c972d89da 100644 --- a/src/features/tasks/utils/contest-table/math_and_algorithm_provider.test.ts +++ b/src/features/tasks/utils/contest-table/math_and_algorithm_provider.test.ts @@ -1,6 +1,6 @@ import { describe, test, expect } from 'vitest'; -import { ContestType } from '$lib/types/contest'; +import { ContestType } from '$lib/contests/types/contest'; import type { TaskResults } from '$lib/types/task'; import { MathAndAlgorithmProvider } from './math_and_algorithm_provider'; diff --git a/src/features/tasks/utils/contest-table/math_and_algorithm_provider.ts b/src/features/tasks/utils/contest-table/math_and_algorithm_provider.ts index ed51c2248..51e175562 100644 --- a/src/features/tasks/utils/contest-table/math_and_algorithm_provider.ts +++ b/src/features/tasks/utils/contest-table/math_and_algorithm_provider.ts @@ -4,7 +4,7 @@ import { } from '$features/tasks/types/contest-table/contest_table_provider'; import type { TaskResult } from '$lib/types/task'; -import { classifyContest } from '$lib/utils/contest'; +import { classifyContest } from '$lib/contests/utils/contest'; import { ContestTableProviderBase } from './contest_table_provider_base'; diff --git a/src/features/tasks/utils/contest-table/tessoku_book_providers.test.ts b/src/features/tasks/utils/contest-table/tessoku_book_providers.test.ts index 2bf3dc1d7..68241da1d 100644 --- a/src/features/tasks/utils/contest-table/tessoku_book_providers.test.ts +++ b/src/features/tasks/utils/contest-table/tessoku_book_providers.test.ts @@ -1,6 +1,6 @@ import { describe, test, expect } from 'vitest'; -import { ContestType } from '$lib/types/contest'; +import { ContestType } from '$lib/contests/types/contest'; import type { TaskResults } from '$lib/types/task'; import { diff --git a/src/features/tasks/utils/contest-table/tessoku_book_providers.ts b/src/features/tasks/utils/contest-table/tessoku_book_providers.ts index 4a32554cc..c11ae6e78 100644 --- a/src/features/tasks/utils/contest-table/tessoku_book_providers.ts +++ b/src/features/tasks/utils/contest-table/tessoku_book_providers.ts @@ -3,10 +3,10 @@ import { type ContestTableDisplayConfig, TESSOKU_SECTIONS, } from '$features/tasks/types/contest-table/contest_table_provider'; -import { ContestType } from '$lib/types/contest'; +import { ContestType } from '$lib/contests/types/contest'; import type { TaskResult } from '$lib/types/task'; -import { classifyContest } from '$lib/utils/contest'; +import { classifyContest } from '$lib/contests/utils/contest'; import { ContestTableProviderBase } from './contest_table_provider_base'; diff --git a/src/features/tasks/utils/contest-table/typical90_provider.test.ts b/src/features/tasks/utils/contest-table/typical90_provider.test.ts index 6b03018df..a81473674 100644 --- a/src/features/tasks/utils/contest-table/typical90_provider.test.ts +++ b/src/features/tasks/utils/contest-table/typical90_provider.test.ts @@ -1,6 +1,6 @@ import { describe, test, expect } from 'vitest'; -import { ContestType } from '$lib/types/contest'; +import { ContestType } from '$lib/contests/types/contest'; import type { TaskResults } from '$lib/types/task'; import { Typical90Provider } from './typical90_provider'; diff --git a/src/features/tasks/utils/contest-table/typical90_provider.ts b/src/features/tasks/utils/contest-table/typical90_provider.ts index 86d5b9009..9afd35298 100644 --- a/src/features/tasks/utils/contest-table/typical90_provider.ts +++ b/src/features/tasks/utils/contest-table/typical90_provider.ts @@ -4,7 +4,7 @@ import { } from '$features/tasks/types/contest-table/contest_table_provider'; import type { TaskResult } from '$lib/types/task'; -import { classifyContest } from '$lib/utils/contest'; +import { classifyContest } from '$lib/contests/utils/contest'; import { ContestTableProviderBase } from './contest_table_provider_base'; diff --git a/src/features/workbooks/components/detail/WorkBookTasksTable.svelte b/src/features/workbooks/components/detail/WorkBookTasksTable.svelte index 1e2d32bf0..71d62dccd 100644 --- a/src/features/workbooks/components/detail/WorkBookTasksTable.svelte +++ b/src/features/workbooks/components/detail/WorkBookTasksTable.svelte @@ -14,7 +14,7 @@ import GradeLabel from '$lib/components/GradeLabel.svelte'; import ExternalLinkWrapper from '$lib/components/ExternalLinkWrapper.svelte'; - import { addContestNameToTaskIndex } from '$lib/utils/contest'; + import { addContestNameToTaskIndex } from '$lib/contests/utils/contest'; import { getTaskUrl, removeTaskIndexFromTitle } from '$lib/utils/task'; import type { diff --git a/src/lib/clients/aizu_online_judge/clients.test.ts b/src/lib/clients/aizu_online_judge/clients.test.ts index ed66b1228..0c1039688 100644 --- a/src/lib/clients/aizu_online_judge/clients.test.ts +++ b/src/lib/clients/aizu_online_judge/clients.test.ts @@ -11,7 +11,7 @@ import { Cache } from '$lib/clients/cache'; import { AojCoursesApiClient, AojChallengesApiClient } from './clients'; -import type { ContestsForImport } from '$lib/types/contest'; +import type { ContestsForImport } from '$lib/contests/types/contest'; import type { TasksForImport } from '$lib/types/task'; import type { AOJCourseAPI, AOJChallengeContestAPI, AOJTaskAPIs } from './types'; diff --git a/src/lib/clients/aizu_online_judge/clients.ts b/src/lib/clients/aizu_online_judge/clients.ts index 33fbc44e9..9af340fdd 100644 --- a/src/lib/clients/aizu_online_judge/clients.ts +++ b/src/lib/clients/aizu_online_judge/clients.ts @@ -2,7 +2,7 @@ import { HttpRequestClient } from '$lib/clients/http_client'; import { ContestTaskCache } from '$lib/clients/cache_strategy'; import { getCachedOrFetchContests, getCachedOrFetchTasks } from '$lib/clients/contest_task_fetcher'; -import type { ContestsForImport } from '$lib/types/contest'; +import type { ContestsForImport } from '$lib/contests/types/contest'; import type { TasksForImport } from '$lib/types/task'; import type { diff --git a/src/lib/clients/aizu_online_judge/utils.ts b/src/lib/clients/aizu_online_judge/utils.ts index 149464eb4..cb22863f7 100644 --- a/src/lib/clients/aizu_online_judge/utils.ts +++ b/src/lib/clients/aizu_online_judge/utils.ts @@ -1,4 +1,4 @@ -import type { ContestForImport } from '$lib/types/contest'; +import type { ContestForImport } from '$lib/contests/types/contest'; import { PENDING } from './types'; import type { AOJTaskAPI } from './types'; diff --git a/src/lib/clients/atcoder/atcoder_problems.test.ts b/src/lib/clients/atcoder/atcoder_problems.test.ts index 8652495a7..9b78df8be 100644 --- a/src/lib/clients/atcoder/atcoder_problems.test.ts +++ b/src/lib/clients/atcoder/atcoder_problems.test.ts @@ -5,7 +5,7 @@ vi.mock('$lib/utils/time', () => ({ delay: vi.fn().mockResolvedValue(undefined), })); -import type { ContestsForImport } from '$lib/types/contest'; +import type { ContestsForImport } from '$lib/contests/types/contest'; import type { TasksForImport } from '$lib/types/task'; import { AtCoderProblemsApiClient } from '$lib/clients/atcoder/atcoder_problems'; diff --git a/src/lib/clients/atcoder/atcoder_problems.ts b/src/lib/clients/atcoder/atcoder_problems.ts index b05b17a59..653509a57 100644 --- a/src/lib/clients/atcoder/atcoder_problems.ts +++ b/src/lib/clients/atcoder/atcoder_problems.ts @@ -1,6 +1,6 @@ import { HttpRequestClient } from '$lib/clients/http_client'; -import type { ContestsForImport } from '$lib/types/contest'; +import type { ContestsForImport } from '$lib/contests/types/contest'; import type { TasksForImport } from '$lib/types/task'; import { ATCODER_PROBLEMS_API_BASE_URL } from '$lib/constants/urls'; diff --git a/src/lib/clients/cache_strategy.ts b/src/lib/clients/cache_strategy.ts index 220343f74..3b9657536 100644 --- a/src/lib/clients/cache_strategy.ts +++ b/src/lib/clients/cache_strategy.ts @@ -1,6 +1,6 @@ import { Cache } from '$lib/clients/cache'; -import type { ContestsForImport } from '$lib/types/contest'; +import type { ContestsForImport } from '$lib/contests/types/contest'; import type { TasksForImport } from '$lib/types/task'; export class ContestTaskCache { diff --git a/src/lib/clients/contest_task_fetcher.ts b/src/lib/clients/contest_task_fetcher.ts index 4fd25fbef..1310d669c 100644 --- a/src/lib/clients/contest_task_fetcher.ts +++ b/src/lib/clients/contest_task_fetcher.ts @@ -1,6 +1,6 @@ import type { HttpRequestClient } from '$lib/clients/http_client'; import type { ContestTaskCache } from '$lib/clients/cache_strategy'; -import type { ContestsForImport } from '$lib/types/contest'; +import type { ContestsForImport } from '$lib/contests/types/contest'; import type { TasksForImport } from '$lib/types/task'; type FetchContestsConfig = { diff --git a/src/lib/clients/http_client.ts b/src/lib/clients/http_client.ts index beb9d0c3c..a01d7bdd0 100644 --- a/src/lib/clients/http_client.ts +++ b/src/lib/clients/http_client.ts @@ -1,4 +1,4 @@ -import type { ContestsForImport } from '$lib/types/contest'; +import type { ContestsForImport } from '$lib/contests/types/contest'; import type { TasksForImport } from '$lib/types/task'; import { delay } from '$lib/utils/time'; diff --git a/src/lib/clients/index.ts b/src/lib/clients/index.ts index 1e4fa29f0..5b9c229d2 100644 --- a/src/lib/clients/index.ts +++ b/src/lib/clients/index.ts @@ -8,7 +8,7 @@ import { AojChallengesApiClient, } from '$lib/clients/aizu_online_judge/clients'; -import type { ContestsForImport } from '$lib/types/contest'; +import type { ContestsForImport } from '$lib/contests/types/contest'; import type { TasksForImport } from '$lib/types/task'; import type { ChallengeParams } from '$lib/clients/aizu_online_judge/types'; diff --git a/src/lib/components/SubmissionStatus/UpdatingModal.svelte b/src/lib/components/SubmissionStatus/UpdatingModal.svelte index ed2fd4a12..74d94fe74 100644 --- a/src/lib/components/SubmissionStatus/UpdatingModal.svelte +++ b/src/lib/components/SubmissionStatus/UpdatingModal.svelte @@ -8,7 +8,7 @@ import { submission_statuses } from '$lib/services/submission_status'; import { errorMessageStore } from '$lib/stores/error_message'; - import { getContestNameLabel } from '$lib/utils/contest'; + import { getContestNameLabel } from '$lib/contests/utils/contest'; import InputFieldWrapper from '$lib/components/InputFieldWrapper.svelte'; interface Props { diff --git a/src/lib/components/TagForm.svelte b/src/lib/components/TagForm.svelte index ac007fe98..5561c6145 100644 --- a/src/lib/components/TagForm.svelte +++ b/src/lib/components/TagForm.svelte @@ -14,7 +14,7 @@ import type { Task } from '$lib/types/task'; import type { Tag } from '$lib/types/tag'; - import { getContestNameLabel } from '$lib/utils/contest'; + import { getContestNameLabel } from '$lib/contests/utils/contest'; import { ATCODER_BASE_CONTEST_URL } from '$lib/constants/urls'; interface Props { diff --git a/src/lib/components/TaskForm.svelte b/src/lib/components/TaskForm.svelte index 7badafcb1..1951a4746 100644 --- a/src/lib/components/TaskForm.svelte +++ b/src/lib/components/TaskForm.svelte @@ -10,7 +10,7 @@ Label, Button, } from 'flowbite-svelte'; - import { addContestNameToTaskIndex } from '$lib/utils/contest'; + import { addContestNameToTaskIndex } from '$lib/contests/utils/contest'; import { taskGradeValues, type Task } from '$lib/types/task'; import { getTaskGradeLabel, removeTaskIndexFromTitle } from '$lib/utils/task'; diff --git a/src/lib/components/TaskList.svelte b/src/lib/components/TaskList.svelte index f0fd84c4f..d74a8df7d 100644 --- a/src/lib/components/TaskList.svelte +++ b/src/lib/components/TaskList.svelte @@ -21,7 +21,7 @@ import { getBackgroundColorFrom } from '$lib/services/submission_status'; - import { addContestNameToTaskIndex } from '$lib/utils/contest'; + import { addContestNameToTaskIndex } from '$lib/contests/utils/contest'; import { countAcceptedTasks, countAllTasks, diff --git a/src/lib/components/TaskListSorted.svelte b/src/lib/components/TaskListSorted.svelte index 2264053b6..0c2a2f286 100644 --- a/src/lib/components/TaskListSorted.svelte +++ b/src/lib/components/TaskListSorted.svelte @@ -12,7 +12,7 @@ } from 'flowbite-svelte'; import type { TaskResults } from '$lib/types/task'; - import { addContestNameToTaskIndex } from '$lib/utils/contest'; + import { addContestNameToTaskIndex } from '$lib/contests/utils/contest'; import { removeTaskIndexFromTitle } from '$lib/utils/task'; interface Props { diff --git a/src/lib/contests/fixtures/contest_type.ts b/src/lib/contests/fixtures/contest_type.ts index beb144430..bb6a0de05 100644 --- a/src/lib/contests/fixtures/contest_type.ts +++ b/src/lib/contests/fixtures/contest_type.ts @@ -615,7 +615,7 @@ export const atCoderMainOfficialOnsite = [ ]; // See: -// getPrefixForAojCourses() in src/lib/utils/contest.ts +// getPrefixForAojCourses() in src/lib/contests/utils/contest.ts const aojCoursesData = [ { name: 'AOJ Courses, ITP1', contestId: 'ITP1' }, { name: 'AOJ Courses, ALDS1', contestId: 'ALDS1' }, diff --git a/src/lib/services/tasks.ts b/src/lib/services/tasks.ts index 81a8017e6..7b5e828db 100644 --- a/src/lib/services/tasks.ts +++ b/src/lib/services/tasks.ts @@ -3,7 +3,7 @@ import { default as db } from '$lib/server/database'; import { getContestTaskPairs } from '$lib/services/contest_task_pairs'; -import { ContestType } from '$lib/types/contest'; +import { ContestType } from '$lib/contests/types/contest'; import type { Task, Tasks, TaskGrade } from '$lib/types/task'; import type { ContestTaskPair, @@ -18,7 +18,7 @@ import { } from '$lib/server/tasks/cache'; import { invalidateVoteCaches } from '$features/votes/server/cache'; -import { classifyContest } from '$lib/utils/contest'; +import { classifyContest } from '$lib/contests/utils/contest'; import { createContestTaskPairKey } from '$lib/utils/contest_task_pair'; // See: diff --git a/src/lib/types/contest.ts b/src/lib/types/contest.ts deleted file mode 100644 index 5eee3f07f..000000000 --- a/src/lib/types/contest.ts +++ /dev/null @@ -1,9 +0,0 @@ -export { - type Contest, - type Contests, - type ContestForImport, - type ContestsForImport, - ContestType, - type ContestPrefix, - type ContestLabelTranslations, -} from '$lib/contests/types/contest'; diff --git a/src/lib/types/task.ts b/src/lib/types/task.ts index d32dc6ea3..2ab3dbef9 100644 --- a/src/lib/types/task.ts +++ b/src/lib/types/task.ts @@ -1,6 +1,6 @@ // Import original enum as type. import type { TaskGrade as TaskGradeOrigin } from '@prisma/client'; -import type { ContestType } from '$lib/types/contest'; +import type { ContestType } from '$lib/contests/types/contest'; export interface Task { contest_type?: ContestType; diff --git a/src/lib/utils/contest.ts b/src/lib/utils/contest.ts deleted file mode 100644 index 52c8df748..000000000 --- a/src/lib/utils/contest.ts +++ /dev/null @@ -1,19 +0,0 @@ -export { - classifyContest, - getContestNameLabel, - getContestPriority, - contestTypePriorities, - addContestNameToTaskIndex, - isWorldTourFinals, - getWorldTourFinalsLabel, - getPastContestLabel, - getJoiContestLabel, - getAtCoderUniversityContestLabel, - getAojContestLabel, - AOJ_COURSES, - getPrefixForAojCourses, - getContestPrefixes, - regexForJag, - regexForAojUniversity, - PAST_TRANSLATIONS, -} from '$lib/contests/utils/contest'; diff --git a/src/lib/utils/task.ts b/src/lib/utils/task.ts index 3827cf94b..f67755165 100644 --- a/src/lib/utils/task.ts +++ b/src/lib/utils/task.ts @@ -1,4 +1,4 @@ -import { ContestType } from '$lib/types/contest'; +import { ContestType } from '$lib/contests/types/contest'; import { type TaskResult, type TaskResults, TaskGrade, type TaskGrades } from '$lib/types/task'; import type { UrlGenerator, UrlGenerators } from '$lib/types/url'; @@ -10,7 +10,7 @@ import { getContestPriority, regexForAojUniversity, regexForJag, -} from '$lib/utils/contest'; +} from '$lib/contests/utils/contest'; // TODO: Codeforces、yukicoder、BOJなどに対応できるようにする /** @@ -89,7 +89,7 @@ export function compareByContestIdAndTaskId( // 1. コンテスト種類別の優先度(昇順) // // See: - // contestTypePriorities in src/lib/utils/contest.ts + // contestTypePriorities in src/lib/contests/utils/contest.ts if (firstContestPriority !== secondContestPriority) { return firstContestPriority - secondContestPriority; } diff --git a/src/lib/utils/task_filter.ts b/src/lib/utils/task_filter.ts index ef08bd7ab..c011c4ef6 100644 --- a/src/lib/utils/task_filter.ts +++ b/src/lib/utils/task_filter.ts @@ -1,4 +1,4 @@ -import { getContestNameLabel } from '$lib/utils/contest'; +import { getContestNameLabel } from '$lib/contests/utils/contest'; import { getTaskUrl } from '$lib/utils/task'; type SearchableTask = { diff --git a/src/routes/(admin)/tasks/+page.server.ts b/src/routes/(admin)/tasks/+page.server.ts index fa4d081f2..4f070c7f0 100644 --- a/src/routes/(admin)/tasks/+page.server.ts +++ b/src/routes/(admin)/tasks/+page.server.ts @@ -1,6 +1,6 @@ import { fail, type Actions } from '@sveltejs/kit'; -import type { Contests, ContestForImport, ContestsForImport } from '$lib/types/contest'; +import type { Contests, ContestForImport, ContestsForImport } from '$lib/contests/types/contest'; import { type Task, type Tasks, type TaskForImport, type TasksForImport } from '$lib/types/task'; import * as taskService from '$lib/services/tasks'; @@ -8,7 +8,7 @@ import { validateAdminAccess } from '$features/auth/services/admin_access'; import { fetchContests, fetchTasks, isContestTaskImportSource } from '$lib/clients'; -import { classifyContest } from '$lib/utils/contest'; +import { classifyContest } from '$lib/contests/utils/contest'; import { sha256 } from '$lib/utils/hash'; import { BAD_REQUEST, INTERNAL_SERVER_ERROR } from '$lib/constants/http-response-status-codes'; diff --git a/src/routes/(admin)/tasks/+page.svelte b/src/routes/(admin)/tasks/+page.svelte index 950adf07c..9df4e1ec4 100644 --- a/src/routes/(admin)/tasks/+page.svelte +++ b/src/routes/(admin)/tasks/+page.svelte @@ -5,7 +5,7 @@ import { Select, Label, Button, PaginationNav } from 'flowbite-svelte'; - import type { Contests } from '$lib/types/contest'; + import type { Contests } from '$lib/contests/types/contest'; import { importSourceEntries, type ContestTaskImportSource } from '$lib/clients'; diff --git a/src/routes/(admin)/tasks/_components/TaskTableForImport.svelte b/src/routes/(admin)/tasks/_components/TaskTableForImport.svelte index 9df142602..0bc03db98 100644 --- a/src/routes/(admin)/tasks/_components/TaskTableForImport.svelte +++ b/src/routes/(admin)/tasks/_components/TaskTableForImport.svelte @@ -13,9 +13,9 @@ Label, } from 'flowbite-svelte'; - import type { Contests } from '$lib/types/contest'; + import type { Contests } from '$lib/contests/types/contest'; import type { ContestTaskImportSource } from '$lib/clients'; - import { getContestNameLabel } from '$lib/utils/contest'; + import { getContestNameLabel } from '$lib/contests/utils/contest'; import { newline } from '$lib/utils/newline'; interface Props { diff --git a/src/routes/(admin)/tasks/_utils/contests.test.ts b/src/routes/(admin)/tasks/_utils/contests.test.ts index ba497de31..aab04a36a 100644 --- a/src/routes/(admin)/tasks/_utils/contests.test.ts +++ b/src/routes/(admin)/tasks/_utils/contests.test.ts @@ -1,6 +1,6 @@ import { describe, test, expect } from 'vitest'; import { filterContests } from './contests'; -import type { Contests } from '$lib/types/contest'; +import type { Contests } from '$lib/contests/types/contest'; const contests: Contests = [ { diff --git a/src/routes/(admin)/tasks/_utils/contests.ts b/src/routes/(admin)/tasks/_utils/contests.ts index 3486cc3cd..a02ed0f29 100644 --- a/src/routes/(admin)/tasks/_utils/contests.ts +++ b/src/routes/(admin)/tasks/_utils/contests.ts @@ -1,4 +1,4 @@ -import type { Contests } from '$lib/types/contest'; +import type { Contests } from '$lib/contests/types/contest'; export function filterContests(contests: Contests, query: string): Contests { const hasNoQuery = !query || query.trim() === ''; diff --git a/src/routes/(admin)/tasks/grade/_components/TaskGradeTable.svelte b/src/routes/(admin)/tasks/grade/_components/TaskGradeTable.svelte index adebfdfc8..3a55a777f 100644 --- a/src/routes/(admin)/tasks/grade/_components/TaskGradeTable.svelte +++ b/src/routes/(admin)/tasks/grade/_components/TaskGradeTable.svelte @@ -19,7 +19,7 @@ import { taskGradeValues, TaskGrade } from '$lib/types/task'; import type { TaskWithVoteInfo } from '$features/votes/services/vote_statistics'; - import { addContestNameToTaskIndex } from '$lib/utils/contest'; + import { addContestNameToTaskIndex } from '$lib/contests/utils/contest'; import { getTaskGradeLabel, compareByContestIdAndTaskId, diff --git a/src/routes/votes/+page.svelte b/src/routes/votes/+page.svelte index 68af64e74..358d80f05 100644 --- a/src/routes/votes/+page.svelte +++ b/src/routes/votes/+page.svelte @@ -20,7 +20,7 @@ import { TaskGrade } from '$lib/types/task'; import { MIN_VOTES_FOR_PROVISIONAL_GRADE } from '$features/votes/constants/statistics'; - import { getContestNameLabel } from '$lib/utils/contest'; + import { getContestNameLabel } from '$lib/contests/utils/contest'; import { getTaskUrl, compareByContestIdAndTaskId } from '$lib/utils/task'; import { filterTasksBySearch } from '$lib/utils/task_filter'; import { resolveDisplayGrade } from '$features/votes/utils/grade_options'; diff --git a/src/routes/workbooks/[slug]/+page.svelte b/src/routes/workbooks/[slug]/+page.svelte index 9863ee84d..a490ebb13 100644 --- a/src/routes/workbooks/[slug]/+page.svelte +++ b/src/routes/workbooks/[slug]/+page.svelte @@ -20,7 +20,7 @@ import CommentAndHint from '$features/workbooks/components/detail/CommentAndHint.svelte'; import { getBackgroundColorFrom } from '$lib/services/submission_status'; - import { addContestNameToTaskIndex } from '$lib/utils/contest'; + import { addContestNameToTaskIndex } from '$lib/contests/utils/contest'; import { getTaskUrl, removeTaskIndexFromTitle } from '$lib/utils/task'; import type { TaskResult } from '$lib/types/task'; diff --git a/src/test/lib/services/fixtures/task_results.ts b/src/test/lib/services/fixtures/task_results.ts index c4f63d289..cd17bee92 100644 --- a/src/test/lib/services/fixtures/task_results.ts +++ b/src/test/lib/services/fixtures/task_results.ts @@ -1,4 +1,4 @@ -import { ContestType } from '$lib/types/contest'; +import { ContestType } from '$lib/contests/types/contest'; import { TaskGrade } from '$lib/types/task'; type MockedSubmissionStatus = { diff --git a/src/test/lib/utils/task.test.ts b/src/test/lib/utils/task.test.ts index f069db06a..8e8a3144e 100644 --- a/src/test/lib/utils/task.test.ts +++ b/src/test/lib/utils/task.test.ts @@ -13,7 +13,7 @@ import { } from './test_cases/task_results'; import { taskResultsForTaskTableHeaderName } from './test_cases/task_table_header_name'; -import { ContestType } from '$lib/types/contest'; +import { ContestType } from '$lib/contests/types/contest'; import { type TaskResult, type TaskResults } from '$lib/types/task'; import type { WorkBookTaskBase } from '$features/workbooks/types/workbook'; From 1ee2eba87e1d062168444865b48a8f85db1d446e Mon Sep 17 00:00:00 2001 From: "k.hiro1818" Date: Sun, 6 Sep 2026 05:20:59 +0000 Subject: [PATCH 5/5] docs: update paths for contest module restructure Co-Authored-By: Claude Opus 4.6 --- .claude/skills/add-contest-table-provider/instructions.md | 8 ++++---- docs/guides/architecture.md | 1 + docs/guides/how-to-add-contest-table-provider.md | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.claude/skills/add-contest-table-provider/instructions.md b/.claude/skills/add-contest-table-provider/instructions.md index 5c3ee8062..8fbdd838d 100644 --- a/.claude/skills/add-contest-table-provider/instructions.md +++ b/.claude/skills/add-contest-table-provider/instructions.md @@ -33,16 +33,16 @@ Step 0 (seed check) is already done. Confirm the following before touching code: > Skip when an existing ContestType already covers this contest family (e.g. `AOJ_JAG` for JAG Prelim). - [ ] Add to `prisma/schema.prisma` ContestType enum → `pnpm exec prisma generate` -- [ ] Add to `ContestType` in `src/lib/types/contest.ts` (same position as schema) +- [ ] Add to `ContestType` in `src/lib/contests/types/contest.ts` (same position as schema) - [ ] `pnpm check` — confirm error resolved ## Layer 3 — Contest utilities (TDD) ### Tests first -- [ ] Add exports to `src/test/lib/utils/test_cases/contest_type.ts` and `contest_name_labels.ts` -- [ ] Add 3 `describe` blocks to `src/test/lib/utils/contest.test.ts`: classify / priority / name label -- [ ] `pnpm test:unit src/test/lib/utils/contest.test.ts` — **RED** +- [ ] Add exports to `src/lib/contests/fixtures/contest_type.ts` and `contest_name_labels.ts` +- [ ] Add 3 `describe` blocks to `src/lib/contests/utils/contest.test.ts`: classify / priority / name label +- [ ] `pnpm test:unit src/lib/contests/utils/contest.test.ts` — **RED** ### Implement diff --git a/docs/guides/architecture.md b/docs/guides/architecture.md index e78ccd92f..db684468c 100644 --- a/docs/guides/architecture.md +++ b/docs/guides/architecture.md @@ -158,6 +158,7 @@ src/lib/ ├── clients/ # 外部 API クライアント(AtCoder Problems, AOJ) ├── components/ # 共通 UI コンポーネント(GradeLabel, TaskGradeList, TaskList, FormWrapper 等) ├── constants/ # アプリ定数 +├── contests/ # コンテスト分類・ラベル生成・優先度(types/, utils/, fixtures/) ├── server/ # サーバー専用の共有インフラ │ ├── database.ts # Prisma クライアント(14+ サービスが依存) │ ├── tasks/ # cache.ts など複数 feature 共有のサーバ処理 diff --git a/docs/guides/how-to-add-contest-table-provider.md b/docs/guides/how-to-add-contest-table-provider.md index a8c68d045..70ef94fa9 100644 --- a/docs/guides/how-to-add-contest-table-provider.md +++ b/docs/guides/how-to-add-contest-table-provider.md @@ -6,8 +6,8 @@ ## 事前確認 -- [ ] `ContestType`(`src/lib/types/contest.ts`)— 既存で対応できるか?複数 contest_id を統一表示するなら複合型 -- [ ] `classifyContest()`(`src/lib/utils/contest.ts`)— 新 contest_id に正しい ContestType を返すか +- [ ] `ContestType`(`src/lib/contests/types/contest.ts`)— 既存で対応できるか?複数 contest_id を統一表示するなら複合型 +- [ ] `classifyContest()`(`src/lib/contests/utils/contest.ts`)— 新 contest_id に正しい ContestType を返すか - [ ] `prisma/tasks.ts` にデータが存在するか(複合型は `prisma/contest_task_pairs.ts` も) - [ ] 実装パターン判定(後述4パターン) - [ ] JOI: 2026 年より `joi{YYYY}ho` → `joi{YYYY}sf`。regex は `(ho|sf)` 対応済み