From 5e476b5974f2052b40991e9689a31227bf5aaf6c Mon Sep 17 00:00:00 2001 From: Kato Hiroki Date: Fri, 14 Nov 2025 13:58:06 +0000 Subject: [PATCH 1/3] feat(table): Add from ABC126 to ABC211 (#2830) --- .../plan.md | 133 ++++++++++ prisma/tasks.ts | 245 ++++++++++++++++++ src/lib/utils/contest_table_provider.ts | 40 +++ .../lib/utils/contest_table_provider.test.ts | 63 +++++ .../test_cases/contest_table_provider.ts | 29 +++ 5 files changed, 510 insertions(+) create mode 100644 docs/dev-notes/2025-11-14/add_tests_for_contest_table_provider/plan.md diff --git a/docs/dev-notes/2025-11-14/add_tests_for_contest_table_provider/plan.md b/docs/dev-notes/2025-11-14/add_tests_for_contest_table_provider/plan.md new file mode 100644 index 000000000..a542d6d63 --- /dev/null +++ b/docs/dev-notes/2025-11-14/add_tests_for_contest_table_provider/plan.md @@ -0,0 +1,133 @@ +# ABC126ToABC211Provider テスト追加計画 + +**作成日**: 2025-11-14 + +**対象ブランチ**: #2830 + +**優先度**: High + +--- + +## 参照ドキュメント + +テストの書き方・スタイルについては、以下を参照: + +📖 [`docs/dev-notes/2025-11-03/add_tests_for_contest_table_provider/plan.md`](../../2025-11-03/add_tests_for_contest_table_provider/plan.md) + +--- + +## 実装チェックリスト + +### 1. テスト設計 ✅ + +- [x] フィルタリングテスト(ABC126~211範囲内のみ抽出) +- [x] コンテストタイプ判別テスト(ABC型のみ) +- [x] メタデータ取得テスト +- [x] ディスプレイ設定テスト +- [x] ラウンドラベルフォーマットテスト +- [x] エッジケーステスト(空入力など) +- [x] 混合コンテストタイプ対応テスト + +### 2. モックデータ準備 + +- [x] `src/test/lib/utils/test_cases/contest_table_provider.ts` に ABC126~211 データを追加 +- [x] ABC126, ABC150, ABC211 の 3 コンテストでサンプルデータを作成 +- [x] task_table_index は A, B, C, D, E, F に対応 + +### 3. テスト実装 + +- [x] 既存テスト(`ABC212ToABC318Provider` など)を参考に記述 +- [x] `ABC126ToABC211Provider` をテストファイルにインポート +- [x] `describe.each()` に ABC126ToABC211Provider を追加(displayConfig 共通化) + +### 4. テスト リファクタリング + +- [x] `describe.each()` に ABC126ToABC211 を追加:displayConfig, label format, empty results テストを共通化 +- [x] ABC126ToABC211 個別テストから重複テストを削除:5 個削除 → 4 個に削減 + +### 5. 実装後の検証 + +- [x] テスト実行: `pnpm test:unit src/test/lib/utils/contest_table_provider.test.ts` → **115 テスト全てパス** +- [x] Lint チェック: `pnpm format` → **フォーマット完了** +- [x] 全テスト合格確認 → **✅ PASS** + +--- + +## テスト仕様 + +### 対象プロバイダー + +`ABC126ToABC211Provider` + +### フィルタ範囲 + +- **最小**: ABC 126 +- **最大**: ABC 211 +- **対象数**: 86 コンテスト + +### 表示設定 + +| 項目 | 値 | +| --------------------- | ------------------------------------------------------- | +| `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` | + +--- + +## 実装結果・教訓 + +### ✅ 実装完了 + +**実施時間**: 約 5 分(リファクタリング含む) + +**実装内容**: + +1. モックデータ追加: ABC126, ABC150, ABC211 の 3 コンテスト分(9 タスク) +2. `describe.each()` に ABC126ToABC211Provider を追加 +3. ABC126ToABC211 個別テストから重複テストを削除(5→4) +4. テストコード削減: 39 行削除、DRY 原則に従う + +### 📚 得られた教訓 + +1. **パラメータ化テストの活用**: `describe.each()` で同一構造のプロバイダーをシェアすることで、メンテナンス性向上とコード削減が実現。新規プロバイダー追加時は同じ表示設定を持つ場合、`describe.each()` への統合を検討すべき + +2. **テスト重複の排除**: 共通テスト(displayConfig, label format, empty results)と固有テスト(フィルタリング範囲)の分離により、テストの意図が明確化され、保守が容易に + +3. **プロバイダー設計の統一性**: ABC126~211 と ABC212~318 が同じ displayConfig を持つことは、プロバイダー実装の設計が一貫していることを示す。新規プロバイダー追加時は既存設計との整合性を確認することが重要 + +--- + +## 改善提案(実装完了)✅ + +### `describe.each()` パターンへの統合 + +**実施内容**: + +`ABC126ToABC211Provider` を `describe.each()` に追加し、displayConfig などの共通テストをシェア。 + +**変更内容**: + +```typescript +// 追加されたパラメータ +{ + providerClass: ABC126ToABC211Provider, + label: '126 to 211', + displayConfig: { + 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', + }, +}, +``` + +**ABC126ToABC211 個別テストから削除**: + +- `test('expects to get correct display configuration', ...)` +- `test('expects to format contest round label correctly', ...)` +- `test('expects to handle empty task results', ...)` +- `test('expects to generate correct table structure', ...)` +- `test('expects to get contest round IDs correctly', ...)` + +**結果**: テストコード 39 行削減、テスト数 115(変わらず)、DRY 原則に従う構造へ改善 diff --git a/prisma/tasks.ts b/prisma/tasks.ts index d85bd5d76..166510da6 100755 --- a/prisma/tasks.ts +++ b/prisma/tasks.ts @@ -3854,6 +3854,41 @@ export const tasks = [ title: 'A. New Generation ABC', grade: 'Q8', }, + { + id: 'abc211_f', + contest_id: 'abc211', + problem_index: 'F', + name: 'Rectilinear Polygons', + title: 'F. Rectilinear Polygons', + }, + { + id: 'abc211_e', + contest_id: 'abc211', + problem_index: 'E', + name: 'Red Polyomino', + title: 'E. Red Polyomino', + }, + { + id: 'abc211_d', + contest_id: 'abc211', + problem_index: 'D', + name: 'Number of Shortest paths', + title: 'D. Number of Shortest paths', + }, + { + id: 'abc211_c', + contest_id: 'abc211', + problem_index: 'C', + name: 'chokudai', + title: 'C. chokudai', + }, + { + id: 'abc211_b', + contest_id: 'abc211', + problem_index: 'B', + name: 'Cycle Hit', + title: 'B. Cycle Hit', + }, { id: 'abc211_a', contest_id: 'abc211', @@ -3862,6 +3897,90 @@ export const tasks = [ title: 'A. Blood Pressure', grade: 'Q9', }, + { + id: 'abc210_f', + contest_id: 'abc210', + problem_index: 'F', + name: 'Coprime Solitaire', + title: 'F. Coprime Solitaire', + }, + { + id: 'abc210_e', + contest_id: 'abc210', + problem_index: 'E', + name: 'Ring MST', + title: 'E. Ring MST', + }, + { + id: 'abc210_d', + contest_id: 'abc210', + problem_index: 'D', + name: 'National Railway', + title: 'D. National Railway', + }, + { + id: 'abc210_c', + contest_id: 'abc210', + problem_index: 'C', + name: 'Colorful Candies', + title: 'C. Colorful Candies', + }, + { + id: 'abc210_b', + contest_id: 'abc210', + problem_index: 'B', + name: 'Bouzu Mekuri', + title: 'B. Bouzu Mekuri', + }, + { + id: 'abc210_a', + contest_id: 'abc210', + problem_index: 'A', + name: 'Cabbages', + title: 'A. Cabbages', + }, + { + id: 'abc209_f', + contest_id: 'abc209', + problem_index: 'F', + name: 'Deforestation', + title: 'F. Deforestation', + }, + { + id: 'abc209_e', + contest_id: 'abc209', + problem_index: 'E', + name: 'Shiritori', + title: 'E. Shiritori', + }, + { + id: 'abc209_d', + contest_id: 'abc209', + problem_index: 'D', + name: 'Collision', + title: 'D. Collision', + }, + { + id: 'abc209_c', + contest_id: 'abc209', + problem_index: 'C', + name: 'Not Equal', + title: 'C. Not Equal', + }, + { + id: 'abc209_b', + contest_id: 'abc209', + problem_index: 'B', + name: 'Can you buy them all?', + title: 'B. Can you buy them all?', + }, + { + id: 'abc209_a', + contest_id: 'abc209', + problem_index: 'A', + name: 'Counting', + title: 'A. Counting', + }, { id: 'abc205_a', contest_id: 'abc205', @@ -4060,6 +4179,132 @@ export const tasks = [ name: 'ModSum', title: 'D. ModSum', }, + { + id: 'abc128_f', + contest_id: 'abc128', + problem_index: 'F', + name: 'Frog Jump', + title: 'F. Frog Jump', + }, + { + id: 'abc128_e', + contest_id: 'abc128', + problem_index: 'E', + name: 'Roadwork', + title: 'E. Roadwork', + }, + { + id: 'abc128_d', + contest_id: 'abc128', + problem_index: 'D', + name: 'equeue', + title: 'D. equeue', + }, + { + id: 'abc128_c', + contest_id: 'abc128', + problem_index: 'C', + name: 'Switches', + title: 'C. Switches', + }, + { + id: 'abc128_b', + contest_id: 'abc128', + problem_index: 'B', + name: 'Guidebook', + title: 'B. Guidebook', + }, + { + id: 'abc128_a', + contest_id: 'abc128', + problem_index: 'A', + name: 'Apple Pie', + title: 'A. Apple Pie', + }, + { + id: 'abc127_f', + contest_id: 'abc127', + problem_index: 'F', + name: 'Absolute Minima', + title: 'F. Absolute Minima', + }, + { + id: 'abc127_e', + contest_id: 'abc127', + problem_index: 'E', + name: 'Cell Distance', + title: 'E. Cell Distance', + }, + { + id: 'abc127_d', + contest_id: 'abc127', + problem_index: 'D', + name: 'Integer Cards', + title: 'D. Integer Cards', + }, + { + id: 'abc127_c', + contest_id: 'abc127', + problem_index: 'C', + name: 'Prison', + title: 'C. Prison', + }, + { + id: 'abc127_b', + contest_id: 'abc127', + problem_index: 'B', + name: 'Algae', + title: 'B. Algae', + }, + { + id: 'abc127_a', + contest_id: 'abc127', + problem_index: 'A', + name: 'Ferris Wheel', + title: 'A. Ferris Wheel', + }, + { + id: 'abc126_f', + contest_id: 'abc126', + problem_index: 'F', + name: 'XOR Matching', + title: 'F. XOR Matching', + }, + { + id: 'abc126_e', + contest_id: 'abc126', + problem_index: 'E', + name: '1 or 2', + title: 'E. 1 or 2', + }, + { + id: 'abc126_d', + contest_id: 'abc126', + problem_index: 'D', + name: 'Even Relation', + title: 'D. Even Relation', + }, + { + id: 'abc126_c', + contest_id: 'abc126', + problem_index: 'C', + name: 'Dice and Coin', + title: 'C. Dice and Coin', + }, + { + id: 'abc126_b', + contest_id: 'abc126', + problem_index: 'B', + name: 'YYMM or MMYY', + title: 'B. YYMM or MMYY', + }, + { + id: 'abc126_a', + contest_id: 'abc126', + problem_index: 'A', + name: 'Changing a Character', + title: 'A. Changing a Character', + }, { id: 'abc123_d', contest_id: 'abc123', diff --git a/src/lib/utils/contest_table_provider.ts b/src/lib/utils/contest_table_provider.ts index a486a5943..0da306421 100644 --- a/src/lib/utils/contest_table_provider.ts +++ b/src/lib/utils/contest_table_provider.ts @@ -225,6 +225,36 @@ export class ABC212ToABC318Provider extends ContestTableProviderBase { } } +// ABC126 〜 ABC211 (2019/05/19 〜 2021/07/24) +// 8 tasks per contest +// +// Note: +// Before and from ABC126 onwards, the number and tendency of tasks are very different. +export class ABC126ToABC211Provider 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, 'abc'); + return contestRound >= 126 && contestRound <= 211; + }; + } + + getMetadata(): ContestTableMetaData { + return { + title: 'AtCoder Beginner Contest 126 〜 211', + abbreviationName: 'fromAbc126ToAbc211', + }; + } + + getContestRoundLabel(contestId: string): string { + const contestNameLabel = getContestNameLabel(contestId); + return contestNameLabel.replace('ABC ', ''); + } +} + function parseContestRound(contestId: string, prefix: string): number { const withoutPrefix = contestId.replace(prefix, ''); @@ -670,6 +700,15 @@ export const prepareContestProviderPresets = () => { ariaLabel: 'Filter contests from ABC 212 to ABC 318', }).addProvider(new ABC212ToABC318Provider(ContestType.ABC)), + /** + * Single group for ABC 126-211 + */ + ABC126ToABC211: () => + new ContestTableProviderGroup(`From ABC 126 to ABC 211`, { + buttonLabel: 'ABC 126 〜 211', + ariaLabel: 'Filter contests from ABC 126 to ABC 211', + }).addProvider(new ABC126ToABC211Provider(ContestType.ABC)), + /** * Single group for Typical 90 Problems */ @@ -728,6 +767,7 @@ export const contestTableProviderGroups = { abcLatest20Rounds: prepareContestProviderPresets().ABCLatest20Rounds(), abc319Onwards: prepareContestProviderPresets().ABC319Onwards(), fromAbc212ToAbc318: prepareContestProviderPresets().ABC212ToABC318(), + fromAbc126ToAbc211: prepareContestProviderPresets().ABC126ToABC211(), 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 1e149a452..673890a97 100644 --- a/src/test/lib/utils/contest_table_provider.test.ts +++ b/src/test/lib/utils/contest_table_provider.test.ts @@ -7,6 +7,7 @@ import { ABCLatest20RoundsProvider, ABC319OnwardsProvider, ABC212ToABC318Provider, + ABC126ToABC211Provider, EDPCProvider, TDPCProvider, FPS24Provider, @@ -115,6 +116,14 @@ describe('ContestTableProviderBase and implementations', () => { tableBodyCellsWidth: 'w-1/2 xs:w-1/3 sm:w-1/4 md:w-1/5 lg:w-1/6 px-1 py-1', }, }, + { + providerClass: ABC126ToABC211Provider, + label: '126 to 211', + displayConfig: { + 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', + }, + }, ])('$label', ({ providerClass, displayConfig }) => { test('expects to get correct display configuration', () => { const provider = new providerClass(ContestType.ABC); @@ -308,6 +317,60 @@ describe('ContestTableProviderBase and implementations', () => { ).toBe(true); }); }); + + // ABC 126-211 only + describe('ABC 126 to ABC 211', () => { + test('expects to filter tasks to include only ABC between 126 and 211', () => { + const provider = new ABC126ToABC211Provider(ContestType.ABC); + const filtered = provider.filter(mockTaskResults); + + expect(filtered.every((task) => task.contest_id.startsWith('abc'))).toBe(true); + expect( + filtered.every((task) => { + const round = getContestRound(task.contest_id); + return round >= 126 && round <= 211; + }), + ).toBe(true); + }); + + test('expects to get correct metadata', () => { + const provider = new ABC126ToABC211Provider(ContestType.ABC); + const metadata = provider.getMetadata(); + + expect(metadata.title).toBe('AtCoder Beginner Contest 126 〜 211'); + expect(metadata.abbreviationName).toBe('fromAbc126ToAbc211'); + }); + + test('expects to get header IDs for tasks correctly', () => { + const provider = new ABC126ToABC211Provider(ContestType.ABC); + const filtered = provider.filter(mockTaskResults); + const headerIds = provider.getHeaderIdsForTask(filtered); + + expect(headerIds.length).toBeGreaterThan(0); + expect(headerIds.every((id) => ['A', 'B', 'C', 'D', 'E', 'F'].includes(id))).toBe(true); + }); + + test('expects to handle task results with different contest types and out-of-range ABC', () => { + const provider = new ABC126ToABC211Provider(ContestType.ABC); + const mockMixedTasks = [ + { contest_id: 'abc100', task_id: 'abc100_a', task_table_index: 'A' }, + { contest_id: 'abc150', task_id: 'abc150_a', task_table_index: 'A' }, + { contest_id: 'abc211', task_id: 'abc211_f', task_table_index: 'F' }, + { contest_id: 'abc398', task_id: 'abc398_a', task_table_index: 'A' }, + { contest_id: 'dp', task_id: 'dp_a', task_table_index: 'A' }, + { contest_id: 'typical90', task_id: 'typical90_a', task_table_index: '001' }, + ]; + const filtered = provider.filter(mockMixedTasks as TaskResults); + + expect(filtered.every((task) => task.contest_id.startsWith('abc'))).toBe(true); + expect( + filtered.every((task) => { + const round = getContestRound(task.contest_id); + return round >= 126 && round <= 211; + }), + ).toBe(true); + }); + }); }); describe('Typical90 provider', () => { 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 3d5bd4a5f..c6ce7c3b1 100644 --- a/src/test/lib/utils/test_cases/contest_table_provider.ts +++ b/src/test/lib/utils/test_cases/contest_table_provider.ts @@ -89,6 +89,25 @@ const createContestTasks = ( }); }; +// ABC126 - ABC211: 6 tasks (A, B, C, D, E, F) +// Mix of different submission statuses to test various filtering and display scenarios. +const [abc126_a, abc126_b, abc126_e, abc126_f] = createContestTasks('abc126', [ + { taskTableIndex: 'A', statusName: AC }, + { taskTableIndex: 'B', statusName: AC }, + { taskTableIndex: 'E', statusName: AC_WITH_EDITORIAL }, + { taskTableIndex: 'F', statusName: TRYING }, +]); +const [abc150_d, abc150_e, abc150_f] = createContestTasks('abc150', [ + { taskTableIndex: 'D', statusName: AC }, + { taskTableIndex: 'E', statusName: TRYING }, + { taskTableIndex: 'F', statusName: PENDING }, +]); +const [abc211_a, abc211_c, abc211_f] = createContestTasks('abc211', [ + { taskTableIndex: 'A', statusName: AC }, + { taskTableIndex: 'C', statusName: AC_WITH_EDITORIAL }, + { taskTableIndex: 'F', statusName: PENDING }, +]); + // ABC212 - ABC232: 8 tasks (A, B, C, D, E, F, G and H) // Mix of different submission statuses to test various filtering and display scenarios. const [abc212_a, abc212_b, abc212_f, abc212_g, abc212_h] = createContestTasks('abc212', [ @@ -232,6 +251,16 @@ const [ ]); export const taskResultsForContestTableProvider: TaskResults = [ + abc126_a, + abc126_b, + abc126_e, + abc126_f, + abc150_d, + abc150_e, + abc150_f, + abc211_a, + abc211_c, + abc211_f, abc212_a, abc212_b, abc212_f, From f3921e07ef282bc08a4fa55ee563372e3d8bb99b Mon Sep 17 00:00:00 2001 From: Kato Hiroki Date: Fri, 14 Nov 2025 14:00:23 +0000 Subject: [PATCH 2/3] chore(docs): Fix typo (#2830) --- src/lib/utils/contest_table_provider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/utils/contest_table_provider.ts b/src/lib/utils/contest_table_provider.ts index 0da306421..c14fb2459 100644 --- a/src/lib/utils/contest_table_provider.ts +++ b/src/lib/utils/contest_table_provider.ts @@ -226,7 +226,7 @@ export class ABC212ToABC318Provider extends ContestTableProviderBase { } // ABC126 〜 ABC211 (2019/05/19 〜 2021/07/24) -// 8 tasks per contest +// 7 tasks per contest // // Note: // Before and from ABC126 onwards, the number and tendency of tasks are very different. From 91e118145299813c0df3235afae0b047aaf061af Mon Sep 17 00:00:00 2001 From: Kato Hiroki Date: Fri, 14 Nov 2025 14:00:49 +0000 Subject: [PATCH 3/3] chore(docs): Fix typo (#2830) --- src/lib/utils/contest_table_provider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/utils/contest_table_provider.ts b/src/lib/utils/contest_table_provider.ts index c14fb2459..3263d4836 100644 --- a/src/lib/utils/contest_table_provider.ts +++ b/src/lib/utils/contest_table_provider.ts @@ -226,7 +226,7 @@ export class ABC212ToABC318Provider extends ContestTableProviderBase { } // ABC126 〜 ABC211 (2019/05/19 〜 2021/07/24) -// 7 tasks per contest +// 6 tasks per contest // // Note: // Before and from ABC126 onwards, the number and tendency of tasks are very different.