Skip to content

Commit 3b5bc58

Browse files
authored
Merge pull request #2786 from AtCoder-NoviSteps/#2785
feat: Add math and algo book to contest table (#2785)
2 parents 1eb8d9b + 3685fb1 commit 3b5bc58

File tree

7 files changed

+977
-0
lines changed

7 files changed

+977
-0
lines changed
Lines changed: 351 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,351 @@
1+
# MathAndAlgorithmProvider テスト追加計画
2+
3+
**作成日**: 2025-11-03
4+
5+
**対象ブランチ**: #2785
6+
7+
**優先度**: High
8+
9+
---
10+
11+
## 参照ドキュメント
12+
13+
テストの書き方・スタイル・ベストプラクティスについては、以下を参照:
14+
15+
📖 [`docs/dev-notes/2025-11-01/add_and_refactoring_tests_for_contest_table_provider/plan.md`](../../2025-11-01/add_and_refactoring_tests_for_contest_table_provider/plan.md)
16+
17+
---
18+
19+
## 1. 概要
20+
21+
### 背景
22+
23+
`MathAndAlgorithmProvider``TessokuBookProvider` と同じ構造で、複数のコンテストの問題を統合した問題集を提供します。
24+
25+
- **セクション範囲**: 001 ~ 104(一部欠損)
26+
- **フォーマット**: 3桁数字(0 padding)
27+
- **複数ソース対応**: 異なる `task_id`(問題集のリンク)
28+
29+
### 目的
30+
31+
TessokuBook テストと同等の粒度で、MathAndAlgorithmProvider の単体テスト 11 個を追加。
32+
33+
---
34+
35+
## 2. 仕様要件
36+
37+
| 項目 | 仕様 | 備考 |
38+
| ------------------ | --------------------------- | ------------------------ |
39+
| **セクション範囲** | 001 ~ 104 | 一部欠損あり(原典準拠) |
40+
| **ソート順序** | 昇順(001 → 102 → ... 104) | 必須 |
41+
| **フォーマット** | 3桁数字(0 padding) | 例: 001, 028, 102 |
42+
| **複数ソース対応** | 異なる problem_id | DB 一意制約で保証 |
43+
44+
---
45+
46+
## 3. テストケース(11件)
47+
48+
### テスト1: フィルタリング
49+
50+
```typescript
51+
test('expects to filter tasks to include only math-and-algorithm contest', () => {
52+
const provider = new MathAndAlgorithmProvider(ContestType.MATH_AND_ALGORITHM);
53+
const mixedTasks = [
54+
{ contest_id: 'abc123', task_id: 'abc123_a', task_table_index: 'A' },
55+
{ contest_id: 'math-and-algorithm', task_id: 'math_and_algorithm_a', task_table_index: '001' },
56+
{ contest_id: 'math-and-algorithm', task_id: 'typical90_o', task_table_index: '101' },
57+
{ contest_id: 'math-and-algorithm', task_id: 'arc117_c', task_table_index: '102' },
58+
];
59+
const filtered = provider.filter(mixedTasks);
60+
61+
expect(filtered?.every((task) => task.contest_id === 'math-and-algorithm')).toBe(true);
62+
expect(filtered).not.toContainEqual(expect.objectContaining({ contest_id: 'abc123' }));
63+
});
64+
```
65+
66+
---
67+
68+
### テスト2: メタデータ取得
69+
70+
```typescript
71+
test('expects to get correct metadata', () => {
72+
const provider = new MathAndAlgorithmProvider(ContestType.MATH_AND_ALGORITHM);
73+
const metadata = provider.getMetadata();
74+
75+
expect(metadata.title).toBe('アルゴリズムと数学');
76+
expect(metadata.abbreviationName).toBe('math-and-algorithm');
77+
});
78+
```
79+
80+
---
81+
82+
### テスト3: 表示設定
83+
84+
```typescript
85+
test('expects to get correct display configuration', () => {
86+
const provider = new MathAndAlgorithmProvider(ContestType.MATH_AND_ALGORITHM);
87+
const displayConfig = provider.getDisplayConfig();
88+
89+
expect(displayConfig.isShownHeader).toBe(false);
90+
expect(displayConfig.isShownRoundLabel).toBe(false);
91+
expect(displayConfig.roundLabelWidth).toBe('');
92+
expect(displayConfig.tableBodyCellsWidth).toBe(
93+
'w-1/2 xs:w-1/3 sm:w-1/4 md:w-1/5 lg:w-1/6 2xl:w-1/7 px-1 py-2',
94+
);
95+
expect(displayConfig.isShownTaskIndex).toBe(true);
96+
});
97+
```
98+
99+
---
100+
101+
### テスト4: ラウンドラベルフォーマット
102+
103+
```typescript
104+
test('expects to format contest round label correctly', () => {
105+
const provider = new MathAndAlgorithmProvider(ContestType.MATH_AND_ALGORITHM);
106+
const label = provider.getContestRoundLabel('math-and-algorithm');
107+
108+
expect(label).toBe('');
109+
});
110+
```
111+
112+
---
113+
114+
### テスト5: テーブル生成(複数ソース対応)
115+
116+
```typescript
117+
test('expects to generate correct table structure with mixed problem sources', () => {
118+
const provider = new MathAndAlgorithmProvider(ContestType.MATH_AND_ALGORITHM);
119+
const tasks = [
120+
{ contest_id: 'math-and-algorithm', task_id: 'math_and_algorithm_a', task_table_index: '001' },
121+
{ contest_id: 'math-and-algorithm', task_id: 'dp_a', task_table_index: '028' },
122+
{ contest_id: 'math-and-algorithm', task_id: 'abc168_c', task_table_index: '036' },
123+
{ contest_id: 'math-and-algorithm', task_id: 'arc117_c', task_table_index: '102' },
124+
];
125+
const table = provider.generateTable(tasks);
126+
127+
expect(table).toHaveProperty('math-and-algorithm');
128+
expect(table['math-and-algorithm']).toHaveProperty('028');
129+
expect(table['math-and-algorithm']['028']).toEqual(expect.objectContaining({ task_id: 'dp_a' }));
130+
});
131+
```
132+
133+
---
134+
135+
### テスト6: ラウンド ID 取得
136+
137+
```typescript
138+
test('expects to get contest round IDs correctly', () => {
139+
const provider = new MathAndAlgorithmProvider(ContestType.MATH_AND_ALGORITHM);
140+
const tasks = [
141+
{ contest_id: 'math-and-algorithm', task_id: 'arc117_c', task_table_index: '102' },
142+
{ contest_id: 'math-and-algorithm', task_id: 'typical90_o', task_table_index: '101' },
143+
];
144+
const roundIds = provider.getContestRoundIds(tasks);
145+
146+
expect(roundIds).toEqual(['math-and-algorithm']);
147+
});
148+
```
149+
150+
---
151+
152+
### テスト7: ヘッダー ID 取得(昇順・複数ソース混在)
153+
154+
```typescript
155+
test('expects to get header IDs for tasks correctly in ascending order', () => {
156+
const provider = new MathAndAlgorithmProvider(ContestType.MATH_AND_ALGORITHM);
157+
const tasks = [
158+
{ contest_id: 'math-and-algorithm', task_id: 'math_and_algorithm_a', task_table_index: '001' },
159+
{ contest_id: 'math-and-algorithm', task_id: 'arc117_c', task_table_index: '102' },
160+
{ contest_id: 'math-and-algorithm', task_id: 'dp_a', task_table_index: '028' },
161+
{ contest_id: 'math-and-algorithm', task_id: 'abc168_c', task_table_index: '036' },
162+
];
163+
const headerIds = provider.getHeaderIdsForTask(tasks);
164+
165+
expect(headerIds).toEqual(['001', '028', '036', '102']);
166+
});
167+
```
168+
169+
---
170+
171+
### テスト8: ソート順序の厳密性(数字ソート)
172+
173+
```typescript
174+
test('expects to maintain proper sort order with numeric indices', () => {
175+
const provider = new MathAndAlgorithmProvider(ContestType.MATH_AND_ALGORITHM);
176+
const tasks = [
177+
{ contest_id: 'math-and-algorithm', task_id: 'typical90_bz', task_table_index: '045' },
178+
{ contest_id: 'math-and-algorithm', task_id: 'abc168_c', task_table_index: '036' },
179+
];
180+
const headerIds = provider.getHeaderIdsForTask(tasks);
181+
182+
// 036 < 045 の順序を厳密に検証
183+
expect(headerIds).toEqual(['036', '045']);
184+
});
185+
```
186+
187+
---
188+
189+
### テスト9: セクション範囲検証
190+
191+
```typescript
192+
test('expects to handle section boundaries correctly (001-104)', () => {
193+
const provider = new MathAndAlgorithmProvider(ContestType.MATH_AND_ALGORITHM);
194+
const tasks = [
195+
{ contest_id: 'math-and-algorithm', task_id: 'math_and_algorithm_a', task_table_index: '001' },
196+
{ contest_id: 'math-and-algorithm', task_id: 'math_and_algorithm_bx', task_table_index: '104' },
197+
];
198+
const headerIds = provider.getHeaderIdsForTask(tasks);
199+
200+
expect(headerIds).toEqual(['001', '104']);
201+
});
202+
```
203+
204+
---
205+
206+
### テスト10: 空入力処理
207+
208+
```typescript
209+
test('expects to handle empty task results', () => {
210+
const provider = new MathAndAlgorithmProvider(ContestType.MATH_AND_ALGORITHM);
211+
const filtered = provider.filter([]);
212+
213+
expect(filtered).toEqual([]);
214+
});
215+
```
216+
217+
---
218+
219+
### テスト11: 混合コンテストタイプの排除
220+
221+
```typescript
222+
test('expects to handle task results with different contest types', () => {
223+
const provider = new MathAndAlgorithmProvider(ContestType.MATH_AND_ALGORITHM);
224+
const mixedTasks = [
225+
{ contest_id: 'math-and-algorithm', task_id: 'math_and_algorithm_a', task_table_index: '001' },
226+
{ contest_id: 'math-and-algorithm', task_id: 'arc117_c', task_table_index: '102' },
227+
{ contest_id: 'typical90', task_id: 'typical90_a', task_table_index: 'A' },
228+
];
229+
const filtered = provider.filter(mixedTasks);
230+
231+
expect(filtered).toHaveLength(2);
232+
expect(filtered?.every((task) => task.contest_id === 'math-and-algorithm')).toBe(true);
233+
});
234+
```
235+
236+
---
237+
238+
## 4. モックデータ
239+
240+
追加先: `src/test/lib/utils/test_cases/contest_table_provider.ts`
241+
242+
```typescript
243+
export const taskResultsForMathAndAlgorithmProvider: TaskResults = [
244+
{
245+
contest_id: 'math-and-algorithm',
246+
task_id: 'dp_a',
247+
task_table_index: '028',
248+
},
249+
{
250+
contest_id: 'math-and-algorithm',
251+
task_id: 'abc168_c',
252+
task_table_index: '036',
253+
},
254+
{
255+
contest_id: 'math-and-algorithm',
256+
task_id: 'typical90_bz',
257+
task_table_index: '045',
258+
},
259+
{
260+
contest_id: 'math-and-algorithm',
261+
task_id: 'abc007_3',
262+
task_table_index: '046',
263+
},
264+
{
265+
contest_id: 'math-and-algorithm',
266+
task_id: 'arc084_b',
267+
task_table_index: '048',
268+
},
269+
{
270+
contest_id: 'math-and-algorithm',
271+
task_id: 'abc145_d',
272+
task_table_index: '052',
273+
},
274+
{
275+
contest_id: 'math-and-algorithm',
276+
task_id: 'abc172_d',
277+
task_table_index: '042',
278+
},
279+
{
280+
contest_id: 'math-and-algorithm',
281+
task_id: 'typical90_j',
282+
task_table_index: '095',
283+
},
284+
{
285+
contest_id: 'math-and-algorithm',
286+
task_id: 'typical90_o',
287+
task_table_index: '101',
288+
},
289+
{
290+
contest_id: 'math-and-algorithm',
291+
task_id: 'arc117_c',
292+
task_table_index: '102',
293+
},
294+
];
295+
```
296+
297+
**出典**: [`prisma/contest_task_pairs.ts`](../../../../prisma/contest_task_pairs.ts) 行 14 ~ 52
298+
299+
---
300+
301+
## 5. 実装手順
302+
303+
**ステップ1**: モックデータを `src/test/lib/utils/test_cases/contest_table_provider.ts` に追加
304+
305+
**ステップ2**: 上記 11 個のテストを `src/test/lib/utils/contest_table_provider.test.ts` に追加
306+
307+
**ステップ3**: テスト実行・検証
308+
309+
```bash
310+
pnpm test:unit src/test/lib/utils/contest_table_provider.test.ts
311+
```
312+
313+
**ステップ4**: Lint チェック
314+
315+
```bash
316+
pnpm lint src/test/lib/utils/contest_table_provider.test.ts
317+
```
318+
319+
---
320+
321+
## 6. 注意点
322+
323+
詳細は参照ドキュメント「教訓統合」セクションを参照。特に以下を確認:
324+
325+
- **ソート順序**: 文字列の辞書順ソート(`'028' < '036' < '045'`
326+
- **複数ソース混在**: `problem_id` が異なる複雑なテストケース(テスト5・11)
327+
- **パラメータ化テスト**: TessokuBook との共通パターン活用可能(参考ドキュメント フェーズ3)
328+
329+
---
330+
331+
## 7. 実装結果・教訓
332+
333+
### ✅ 実装完了
334+
335+
**実施時間**: 13.4 秒(テスト実行含む)
336+
337+
**実装内容**:
338+
339+
1. モックデータ追加: 10 個のサンプルタスク(`contest_table_provider.ts`
340+
2. テストケース実装: 11 個の単体テスト
341+
3. モック拡張: `classifyContest``math-and-algorithm` サポートを追加
342+
343+
### 📚 得られた教訓
344+
345+
1. **コンテストタイプのモック更新**:新規プロバイダー追加時、`vi.mock()` に新しいコンテストタイプを追加する必要あり。参照ドキュメント(2025-11-01)では言及されていなかった重要なポイント
346+
347+
2. **テストの再利用性**:TessokuBook と MathAndAlgorithmProvider は構造同一のため、テストテンプレートを完全流用可能。共有パターン化の価値が確認できた
348+
349+
3. **ソート順序の自動確認**:文字列ソート(昇順)が正確に機能するため、インデックス形式の統一(3桁数字)が重要
350+
351+
4. **ファイルフォーマット**:Prettier による自動フォーマットで一部ファイルが修正されたため、実装後の linting 実行は必須

0 commit comments

Comments
 (0)