|
| 1 | +# Plan: AtCoder Weekday Contest (AWC) テーブル追加 |
| 2 | + |
| 3 | +## Context |
| 4 | + |
| 5 | +Issue [#3153](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/3153) の対応。新しいコンテストシリーズ「AtCoder Weekday Contest」のテーブルを追加する。パターン1(範囲フィルタ型)で、ARC104Onwards / AGC001Onwards と同じ構造。 |
| 6 | + |
| 7 | +**仕様**: |
| 8 | + |
| 9 | +- ContestType: `AWC`(新規) |
| 10 | +- contest_id: `awc0001`, `awc0002`, ...(awc + 4桁数字) |
| 11 | +- 範囲: awc0001〜(上限なし) |
| 12 | +- セクション: A〜E(5問/回) |
| 13 | +- タイトル: "AtCoder Weekday Contest" |
| 14 | + |
| 15 | +## 実装手順 |
| 16 | + |
| 17 | +### Step 1: Prisma スキーマに `AWC` 追加 |
| 18 | + |
| 19 | +**File**: [prisma/schema.prisma](prisma/schema.prisma) |
| 20 | + |
| 21 | +`ContestType` enum に `AWC` を追加(`AGC_LIKE` の次)。 |
| 22 | + |
| 23 | +``` |
| 24 | +pnpm exec prisma migrate dev --name add_awc_to_contest_type |
| 25 | +``` |
| 26 | + |
| 27 | +### Step 2: TypeScript 型更新 |
| 28 | + |
| 29 | +**File**: [src/lib/types/contest.ts](src/lib/types/contest.ts) |
| 30 | + |
| 31 | +- `ContestType` オブジェクトに `AWC: 'AWC'` 追加(`AGC_LIKE` の次) |
| 32 | +- `contestTypePriorities` に `[ContestType.AWC, 16]` 追加し、以降(UNIVERSITY 〜 AOJ_JAG)の優先度を +1 シフト: |
| 33 | + - UNIVERSITY: 16 → 17 |
| 34 | + - FPS_24: 17 → 18 |
| 35 | + - OTHERS: 18 → 19 |
| 36 | + - AOJ_COURSES: 19 → 20 |
| 37 | + - AOJ_PCK: 20 → 21 |
| 38 | + - AOJ_JAG: 21 → 22 |
| 39 | + |
| 40 | +### Step 3: `classifyContest()` / `getContestNameLabel()` 更新 |
| 41 | + |
| 42 | +**File**: [src/lib/utils/contest.ts](src/lib/utils/contest.ts) |
| 43 | + |
| 44 | +- `classifyContest()` に AWC パターン追加(L17付近、AGC の後): |
| 45 | + ```typescript |
| 46 | + if (/^awc\d{4}$/.exec(contest_id)) { |
| 47 | + return ContestType.AWC; |
| 48 | + } |
| 49 | + ``` |
| 50 | +- `regexForAxc` の下に `regexForAwc = /^(awc)(\d{4})/i` 追加 |
| 51 | +- `getContestNameLabel()` に AWC 分岐追加(`regexForAxc` の後): |
| 52 | + → `"awc0001"` → `"AWC 0001"` |
| 53 | + |
| 54 | +#### 単体テスト |
| 55 | + |
| 56 | +既存テストの構造に合わせて、3つのテストケースファイルに AWC を追加し、テスト本体にも describe ブロックを追加する。 |
| 57 | + |
| 58 | +**1. `classifyContest()` テスト** |
| 59 | + |
| 60 | +- **File**: [src/test/lib/utils/test_cases/contest_type.ts](src/test/lib/utils/test_cases/contest_type.ts) |
| 61 | + - `awc` エクスポートを追加。テストケース: |
| 62 | + - `awc0001` → `ContestType.AWC` |
| 63 | + - `awc0002` → `ContestType.AWC` |
| 64 | + - `awc9999` → `ContestType.AWC` |
| 65 | + |
| 66 | +- **File**: [src/test/lib/utils/contest.test.ts](src/test/lib/utils/contest.test.ts) |
| 67 | + - `classify contest > AtCoder` に `when contest_id contains awc` describe ブロック追加(`agc-like` の後) |
| 68 | + - `get contest priority > AtCoder` にも同様の describe ブロック追加 |
| 69 | + |
| 70 | +**2. `getContestNameLabel()` テスト** |
| 71 | + |
| 72 | +- **File**: [src/test/lib/utils/test_cases/contest_name_labels.ts](src/test/lib/utils/test_cases/contest_name_labels.ts) |
| 73 | + - `awc` エクスポートを追加。テストケース: |
| 74 | + - `awc0001` → `"AWC 0001"` |
| 75 | + - `awc0002` → `"AWC 0002"` |
| 76 | + - `awc9999` → `"AWC 9999"` |
| 77 | + |
| 78 | +- **File**: [src/test/lib/utils/contest.test.ts](src/test/lib/utils/contest.test.ts) |
| 79 | + - `get contest name label > AtCoder` に `when contest_id contains awc` describe ブロック追加 |
| 80 | + |
| 81 | +**3. `addContestNameToTaskIndex()` テスト** |
| 82 | + |
| 83 | +- **File**: [src/test/lib/utils/test_cases/contest_name_and_task_index.ts](src/test/lib/utils/test_cases/contest_name_and_task_index.ts) |
| 84 | + - `awc` エクスポートを追加。テストケース: |
| 85 | + - `awc0001`, task `A` → `"AWC 0001 - A"` |
| 86 | + - `awc0001`, task `E` → `"AWC 0001 - E"` |
| 87 | + |
| 88 | +- **File**: [src/test/lib/utils/contest.test.ts](src/test/lib/utils/contest.test.ts) |
| 89 | + - `add contest name to task index > AtCoder` に `when contest_id contains awc` describe ブロック追加 |
| 90 | + |
| 91 | +### Step 4: Provider クラス実装 |
| 92 | + |
| 93 | +**File**: [src/lib/utils/contest_table_provider.ts](src/lib/utils/contest_table_provider.ts) |
| 94 | + |
| 95 | +`ABCLikeProvider`(L468)の後に `AWC0001OnwardsProvider` を追加。`ARC104OnwardsProvider`(L336-359)をテンプレートとして使用: |
| 96 | + |
| 97 | +```typescript |
| 98 | +export class AWC0001OnwardsProvider extends ContestTableProviderBase { |
| 99 | + protected setFilterCondition(): (taskResult: TaskResult) => boolean { |
| 100 | + return (taskResult: TaskResult) => { |
| 101 | + if (classifyContest(taskResult.contest_id) !== this.contestType) { |
| 102 | + return false; |
| 103 | + } |
| 104 | + const contestRound = parseContestRound(taskResult.contest_id, 'awc'); |
| 105 | + return contestRound >= 1 && contestRound <= 9999; |
| 106 | + }; |
| 107 | + } |
| 108 | + |
| 109 | + getMetadata(): ContestTableMetaData { |
| 110 | + return { |
| 111 | + title: 'AtCoder Weekday Contest 0001 〜 ', |
| 112 | + abbreviationName: 'awc0001Onwards', |
| 113 | + }; |
| 114 | + } |
| 115 | + |
| 116 | + getContestRoundLabel(contestId: string): string { |
| 117 | + const contestNameLabel = getContestNameLabel(contestId); |
| 118 | + return contestNameLabel.replace('AWC ', ''); |
| 119 | + } |
| 120 | +} |
| 121 | +``` |
| 122 | + |
| 123 | +- `getDisplayConfig()` はベースクラスのデフォルト(`isShownHeader: true`, `isShownRoundLabel: true`, `isShownTaskIndex: false`)をそのまま使用 |
| 124 | + |
| 125 | +### Step 5: プリセット登録 |
| 126 | + |
| 127 | +**File**: [src/lib/utils/contest_table_provider.ts](src/lib/utils/contest_table_provider.ts) |
| 128 | + |
| 129 | +- `prepareContestProviderPresets()`(L1128)に追加: |
| 130 | + ```typescript |
| 131 | + AWC0001Onwards: () => |
| 132 | + new ContestTableProviderGroup(`AWC 0001 Onwards`, { |
| 133 | + buttonLabel: 'AWC 0001 〜 ', |
| 134 | + ariaLabel: 'Filter contests from AWC 0001 onwards', |
| 135 | + }).addProvider(new AWC0001OnwardsProvider(ContestType.AWC)), |
| 136 | + ``` |
| 137 | +- `contestTableProviderGroups`(L1307)に追加: |
| 138 | + ```typescript |
| 139 | + awc0001Onwards: prepareContestProviderPresets().AWC0001Onwards(), |
| 140 | + ``` |
| 141 | + |
| 142 | +### Step 6: シードデータ追加 |
| 143 | + |
| 144 | +**File**: [prisma/tasks.ts](prisma/tasks.ts) |
| 145 | + |
| 146 | +AWC のタスクデータを追加(実際の問題名は AtCoder Problems API から確認が必要)。 |
| 147 | + |
| 148 | +### Step 7: テスト実装 |
| 149 | + |
| 150 | +**Files**: |
| 151 | + |
| 152 | +- [src/test/lib/utils/test_cases/contest_table_provider.ts](src/test/lib/utils/test_cases/contest_table_provider.ts) — モックデータ追加 |
| 153 | +- [src/test/lib/utils/contest_table_provider.test.ts](src/test/lib/utils/contest_table_provider.test.ts) — テストスイート追加 |
| 154 | + |
| 155 | +テスト内容: |
| 156 | + |
| 157 | +1. フィルタリング検証(AWC のみ通過、他を除外) |
| 158 | +2. メタデータ(title, abbreviationName) |
| 159 | +3. ディスプレイ設定(isShownHeader, isShownRoundLabel, isShownTaskIndex) |
| 160 | +4. ラウンドラベル(`"awc0001"` → `"0001"`) |
| 161 | +5. テーブル構造生成(5問 A〜E) |
| 162 | +6. 空入力ハンドリング |
| 163 | +7. プリセットグループの検証 |
| 164 | + |
| 165 | +`classifyContest` モックに AWC 分岐を追加: |
| 166 | + |
| 167 | +```typescript |
| 168 | +} else if (/^awc\d{4}$/.test(contestId)) { |
| 169 | + return ContestType.AWC; |
| 170 | +``` |
| 171 | +
|
| 172 | +## 検証 |
| 173 | +
|
| 174 | +```bash |
| 175 | +pnpm exec prisma migrate dev --name add_awc_contest_type |
| 176 | +pnpm test:unit src/test/lib/utils/contest_table_provider.test.ts |
| 177 | +pnpm check |
| 178 | +pnpm lint |
| 179 | +pnpm format |
| 180 | +``` |
| 181 | +
|
| 182 | +## 実装完了後の教訓 |
| 183 | +
|
| 184 | +### Step 3 までのフロー(Utility関数) |
| 185 | +
|
| 186 | +- `classifyContest()` → コンテスト文字列を認識(パターン照合) |
| 187 | +- `getContestNameLabel()` → 表示用フォーマット("awc0001" → "AWC 0001") |
| 188 | +- コンテスト型を優先度マップに登録 → 画面表示順を制御 |
| 189 | +
|
| 190 | +**重要**: regex パターンの桁数に注意。AWC は `\d{4}` (4桁)、ARC/AGC は `\d{3}` (3桁) だが、分類ロジック (`classifyContest`) の順序が重要。前後の誤判定を避けるため、より特異度の高いパターン(4桁)を先に配置。 |
| 191 | +
|
| 192 | +### Step 4-5 のフロー(Provider クラス + 登録) |
| 193 | +
|
| 194 | +- `ContestTableProviderBase` を拡張し `setFilterCondition()`・`getMetadata()`・`getContestRoundLabel()` 実装 |
| 195 | +- `prepareContestProviderPresets()` → Provider ファクトリ関数 |
| 196 | +- `contestTableProviderGroups` → 名前ごとにインスタンス化 |
| 197 | +
|
| 198 | +**重要**: Provider クラスは `ABCLikeProvider` の直後に配置するのではなく、コンテスト系列ごとにグループ化するのが推奨。AWC はコンテスト系列なので、AGC や ARC の近くに配置。 |
| 199 | +
|
| 200 | +### テスト戦略 |
| 201 | +
|
| 202 | +Unit テスト(`contest.test.ts`)で基本分類を検証済みなので、Provider テストは省略可能。ただし、複雑な範囲フィルタを使う場合は Provider 単体テストを追加推奨。 |
0 commit comments