From be2a875ddf9a3224e9dca079f5ac3cd60357f22c Mon Sep 17 00:00:00 2001 From: Kato Hiroki Date: Mon, 24 Nov 2025 13:22:32 +0000 Subject: [PATCH 1/3] feat(table): Add table for AGC 001 onwards (#2837) --- .../plan.md | 541 ++++++++++++++++++ src/lib/utils/contest_table_provider.ts | 37 ++ .../lib/utils/contest_table_provider.test.ts | 183 ++++++ .../test_cases/contest_table_provider.ts | 97 ++++ 4 files changed, 858 insertions(+) create mode 100644 docs/dev-notes/2025-11-19/add_tests_for_contest_table_provider/plan.md diff --git a/docs/dev-notes/2025-11-19/add_tests_for_contest_table_provider/plan.md b/docs/dev-notes/2025-11-19/add_tests_for_contest_table_provider/plan.md new file mode 100644 index 000000000..877cd9663 --- /dev/null +++ b/docs/dev-notes/2025-11-19/add_tests_for_contest_table_provider/plan.md @@ -0,0 +1,541 @@ +# AGC001OnwardsProvider テスト追加計画 + +**作成日**: 2025-11-19 + +**対象ブランチ**: #2837 + +**優先度**: High + +--- + +## 参照ドキュメント + +テストの書き方・スタイルについては、以下を参照: + +📖 [`docs/dev-notes/2025-11-15/add_tests_for_contest_table_provider/plan.md`](../../2025-11-15/add_tests_for_contest_table_provider/plan.md) (ARC104OnwardsProvider) + +**本ドキュメントは ARC版の差分版です。基本構造は ARC版に準じます。** + +--- + +## 実装チェックリスト + +### 1. テスト設計 📋 + +- [ ] フィルタリングテスト(AGC001~999範囲内のみ抽出) +- [ ] コンテストタイプ判別テスト(AGC型のみ) +- [ ] メタデータ取得テスト +- [ ] ディスプレイ設定テスト +- [ ] ラウンドラベルフォーマットテスト +- [ ] エッジケーステスト(空入力など) +- [ ] 混合コンテストタイプ対応テスト +- [ ] **複数問題パターンテスト(4問、5問、6問、7問)** + +### 2. モックデータ準備 + +- [ ] `src/test/lib/utils/test_cases/contest_table_provider.ts` に AGC001+ データを追加 +- [ ] AGC001(6問: A, B, C, D, E, F)- 標準パターン +- [ ] AGC002(6問: A, B, C, D, E, F)- 標準パターン +- [ ] AGC009(5問: A, B, C, D, E)- 例外パターン +- [ ] AGC028(7問: A, B, C, D, E, F, F2)- 2025年11時点で、唯一の7問パターン +- [ ] AGC073(4問: A, B, C, D)- 2025年11時点で、唯一の4問パターン +- [ ] AGC074(5問: A, B, C, D, E)- AGC067以降の5問パターン + +### 3. テスト実装 + +- [ ] 既存テスト(ARC104OnwardsProvider)を参考に記述 +- [ ] `AGC001OnwardsProvider` をテストファイルにインポート +- [ ] `describe.each()` に AGC001OnwardsProvider を追加(displayConfig 共通化) +- [ ] AGC001Onwards個別テストで複数問題パターンの検証を追加 + +### 4. テスト リファクタリング + +- [ ] displayConfig 共通テストを `describe.each()` で統合 +- [ ] AGC001Onwards固有テスト(フィルタリング範囲、複数パターン)を実装 + +### 5. 実装後の検証 + +- [ ] テスト実行: `pnpm test:unit src/test/lib/utils/contest_table_provider.test.ts` +- [ ] Lint チェック: `pnpm format` +- [ ] 全テスト合格確認 + +--- + +## 1. テスト対象プロバイダー + +### AGC001OnwardsProvider + +| 項目 | 仕様 | 備考 | +| ---------------- | -------------------- | ------------------ | +| **範囲** | AGC 001 ~ 999 | 開始日: 2016/07/16 | +| **問題数** | 4~7問 | ラウンドにより変動 | +| **フォーマット** | A, B, C, D, E, F, F2 | 標準は6問(F迄) | + +--- + +## 2. 問題パターン仕様 + +### パターン1: 4問コンテスト(AGC073) + +``` +task_table_index: A, B, C, D +``` + +**用例**: AGC073(唯一) + +--- + +### パターン2: 5問コンテスト(AGC009、AGC067~) + +``` +task_table_index: A, B, C, D, E +``` + +**用例**: AGC009(歴史的)、AGC067以降(標準) + +--- + +### パターン3: 6問コンテスト(標準) + +``` +task_table_index: A, B, C, D, E, F +``` + +**用例**: AGC001, AGC002, AGC010~AGC066 など大多数のラウンド + +--- + +### パターン4: 7問コンテスト(AGC028のみ) + +``` +task_table_index: A, B, C, D, E, F, F2 +``` + +**用例**: AGC028(非常に例外的) + +--- + +## 3. 表示設定(displayConfig) + +| 項目 | 値 | +| --------------------- | ------------------------------------------------------- | +| `isShownHeader` | `true` | +| `isShownRoundLabel` | `true` | +| `roundLabelWidth` | `'xl:w-16'` | +| `tableBodyCellsWidth` | `'w-1/2 xs:w-1/3 sm:w-1/4 md:w-1/5 lg:w-1/6 px-1 py-1'` | +| `isShownTaskIndex` | `false` | + +**備考**: `ARC104OnwardsProvider` と同じ設定 + +--- + +## 4. テストケース仕様 + +> 詳細は [`docs/dev-notes/2025-11-15/add_tests_for_contest_table_provider/plan.md`](../../2025-11-15/add_tests_for_contest_table_provider/plan.md) の「4. テストケース仕様」を参照。 +> +> AGC版では以下の差分のみ記載: + +### 4.1 共通テスト(describe.each()統合) + +ARC版と同様(displayConfig, ラウンドラベルフォーマット, 空入力処理) + +### 4.2 AGC001Onwards 固有テスト(差分) + +#### テスト: フィルタリング(範囲検証) + +```typescript +test('expects to filter tasks to include only AGC001 and later', () => { + const provider = new AGC001OnwardsProvider(ContestType.AGC); + const filtered = provider.filter(mockTaskResults); + + expect(filtered.every((task) => task.contest_id.startsWith('agc'))).toBe(true); + expect( + filtered.every((task) => { + const round = getContestRound(task.contest_id, 'agc'); + return round >= 1 && round <= 999; + }), + ).toBe(true); +}); +``` + +**期待値**: AGC001~999範囲内のみ +**参照**: ARC版テスト4を AGC用に適応 + +--- + +#### テスト: メタデータ取得 + +```typescript +test('expects to get correct metadata', () => { + const provider = new AGC001OnwardsProvider(ContestType.AGC); + const metadata = provider.getMetadata(); + + expect(metadata.title).toBe('AtCoder Grand Contest 001 〜 '); + expect(metadata.abbreviationName).toBe('agc001Onwards'); +}); +``` + +**参照**: ARC版テスト5 + +--- + +#### テスト: 4問パターン(AGC073) + +```typescript +test('expects to handle 4-problem contest pattern (AGC073)', () => { + const provider = new AGC001OnwardsProvider(ContestType.AGC); + const tasks = [ + { contest_id: 'agc073', task_id: 'agc073_a', task_table_index: 'A' }, + { contest_id: 'agc073', task_id: 'agc073_b', task_table_index: 'B' }, + { contest_id: 'agc073', task_id: 'agc073_c', task_table_index: 'C' }, + { contest_id: 'agc073', task_id: 'agc073_d', task_table_index: 'D' }, + ]; + const filtered = provider.filter(tasks as TaskResults); + const headerIds = provider.getHeaderIdsForTask(filtered); + + expect(filtered).toHaveLength(4); + expect(headerIds).toEqual(['A', 'B', 'C', 'D']); +}); +``` + +**参照**: ARC版テスト9を AGCに適用 + +--- + +#### テスト: 5問パターン(AGC009・AGC074) + +```typescript +test('expects to handle 5-problem contest pattern (AGC009, AGC074)', () => { + const provider = new AGC001OnwardsProvider(ContestType.AGC); + const tasks = [ + { contest_id: 'agc009', task_id: 'agc009_a', task_table_index: 'A' }, + { contest_id: 'agc009', task_id: 'agc009_b', task_table_index: 'B' }, + { contest_id: 'agc009', task_id: 'agc009_c', task_table_index: 'C' }, + { contest_id: 'agc009', task_id: 'agc009_d', task_table_index: 'D' }, + { contest_id: 'agc009', task_id: 'agc009_e', task_table_index: 'E' }, + ]; + const filtered = provider.filter(tasks as TaskResults); + const headerIds = provider.getHeaderIdsForTask(filtered); + + expect(filtered).toHaveLength(5); + expect(headerIds).toEqual(['A', 'B', 'C', 'D', 'E']); +}); +``` + +**参照**: ARC版テスト10を AGCに適用 + +--- + +#### テスト: 7問パターン+F2(AGC028) + +```typescript +test('expects to handle 7-problem contest pattern with F2 (AGC028)', () => { + const provider = new AGC001OnwardsProvider(ContestType.AGC); + const tasks = [ + { contest_id: 'agc028', task_id: 'agc028_a', task_table_index: 'A' }, + { contest_id: 'agc028', task_id: 'agc028_b', task_table_index: 'B' }, + { contest_id: 'agc028', task_id: 'agc028_c', task_table_index: 'C' }, + { contest_id: 'agc028', task_id: 'agc028_d', task_table_index: 'D' }, + { contest_id: 'agc028', task_id: 'agc028_e', task_table_index: 'E' }, + { contest_id: 'agc028', task_id: 'agc028_f', task_table_index: 'F' }, + { contest_id: 'agc028', task_id: 'agc028_f2', task_table_index: 'F2' }, + ]; + const filtered = provider.filter(tasks as TaskResults); + const headerIds = provider.getHeaderIdsForTask(filtered); + + expect(filtered).toHaveLength(7); + expect(headerIds).toEqual(['A', 'B', 'C', 'D', 'E', 'F', 'F2']); +}); +``` + +**参照**: ARC版テスト11を AGCに適用 + +--- + +#### その他のテスト + +- テスト: 混合コンテストタイプの排除 +- テスト: 範囲外コンテストの排除(AGC000以下) +- テスト: ソート順序(昇順確認) +- テスト: テーブル生成 +- テスト: ラウンド ID 取得 +- テスト: ヘッダー ID 取得 + +**参照**: ARC版テスト6, 7, 8, 12, 13, 14 + +--- + +## 5. モックデータ設計 + +### 5.1 追加先 + +`src/test/lib/utils/test_cases/contest_table_provider.ts` + +### 5.2 構成 + +#### パターンA: AGC001(6問、標準) + +```typescript +const [agc001_a, agc001_b, agc001_c, agc001_d, agc001_e, agc001_f] = createContestTasks('agc001', [ + { taskTableIndex: 'A', statusName: AC }, + { taskTableIndex: 'B', statusName: AC }, + { taskTableIndex: 'C', statusName: AC_WITH_EDITORIAL }, + { taskTableIndex: 'D', statusName: TRYING }, + { taskTableIndex: 'E', statusName: PENDING }, + { taskTableIndex: 'F', statusName: PENDING }, +]); +``` + +--- + +#### パターンB: AGC002(6問、標準) + +```typescript +const [agc002_a, agc002_b, agc002_c, agc002_d, agc002_e, agc002_f] = createContestTasks('agc002', [ + { taskTableIndex: 'A', statusName: AC }, + { taskTableIndex: 'B', statusName: AC }, + { taskTableIndex: 'C', statusName: AC }, + { taskTableIndex: 'D', statusName: AC_WITH_EDITORIAL }, + { taskTableIndex: 'E', statusName: TRYING }, + { taskTableIndex: 'F', statusName: PENDING }, +]); +``` + +--- + +#### パターンC: AGC009(5問、歴史的例外) + +```typescript +const [agc009_a, agc009_b, agc009_c, agc009_d, agc009_e] = createContestTasks('agc009', [ + { taskTableIndex: 'A', statusName: AC }, + { taskTableIndex: 'B', statusName: AC }, + { taskTableIndex: 'C', statusName: AC_WITH_EDITORIAL }, + { taskTableIndex: 'D', statusName: TRYING }, + { taskTableIndex: 'E', statusName: PENDING }, +]); +``` + +--- + +#### パターンD: AGC028(7問、F2含む) + +```typescript +const [agc028_a, agc028_b, agc028_c, agc028_d, agc028_e, agc028_f, agc028_f2] = createContestTasks( + 'agc028', + [ + { taskTableIndex: 'A', statusName: AC }, + { taskTableIndex: 'B', statusName: AC }, + { taskTableIndex: 'C', statusName: AC }, + { taskTableIndex: 'D', statusName: AC_WITH_EDITORIAL }, + { taskTableIndex: 'E', statusName: TRYING }, + { taskTableIndex: 'F', statusName: PENDING }, + { taskTableIndex: 'F2', statusName: PENDING }, + ], +); +``` + +--- + +#### パターンE: AGC073(4問) + +```typescript +const [agc073_a, agc073_b, agc073_c, agc073_d] = createContestTasks('agc073', [ + { taskTableIndex: 'A', statusName: AC }, + { taskTableIndex: 'B', statusName: AC_WITH_EDITORIAL }, + { taskTableIndex: 'C', statusName: TRYING }, + { taskTableIndex: 'D', statusName: PENDING }, +]); +``` + +--- + +#### パターンF: AGC074(5問、AGC067以降の標準) + +```typescript +const [agc074_a, agc074_b, agc074_c, agc074_d, agc074_e] = createContestTasks('agc074', [ + { taskTableIndex: 'A', statusName: AC }, + { taskTableIndex: 'B', statusName: AC }, + { taskTableIndex: 'C', statusName: AC_WITH_EDITORIAL }, + { taskTableIndex: 'D', statusName: TRYING }, + { taskTableIndex: 'E', statusName: PENDING }, +]); +``` + +--- + +### 5.3 エクスポート + +```typescript +export const taskResultsForAGC001OnwardsProvider: TaskResults = [ + agc001_a, + agc001_b, + agc001_c, + agc001_d, + agc001_e, + agc001_f, + agc002_a, + agc002_b, + agc002_c, + agc002_d, + agc002_e, + agc002_f, + agc009_a, + agc009_b, + agc009_c, + agc009_d, + agc009_e, + agc028_a, + agc028_b, + agc028_c, + agc028_d, + agc028_e, + agc028_f, + agc028_f2, + agc073_a, + agc073_b, + agc073_c, + agc073_d, + agc074_a, + agc074_b, + agc074_c, + agc074_d, + agc074_e, +]; +``` + +--- + +## 6. 実装手順 + +**ステップ1**: モックデータを `src/test/lib/utils/test_cases/contest_table_provider.ts` に追加 + +**ステップ2**: `src/test/lib/utils/contest_table_provider.test.ts` に以下を追加 + +- `describe.each()` に `AGC001OnwardsProvider` を追加(displayConfig等共通テスト) +- `describe('AGC 001 Onwards')` セクションで固有テスト(14個以上)を実装 + +**ステップ3**: テスト実行・検証 + +```bash +pnpm test:unit src/test/lib/utils/contest_table_provider.test.ts +``` + +**ステップ4**: Lint チェック + +```bash +pnpm format +``` + +--- + +## 7. AGC固有の注意点 + +### 7.1 複数例外パターンの網羅 + +AGCは以下4つの問題数パターンを持つため、各パターンを明示的にテストすることが重要: + +- 4問(AGC073) +- 5問(AGC009、AGC067~) +- 6問(〜AGC066の標準) +- 7問+F2(AGC028) + +### 7.2 モックデータの多様性 + +AGC001, AGC002(標準6問)、AGC009(歴史的5問)、AGC028(特殊7問)、AGC073(4問)、AGC074(新5問)の6パターンを用意することで、仕様変更に対応しやすい設計 + +### 7.3 displayConfig の確認 + +ARC104OnwardsProvider と同一 + +--- + +## 8. テスト数想定 + +| カテゴリ | 個数 | 備考 | +| --------------------------------- | --------- | ----------------------------------------------------- | +| 共通テスト(describe.each()統合) | 3-4 | displayConfig, ラウンドラベル, 空入力など | +| AGC001Onwards固有テスト | 14-16 | パターン4つ+その他(フィルタリング、メタデータなど) | +| **合計** | **17-20** | ARC版(14-16)より若干多い(パターン数増加のため) | + +--- + +## 9. 参考: 歴史的背景 + +- **AGC001-AGC008**: 基本は6問 +- **AGC009**: 例外的に5問 +- **AGC010-AGC027**: 基本は6問 +- **AGC028**: 例外的に7問(F2含む) +- **AGC029-AGC066**: 基本は6問 +- **AGC067-AGC072**: 基本は5問(仕様変更) +- **AGC073**: 例外的に4問 +- **AGC074以降**: 標準5問 + +--- + +## 10. 実装前確認事項 + +### 確認日: 2025-11-19 + +#### Q1: 既存テストファイルの存在状況 + +**結果**: ✅ Yes + +- `src/test/lib/utils/contest_table_provider.test.ts` は存在 +- ARC104OnwardsProvider のテストが既に実装済み(約150行) +- テストパターン: フィルタリング、メタデータ、4/5/6/7問パターン等 + +**参照**: Lines 385-530 の "ARC 104 Onwards" describe ブロック + +--- + +#### Q2: モックデータファイルの存在状況 + +**結果**: ✅ Yes + +- `src/test/lib/utils/test_cases/contest_table_provider.ts` は存在 +- 複数のABC、ARC、Typical90等のモックデータが既に定義 +- `taskResultsForARC104OnwardsProvider` がエクスポート済み + +**参照**: Lines 1-151(以降も続く)で各コンテストタイプのデータ定義 + +--- + +#### Q3: AGC001OnwardsProvider の実装状況 + +**結果**: ✅ Yes + +- `src/lib/utils/contest_table_provider.ts` Lines 287-310 に実装済み +- 実装内容: + - `setFilterCondition()`: AGC001~AGC999のフィルタリング + - `getMetadata()`: タイトル 'AtCoder Grand Contest 001 〜 ' + - `getContestRoundLabel()`: コンテスト名ラベル生成 + - ヘルパー関数 `parseContestRound()` で丸め処理 + +--- + +## 11. 実装完了記録 + +**実装日**: 2025-11-19 + +**テスト結果**: ✅ All tests passed (142 tests passed) + +**実装時の学習**: + +1. **モック設定の重要性**: テストファイルの`vi.mock()`セクションでは、被テストのコードが使用するすべての依存関数に対応する必要がある。AGC対応の際、モックに`classifyContest`と`getContestNameLabel`のAGC処理が不足していたため、フィルタリングが機能しなかった。 + +2. **複数パターン対応のテスト設計**: AGCは4/5/6/7問の4つのパターンを持つため、各パターンを個別にテストすることで、仕様変更に対応しやすいテストスイートを実現できた。モックデータ(agc001, agc002, agc009, agc028, agc073, agc074)を6つのコンテストで用意することで、パターンごとの検証が明確になった。 + +3. **ARC版との差分適用**: ARC104OnwardsProvider(2025-11-15計画)のテスト実装を参考にすることで、同様の構造のAGC001OnwardsProviderテストをスムーズに実装できた。既存実装パターンを活用することで、開発効率が大幅に向上した。 + +4. **テスト駆動による品質確認**: 計画で指定された全要件(フィルタリング、メタデータ、displayConfig、4/5/6/7問パターン、ソート順序、エッジケース等)に対してテストを実装することで、実装の正確性を機械的に検証できた。 + +**成果物**: + +- AGC001OnwardsProvider用モックデータ6パターン(33個のテスク)追加 +- AGC001Onwards固有テスト18個追加(16個実装 + displayConfig・ラウンドラベル共通テスト) +- テストモック更新(classifyContest, getContestNameLabelにAGC対応追加) + +**次ステップ**: + +- 他のコンテストプロバイダー(例: ABC系列、Typical90など)への同様テスト実装をスケール +- テストカバレッジの継続的監視と改善 diff --git a/src/lib/utils/contest_table_provider.ts b/src/lib/utils/contest_table_provider.ts index 06b577c7e..0e9ec5dc5 100644 --- a/src/lib/utils/contest_table_provider.ts +++ b/src/lib/utils/contest_table_provider.ts @@ -282,6 +282,33 @@ export class ARC104OnwardsProvider extends ContestTableProviderBase { } } +// AGC001 〜 (2016/07/16 〜 ) +// 4 〜 7 tasks per contest +export class AGC001OnwardsProvider extends ContestTableProviderBase { + protected setFilterCondition(): (taskResult: TaskResult) => boolean { + return (taskResult: TaskResult) => { + if (classifyContest(taskResult.contest_id) !== this.contestType) { + return false; + } + + const contestRound = parseContestRound(taskResult.contest_id, 'agc'); + return contestRound >= 1 && contestRound <= 999; + }; + } + + getMetadata(): ContestTableMetaData { + return { + title: 'AtCoder Grand Contest 001 〜 ', + abbreviationName: 'agc001Onwards', + }; + } + + getContestRoundLabel(contestId: string): string { + const contestNameLabel = getContestNameLabel(contestId); + return contestNameLabel.replace('AGC ', ''); + } +} + function parseContestRound(contestId: string, prefix: string): number { const withoutPrefix = contestId.replace(prefix, ''); @@ -745,6 +772,15 @@ export const prepareContestProviderPresets = () => { ariaLabel: 'Filter contests from ARC 104 onwards', }).addProvider(new ARC104OnwardsProvider(ContestType.ARC)), + /** + * Single group for AGC 001 onwards + */ + AGC001Onwards: () => + new ContestTableProviderGroup(`AGC 001 Onwards`, { + buttonLabel: 'AGC 001 〜 ', + ariaLabel: 'Filter contests from AGC 001 onwards', + }).addProvider(new AGC001OnwardsProvider(ContestType.AGC)), + /** * Single group for Typical 90 Problems */ @@ -805,6 +841,7 @@ export const contestTableProviderGroups = { fromAbc212ToAbc318: prepareContestProviderPresets().ABC212ToABC318(), fromAbc126ToAbc211: prepareContestProviderPresets().ABC126ToABC211(), arc104Onwards: prepareContestProviderPresets().ARC104Onwards(), + agc001Onwards: prepareContestProviderPresets().AGC001Onwards(), typical90: prepareContestProviderPresets().Typical90(), tessokuBook: prepareContestProviderPresets().TessokuBook(), mathAndAlgorithm: prepareContestProviderPresets().MathAndAlgorithm(), diff --git a/src/test/lib/utils/contest_table_provider.test.ts b/src/test/lib/utils/contest_table_provider.test.ts index b4b26bacd..c5b2ca28d 100644 --- a/src/test/lib/utils/contest_table_provider.test.ts +++ b/src/test/lib/utils/contest_table_provider.test.ts @@ -9,6 +9,7 @@ import { ABC212ToABC318Provider, ABC126ToABC211Provider, ARC104OnwardsProvider, + AGC001OnwardsProvider, EDPCProvider, TDPCProvider, FPS24Provider, @@ -27,6 +28,7 @@ import { TESSOKU_SECTIONS } from '$lib/types/contest_table_provider'; import { taskResultsForContestTableProvider, taskResultsForARC104OnwardsProvider, + taskResultsForAGC001OnwardsProvider, } from './test_cases/contest_table_provider'; // Mock the imported functions @@ -36,6 +38,8 @@ vi.mock('$lib/utils/contest', () => ({ return ContestType.ABC; } else if (contestId.startsWith('arc')) { return ContestType.ARC; + } else if (contestId.startsWith('agc')) { + return ContestType.AGC; } else if (contestId === 'dp') { return ContestType.EDPC; } else if (contestId === 'tdpc') { @@ -60,6 +64,8 @@ vi.mock('$lib/utils/contest', () => ({ return `ABC ${contestId.replace('abc', '')}`; } else if (contestId.startsWith('arc')) { return `ARC ${contestId.replace('arc', '')}`; + } else if (contestId.startsWith('agc')) { + return `AGC ${contestId.replace('agc', '')}`; } else if (contestId === 'dp' || contestId === 'tdpc' || contestId === 'typical90') { return ''; } else if (contestId.startsWith('joi')) { @@ -528,6 +534,183 @@ describe('ContestTableProviderBase and implementations', () => { }); }); + describe('AGC 001 Onwards', () => { + test('expects to filter tasks to include only AGC001 and later', () => { + const provider = new AGC001OnwardsProvider(ContestType.AGC); + const filtered = provider.filter(taskResultsForAGC001OnwardsProvider); + + expect(filtered.every((task) => task.contest_id.startsWith('agc'))).toBe(true); + expect( + filtered.every((task) => { + const round = getContestRound(task.contest_id); + return round >= 1 && round <= 999; + }), + ).toBe(true); + }); + + test('expects to get correct metadata', () => { + const provider = new AGC001OnwardsProvider(ContestType.AGC); + const metadata = provider.getMetadata(); + + expect(metadata.title).toBe('AtCoder Grand Contest 001 〜 '); + expect(metadata.abbreviationName).toBe('agc001Onwards'); + }); + + test('expects to get correct display configuration', () => { + const provider = new AGC001OnwardsProvider(ContestType.AGC); + const displayConfig = provider.getDisplayConfig(); + + expect(displayConfig.isShownHeader).toBe(true); + expect(displayConfig.isShownRoundLabel).toBe(true); + expect(displayConfig.roundLabelWidth).toBe('xl:w-16'); + expect(displayConfig.tableBodyCellsWidth).toBe( + 'w-1/2 xs:w-1/3 sm:w-1/4 md:w-1/5 lg:w-1/6 px-1 py-1', + ); + expect(displayConfig.isShownTaskIndex).toBe(false); + }); + + test('expects to format contest round label correctly', () => { + const provider = new AGC001OnwardsProvider(ContestType.AGC); + const label = provider.getContestRoundLabel('agc001'); + + expect(label).toBe('001'); + }); + + test('expects to generate correct table structure', () => { + const provider = new AGC001OnwardsProvider(ContestType.AGC); + const filtered = provider.filter(taskResultsForAGC001OnwardsProvider); + const table = provider.generateTable(filtered); + + expect(Object.keys(table).length).toBeGreaterThan(0); + expect(table).toHaveProperty('agc001'); + expect(table).toHaveProperty('agc002'); + expect(table).toHaveProperty('agc009'); + expect(table).toHaveProperty('agc028'); + expect(table).toHaveProperty('agc073'); + expect(table).toHaveProperty('agc074'); + }); + + test('expects to get contest round IDs correctly', () => { + const provider = new AGC001OnwardsProvider(ContestType.AGC); + const filtered = provider.filter(taskResultsForAGC001OnwardsProvider); + const roundIds = provider.getContestRoundIds(filtered); + + expect(roundIds).toContain('agc001'); + expect(roundIds).toContain('agc002'); + expect(roundIds).toContain('agc009'); + expect(roundIds).toContain('agc028'); + expect(roundIds).toContain('agc073'); + expect(roundIds).toContain('agc074'); + expect(roundIds.every((id) => id.startsWith('agc'))).toBe(true); + }); + + test('expects to get header IDs for tasks correctly', () => { + const provider = new AGC001OnwardsProvider(ContestType.AGC); + const filtered = provider.filter(taskResultsForAGC001OnwardsProvider); + const headerIds = provider.getHeaderIdsForTask(filtered); + + expect(headerIds.length).toBeGreaterThan(0); + expect(headerIds.every((id) => id.length > 0)).toBe(true); + }); + + test('expects to handle 4-problem contest pattern (AGC073)', () => { + const provider = new AGC001OnwardsProvider(ContestType.AGC); + const agc073Tasks = taskResultsForAGC001OnwardsProvider.filter( + (task) => task.contest_id === 'agc073', + ); + const headerIds = provider.getHeaderIdsForTask(agc073Tasks as TaskResults); + + expect(agc073Tasks).toHaveLength(4); + expect(headerIds).toEqual(['A', 'B', 'C', 'D']); + }); + + test('expects to handle 5-problem contest pattern (AGC009, AGC074)', () => { + const provider = new AGC001OnwardsProvider(ContestType.AGC); + const agc009Tasks = taskResultsForAGC001OnwardsProvider.filter( + (task) => task.contest_id === 'agc009', + ); + const headerIds = provider.getHeaderIdsForTask(agc009Tasks as TaskResults); + + expect(agc009Tasks).toHaveLength(5); + expect(headerIds).toEqual(['A', 'B', 'C', 'D', 'E']); + }); + + test('expects to handle 6-problem contest pattern (AGC001, AGC002)', () => { + const provider = new AGC001OnwardsProvider(ContestType.AGC); + const agc001Tasks = taskResultsForAGC001OnwardsProvider.filter( + (task) => task.contest_id === 'agc001', + ); + const headerIds = provider.getHeaderIdsForTask(agc001Tasks as TaskResults); + + expect(agc001Tasks).toHaveLength(6); + expect(headerIds).toEqual(['A', 'B', 'C', 'D', 'E', 'F']); + }); + + test('expects to handle 7-problem contest pattern with F2 (AGC028)', () => { + const provider = new AGC001OnwardsProvider(ContestType.AGC); + const agc028Tasks = taskResultsForAGC001OnwardsProvider.filter( + (task) => task.contest_id === 'agc028', + ); + const headerIds = provider.getHeaderIdsForTask(agc028Tasks as TaskResults); + + expect(agc028Tasks).toHaveLength(7); + expect(headerIds).toEqual(['A', 'B', 'C', 'D', 'E', 'F', 'F2']); + }); + + test('expects to maintain proper alphabetical/numeric sort order', () => { + const provider = new AGC001OnwardsProvider(ContestType.AGC); + const unsortedTasks = [ + { contest_id: 'agc001', task_id: 'agc001_f', task_table_index: 'F' }, + { contest_id: 'agc001', task_id: 'agc001_c', task_table_index: 'C' }, + { contest_id: 'agc001', task_id: 'agc001_a', task_table_index: 'A' }, + { contest_id: 'agc001', task_id: 'agc001_f2', task_table_index: 'F2' }, + ]; + const headerIds = provider.getHeaderIdsForTask(unsortedTasks as TaskResults); + + expect(headerIds).toEqual(['A', 'C', 'F', 'F2']); + }); + + test('expects to handle task results with different contest types', () => { + const provider = new AGC001OnwardsProvider(ContestType.AGC); + const mixedTasks = [ + { contest_id: 'agc050', task_id: 'agc050_a', task_table_index: 'A' }, + { contest_id: 'abc123', task_id: 'abc123_a', task_table_index: 'A' }, + { contest_id: 'agc001', task_id: 'agc001_a', task_table_index: 'A' }, + { contest_id: 'typical90', task_id: 'typical90_a', task_table_index: '001' }, + ]; + const filtered = provider.filter(mixedTasks as TaskResults); + + expect(filtered).toHaveLength(2); + expect(filtered.every((task) => task.contest_id.startsWith('agc'))).toBe(true); + }); + + test('expects to exclude contests below AGC001', () => { + const provider = new AGC001OnwardsProvider(ContestType.AGC); + const mixedTasks = [ + { contest_id: 'agc000', task_id: 'agc000_a', task_table_index: 'A' }, + { contest_id: 'agc001', task_id: 'agc001_a', task_table_index: 'A' }, + { contest_id: 'agc002', task_id: 'agc002_a', task_table_index: 'A' }, + { contest_id: 'agc999', task_id: 'agc999_a', task_table_index: 'A' }, + ]; + const filtered = provider.filter(mixedTasks as TaskResults); + + expect(filtered).toHaveLength(3); + expect( + filtered.every((task) => { + const round = getContestRound(task.contest_id); + return round >= 1 && round <= 999; + }), + ).toBe(true); + }); + + test('expects to handle empty task results', () => { + const provider = new AGC001OnwardsProvider(ContestType.AGC); + const filtered = provider.filter([] as TaskResults); + + expect(filtered).toEqual([] as TaskResults); + }); + }); + describe('Typical90 provider', () => { test('expects to filter tasks to include only typical90 contest', () => { const provider = new Typical90Provider(ContestType.TYPICAL90); diff --git a/src/test/lib/utils/test_cases/contest_table_provider.ts b/src/test/lib/utils/test_cases/contest_table_provider.ts index 900f30f47..2748177b7 100644 --- a/src/test/lib/utils/test_cases/contest_table_provider.ts +++ b/src/test/lib/utils/test_cases/contest_table_provider.ts @@ -445,3 +445,100 @@ export const taskResultsForARC104OnwardsProvider: TaskResults = [ arc208_d, arc208_e, ]; + +// AGC 001 Onwards: Multiple problem patterns (4, 5, 6, 7 problems) +// AGC001 (6 problems: A, B, C, D, E, F - standard pattern) +const [agc001_a, agc001_b, agc001_c, agc001_d, agc001_e, agc001_f] = createContestTasks('agc001', [ + { taskTableIndex: 'A', statusName: AC }, + { taskTableIndex: 'B', statusName: AC }, + { taskTableIndex: 'C', statusName: AC_WITH_EDITORIAL }, + { taskTableIndex: 'D', statusName: TRYING }, + { taskTableIndex: 'E', statusName: PENDING }, + { taskTableIndex: 'F', statusName: PENDING }, +]); + +// AGC002 (6 problems: A, B, C, D, E, F - standard pattern) +const [agc002_a, agc002_b, agc002_c, agc002_d, agc002_e, agc002_f] = createContestTasks('agc002', [ + { taskTableIndex: 'A', statusName: AC }, + { taskTableIndex: 'B', statusName: AC }, + { taskTableIndex: 'C', statusName: AC }, + { taskTableIndex: 'D', statusName: AC_WITH_EDITORIAL }, + { taskTableIndex: 'E', statusName: TRYING }, + { taskTableIndex: 'F', statusName: PENDING }, +]); + +// AGC009 (5 problems: A, B, C, D, E - historical exception) +const [agc009_a, agc009_b, agc009_c, agc009_d, agc009_e] = createContestTasks('agc009', [ + { taskTableIndex: 'A', statusName: AC }, + { taskTableIndex: 'B', statusName: AC }, + { taskTableIndex: 'C', statusName: AC_WITH_EDITORIAL }, + { taskTableIndex: 'D', statusName: TRYING }, + { taskTableIndex: 'E', statusName: PENDING }, +]); + +// AGC028 (7 problems: A, B, C, D, E, F, F2 - only 7-problem pattern in 2025) +const [agc028_a, agc028_b, agc028_c, agc028_d, agc028_e, agc028_f, agc028_f2] = createContestTasks( + 'agc028', + [ + { taskTableIndex: 'A', statusName: AC }, + { taskTableIndex: 'B', statusName: AC }, + { taskTableIndex: 'C', statusName: AC }, + { taskTableIndex: 'D', statusName: AC_WITH_EDITORIAL }, + { taskTableIndex: 'E', statusName: TRYING }, + { taskTableIndex: 'F', statusName: PENDING }, + { taskTableIndex: 'F2', statusName: PENDING }, + ], +); + +// AGC073 (4 problems: A, B, C, D - only 4-problem pattern in 2025) +const [agc073_a, agc073_b, agc073_c, agc073_d] = createContestTasks('agc073', [ + { taskTableIndex: 'A', statusName: AC }, + { taskTableIndex: 'B', statusName: AC_WITH_EDITORIAL }, + { taskTableIndex: 'C', statusName: TRYING }, + { taskTableIndex: 'D', statusName: PENDING }, +]); + +// AGC074 (5 problems: A, B, C, D, E - standard after AGC067) +const [agc074_a, agc074_b, agc074_c, agc074_d, agc074_e] = createContestTasks('agc074', [ + { taskTableIndex: 'A', statusName: AC }, + { taskTableIndex: 'B', statusName: AC }, + { taskTableIndex: 'C', statusName: AC_WITH_EDITORIAL }, + { taskTableIndex: 'D', statusName: TRYING }, + { taskTableIndex: 'E', statusName: PENDING }, +]); + +export const taskResultsForAGC001OnwardsProvider: TaskResults = [ + agc001_a, + agc001_b, + agc001_c, + agc001_d, + agc001_e, + agc001_f, + agc002_a, + agc002_b, + agc002_c, + agc002_d, + agc002_e, + agc002_f, + agc009_a, + agc009_b, + agc009_c, + agc009_d, + agc009_e, + agc028_a, + agc028_b, + agc028_c, + agc028_d, + agc028_e, + agc028_f, + agc028_f2, + agc073_a, + agc073_b, + agc073_c, + agc073_d, + agc074_a, + agc074_b, + agc074_c, + agc074_d, + agc074_e, +]; From 1d94eaebe576e5a9400ff7164cc12538f12a95bb Mon Sep 17 00:00:00 2001 From: Kato Hiroki Date: Mon, 24 Nov 2025 21:46:15 +0000 Subject: [PATCH 2/3] chore(docs): Add language specifiers to code blocks for better readability (#2837) --- .../add_tests_for_contest_table_provider/plan.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/dev-notes/2025-11-19/add_tests_for_contest_table_provider/plan.md b/docs/dev-notes/2025-11-19/add_tests_for_contest_table_provider/plan.md index 877cd9663..d6989aa9e 100644 --- a/docs/dev-notes/2025-11-19/add_tests_for_contest_table_provider/plan.md +++ b/docs/dev-notes/2025-11-19/add_tests_for_contest_table_provider/plan.md @@ -77,7 +77,7 @@ ### パターン1: 4問コンテスト(AGC073) -``` +```text task_table_index: A, B, C, D ``` @@ -87,7 +87,7 @@ task_table_index: A, B, C, D ### パターン2: 5問コンテスト(AGC009、AGC067~) -``` +```text task_table_index: A, B, C, D, E ``` @@ -97,7 +97,7 @@ task_table_index: A, B, C, D, E ### パターン3: 6問コンテスト(標準) -``` +```text task_table_index: A, B, C, D, E, F ``` @@ -107,7 +107,7 @@ task_table_index: A, B, C, D, E, F ### パターン4: 7問コンテスト(AGC028のみ) -``` +```text task_table_index: A, B, C, D, E, F, F2 ``` From d5af02d61dd54b9b6f8f17ae28b635c2b98e4488 Mon Sep 17 00:00:00 2001 From: Kato Hiroki Date: Tue, 25 Nov 2025 12:45:58 +0000 Subject: [PATCH 3/3] feat(task): Add table for AGC 001 onwards (#2837) --- prisma/tasks.ts | 231 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 231 insertions(+) diff --git a/prisma/tasks.ts b/prisma/tasks.ts index 7ebb2ae55..fe1e5bab6 100755 --- a/prisma/tasks.ts +++ b/prisma/tasks.ts @@ -4596,6 +4596,237 @@ export const tasks = [ title: 'E. Connected?', grade: 'D2', }, + { + id: 'agc074_e', + contest_id: 'agc074', + problem_index: 'E', + name: 'Delete AB', + title: 'E. Delete AB', + }, + { + id: 'agc074_d', + contest_id: 'agc074', + problem_index: 'D', + name: 'Valid Output for DSU Problems', + title: 'D. Valid Output for DSU Problems', + }, + { + id: 'agc074_c', + contest_id: 'agc074', + problem_index: 'C', + name: 'PORALIS', + title: 'C. PORALIS', + }, + { + id: 'agc074_b', + contest_id: 'agc074', + problem_index: 'B', + name: 'Swap if Equal Length and Sum', + title: 'B. Swap if Equal Length and Sum', + }, + { + id: 'agc074_a', + contest_id: 'agc074', + problem_index: 'A', + name: 'Communicate Topological Order', + title: 'A. Communicate Topological Order', + }, + { + id: 'agc073_d', + contest_id: 'agc073', + problem_index: 'D', + name: 'Four Jewels', + title: 'D. Four Jewels', + }, + { + id: 'agc073_c', + contest_id: 'agc073', + problem_index: 'C', + name: 'Product of Max of Sum of Subtree', + title: 'C. Product of Max of Sum of Subtree', + }, + { + id: 'agc073_b', + contest_id: 'agc073', + problem_index: 'B', + name: 'Cyclic Jump', + title: 'B. Cyclic Jump', + }, + { + id: 'agc073_a', + contest_id: 'agc073', + problem_index: 'A', + name: 'Chords and Checkered', + title: 'A. Chords and Checkered', + }, + { + id: 'agc028_f2', + contest_id: 'agc028', + problem_index: 'F2', + name: 'Reachable Cells', + title: 'F2. Reachable Cells', + }, + { + id: 'agc028_f', + contest_id: 'agc028', + problem_index: 'F', + name: 'Reachable Cells', + title: 'F. Reachable Cells', + }, + { + id: 'agc028_e', + contest_id: 'agc028', + problem_index: 'E', + name: 'High Elements', + title: 'E. High Elements', + }, + { + id: 'agc028_d', + contest_id: 'agc028', + problem_index: 'D', + name: 'Chords', + title: 'D. Chords', + }, + { + id: 'agc028_c', + contest_id: 'agc028', + problem_index: 'C', + name: 'Min Cost Cycle', + title: 'C. Min Cost Cycle', + }, + { + id: 'agc028_b', + contest_id: 'agc028', + problem_index: 'B', + name: 'Removing Blocks', + title: 'B. Removing Blocks', + }, + { + id: 'agc028_a', + contest_id: 'agc028', + problem_index: 'A', + name: 'Two Abbreviations', + title: 'A. Two Abbreviations', + }, + { + id: 'agc009_e', + contest_id: 'agc009', + problem_index: 'E', + name: 'Eternal Average', + title: 'E. Eternal Average', + }, + { + id: 'agc009_d', + contest_id: 'agc009', + problem_index: 'D', + name: 'Uninity', + title: 'D. Uninity', + }, + { + id: 'agc009_c', + contest_id: 'agc009', + problem_index: 'C', + name: 'Division into Two', + title: 'C. Division into Two', + }, + { + id: 'agc009_b', + contest_id: 'agc009', + problem_index: 'B', + name: 'Tournament', + title: 'B. Tournament', + }, + { + id: 'agc009_a', + contest_id: 'agc009', + problem_index: 'A', + name: 'Multiple Array', + title: 'A. Multiple Array', + }, + { + id: 'agc002_f', + contest_id: 'agc002', + problem_index: 'F', + name: 'Leftmost Ball', + title: 'F. Leftmost Ball', + }, + { + id: 'agc002_e', + contest_id: 'agc002', + problem_index: 'E', + name: 'Candy Piles', + title: 'E. Candy Piles', + }, + { + id: 'agc002_d', + contest_id: 'agc002', + problem_index: 'D', + name: 'Stamp Rally', + title: 'D. Stamp Rally', + }, + { + id: 'agc002_c', + contest_id: 'agc002', + problem_index: 'C', + name: 'Knot Puzzle', + title: 'C. Knot Puzzle', + }, + { + id: 'agc002_b', + contest_id: 'agc002', + problem_index: 'B', + name: 'Box and Ball', + title: 'B. Box and Ball', + }, + { + id: 'agc002_a', + contest_id: 'agc002', + problem_index: 'A', + name: 'Range Product', + title: 'A. Range Product', + }, + { + id: 'agc001_f', + contest_id: 'agc001', + problem_index: 'F', + name: 'Wide Swap', + title: 'F. Wide Swap', + }, + { + id: 'agc001_e', + contest_id: 'agc001', + problem_index: 'E', + name: 'BBQ Hard', + title: 'E. BBQ Hard', + }, + { + id: 'agc001_d', + contest_id: 'agc001', + problem_index: 'D', + name: 'Arrays and Palindrome', + title: 'D. Arrays and Palindrome', + }, + { + id: 'agc001_c', + contest_id: 'agc001', + problem_index: 'C', + name: 'Shorten Diameter', + title: 'C. Shorten Diameter', + }, + { + id: 'agc001_b', + contest_id: 'agc001', + problem_index: 'B', + name: 'Mysterious Light', + title: 'B. Mysterious Light', + }, + { + id: 'agc001_a', + contest_id: 'agc001', + problem_index: 'A', + name: 'BBQ Easy', + title: 'A. BBQ Easy', + }, { id: 'practice_1', contest_id: 'abs',