Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .claude/skills/add-contest-table-provider/instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Step 0 (seed check) is already done. Confirm the following before touching code:
**Pattern 1 additional:**

- Numeric range: start and end (open-ended if no upper bound)?
- Splitting an existing "Onwards" provider? Rename to `{Start}To{End}Provider`; the new special-edition provider uses a fixed section string (e.g. `super(contestType, '0100')`) to coexist under the same ContestType.
- Shared problems with another contest (e.g. ARC–ABC overlap)? Which contest_ids appear in both?
- Round label format (e.g. `ABC 042`)?

Expand Down Expand Up @@ -93,6 +94,7 @@ Step 0 (seed check) is already done. Confirm the following before touching code:

- [ ] Add test cases covering range boundaries and at least one mid-range value
- [ ] If shared problems exist: add a test case with mixed contest_ids to confirm exclusion
- [ ] If splitting an existing range: add a combined-fixture test confirming the upper bound excludes the adjacent range's contest_id: `[...fixtureA, ...fixtureB]` → `filter` → assert `some(task => task.contest_id === 'out-of-range') === false`
- [ ] `pnpm test:unit <providers.test.ts>` — **expect RED**
- [ ] Implement Provider using `parseContestRound()` range check
- [ ] `pnpm test:unit <providers.test.ts>` — **expect GREEN**
Expand Down Expand Up @@ -124,13 +126,13 @@ Step 0 (seed check) is already done. Confirm the following before touching code:
- [ ] Update `contest_table_provider_groups.test.ts`:
- New group name string, `buttonLabel`, `ariaLabel`
- `getSize()` incremented to reflect the new provider count
- Add `getProvider(ContestType.XXX)` assertion
- Add `getProvider(ContestType.XXX)` assertion; for section-based providers use `getProvider(ContestType.XXX, 'section')`
- Add import of new Provider class
- [ ] `pnpm test:unit contest_table_provider_groups.test.ts` — **expect RED**
- [ ] Update `contest_table_provider_groups.ts`:
- Add import of new Provider class
- Update group name string, `buttonLabel`, `ariaLabel`
- Add `new XXXProvider(ContestType.XXX)` to `addProviders()`
- Add `new XXXProvider(ContestType.XXX)` to `addProvider()` chain — **`addProvider` call order = display order (first = top)**
- [ ] `pnpm test:unit src/features/tasks/utils/contest-table/` — **expect GREEN**

---
Expand Down
57 changes: 41 additions & 16 deletions docs/guides/how-to-add-contest-table-provider.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ protected setFilterCondition(): (taskResult: TaskResult) => boolean {
- `super(contestType, String(year))` でセクションを年度文字列にし、プロバイダキーを `AOJ_ICPC::2025` のように一意化
- 年度範囲定数(`OLDEST_YEAR` / `LATEST_YEAR`)をモジュールトップで `export` し、tests でも参照できるようにする
- グループ登録時は最新年から古い年へ降順ループし、テーブルを新しい順に並べる
- **固定セクションバリアント**: N 回インスタンス化ではなく、同一グループ内で同一 ContestType のプロバイダーを 2 種類共存させる場合も同じ仕組みが使える。一方に固定文字列を渡し(例: `super(contestType, '0100')` → key `AWC::0100`)、もう一方はセクションなし(key `AWC`)とすることで衝突を回避する

**実装例**:

Expand Down Expand Up @@ -312,18 +313,18 @@ class TessokuBookSectionProvider extends TessokuBookProvider {

### 範囲フィルタ型

| コンテスト | 範囲 | フォーマット | セクション | ラベル | 特有の注意 |
| ----------- | -------- | ------------ | ---------- | ------ | ------------- |
| ABC 001-041 | 001~041 | 001, 041 | A~D | あり | 旧形式 |
| ABC 042-125 | 042~125 | 042, 125 | A~D | あり | 共有問題(ARC) |
| ABC 126-211 | 126~211 | 126, 211 | A~F | あり | 6問制 |
| ABC 212-318 | 212~318 | 212, 318 | A~Ex/H | あり | 8問制 |
| ABC 319- | 319~ | 319 | A~G | あり | 標準形式 |
| ARC 001-057 | 001~057 | 001, 057 | A~D | あり | 旧形式 |
| ARC 058-103 | 058~103 | 058, 103 | C~F | あり | 共有問題(ABC) |
| ARC 104- | 104~ | 104 | 4~6問 | あり | - |
| AGC 001- | 001~ | 001 | 4~7問 | あり | - |
| AWC 0001- | 0001~ | 0001 | A~E | あり | - |
| コンテスト | 範囲 | フォーマット | セクション | ラベル | 特有の注意 |
| ------------- | ---------- | ------------ | ---------- | ------ | ------------- |
| ABC 001-041 | 001~041 | 001, 041 | A~D | あり | 旧形式 |
| ABC 042-125 | 042~125 | 042, 125 | A~D | あり | 共有問題(ARC) |
| ABC 126-211 | 126~211 | 126, 211 | A~F | あり | 6問制 |
| ABC 212-318 | 212~318 | 212, 318 | A~Ex/H | あり | 8問制 |
| ABC 319- | 319~ | 319 | A~G | あり | 標準形式 |
| ARC 001-057 | 001~057 | 001, 057 | A~D | あり | 旧形式 |
| ARC 058-103 | 058~103 | 058, 103 | C~F | あり | 共有問題(ABC) |
| ARC 104- | 104~ | 104 | 4~6問 | あり | - |
| AGC 001- | 001~ | 001 | 4~7問 | あり | - |
| AWC 0001-0099 | 0001~0099 | 0001 | A~E | あり | - |

### 単一ソース型

Expand All @@ -336,9 +337,11 @@ class TessokuBookSectionProvider extends TessokuBookProvider {
| ACL_PRACTICE | `'practice2'` | 12問 | A~L |
| ACL_BEGINNER\* | `'abl'` | 6問 | A~F |
| ACL_CONTEST1\* | `'acl1'` | 6問 | A~F |
| AWC0100† | `'awc0100'` | 15問 | A~O |

\*注: ACL_PRACTICE、ACL_BEGINNER、ACL_CONTEST1 は `Acl` グループの下で 3 つのコンテストが統一管理されています。
\*\*注: EDPC・TDPC・NDPC・FPS 24 は `dps` グループ下で 4 つのコンテストが統一管理されています。
†注: ContestType.AWC を再利用し、section `'0100'` で同グループ内の AWC0001To0099Provider と共存(provider key = `AWC::0100`)。`getProvider(ContestType.AWC, '0100')` で取得。

### 複合ソース型

Expand Down Expand Up @@ -367,7 +370,14 @@ class TessokuBookSectionProvider extends TessokuBookProvider {

### パターン固有テスト

- **範囲フィルタ型**: 範囲境界値テスト、共有問題の有無確認
- **範囲フィルタ型**: 範囲境界値テスト、共有問題の有無確認。既存プロバイダーを上限付きに分割した場合は、隣接するもう一方のフィクスチャを結合して上限境界の除外を確認する:

```typescript
const combined = [...taskResultsForAWC0001To0099Provider, ...taskResultsForAWC0100Provider];
const filtered = provider.filter(combined);
expect(filtered.some((task) => task.contest_id === 'awc0100')).toBe(false);
```
Comment thread
coderabbitai[bot] marked this conversation as resolved.

- **複合ソース型**: 複数 contest_id 混在テスト、セクション分割ロジック

### Vitest テスト例
Expand Down Expand Up @@ -496,7 +506,7 @@ export const taskResultsForNewProvider: TaskResults = [

---

## よくあるミス Top 5
## よくあるミス

### 1. **getDisplayConfig() での属性漏れ**

Expand Down Expand Up @@ -598,6 +608,21 @@ describe('CustomProvider with unique config', () => {

---

### 6. `addProvider` の順序を意識しない

**問題**: 同一グループ内の複数プロバイダーは `addProvider` の呼び出し順が画面上の表示順(先 = 上)になる。

**解決策**: 上に表示したいプロバイダーを先に `addProvider` する。

```typescript
// AWC0100 を AWC0001-0099 の上に表示する場合
group
.addProvider(new AWC0100Provider(ContestType.AWC)) // 上
.addProvider(new AWC0001To0099Provider(ContestType.AWC)); // 下
```

---

## 実装完了後

### ドキュメント更新チェックリスト
Expand All @@ -622,7 +647,7 @@ describe('CustomProvider with unique config', () => {
- [#2837](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/2837) - AGC001OnwardsProvider
- [#2838](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/2838) - ABC001~041 & ARC001~057
- [#2840](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/2840)、[#3108](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/3108) - ABCLikeProvider
- [#3153](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/3153) - AWC0001OnwardsProvider
- [#3153](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/3153) - AWC0001To0099Provider(旧: AWC0001OnwardsProvider)、AWC0100Provider
- [#2776](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/2776) - TessokuBookProvider
- [#2785](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/2785) - MathAndAlgorithmProvider
- [#2797](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/2797) - FPS24Provider
Expand All @@ -638,4 +663,4 @@ describe('CustomProvider with unique config', () => {

---

**最終更新**: 2026-05-10
**最終更新**: 2026-06-24
105 changes: 105 additions & 0 deletions prisma/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8354,6 +8354,111 @@ export const tasks = [
name: '4/N',
title: 'C. 4/N',
},
{
id: 'awc0100_a',
contest_id: 'awc0100',
problem_index: 'A',
name: 'Task A',
title: 'A. Task A',
},
{
id: 'awc0100_b',
contest_id: 'awc0100',
problem_index: 'B',
name: 'Task B',
title: 'B. Task B',
},
{
id: 'awc0100_c',
contest_id: 'awc0100',
problem_index: 'C',
name: 'Task C',
title: 'C. Task C',
},
{
id: 'awc0100_d',
contest_id: 'awc0100',
problem_index: 'D',
name: 'Task D',
title: 'D. Task D',
},
{
id: 'awc0100_e',
contest_id: 'awc0100',
problem_index: 'E',
name: 'Task E',
title: 'E. Task E',
},
{
id: 'awc0100_f',
contest_id: 'awc0100',
problem_index: 'F',
name: 'Task F',
title: 'F. Task F',
},
{
id: 'awc0100_g',
contest_id: 'awc0100',
problem_index: 'G',
name: 'Task G',
title: 'G. Task G',
},
{
id: 'awc0100_h',
contest_id: 'awc0100',
problem_index: 'H',
name: 'Task H',
title: 'H. Task H',
},
{
id: 'awc0100_i',
contest_id: 'awc0100',
problem_index: 'I',
name: 'Task I',
title: 'I. Task I',
},
{
id: 'awc0100_j',
contest_id: 'awc0100',
problem_index: 'J',
name: 'Task J',
title: 'J. Task J',
},
{
id: 'awc0100_k',
contest_id: 'awc0100',
problem_index: 'K',
name: 'Task K',
title: 'K. Task K',
},
{
id: 'awc0100_l',
contest_id: 'awc0100',
problem_index: 'L',
name: 'Task L',
title: 'L. Task L',
},
{
id: 'awc0100_m',
contest_id: 'awc0100',
problem_index: 'M',
name: 'Task M',
title: 'M. Task M',
},
{
id: 'awc0100_n',
contest_id: 'awc0100',
problem_index: 'N',
name: 'Task N',
title: 'N. Task N',
},
{
id: 'awc0100_o',
contest_id: 'awc0100',
problem_index: 'O',
name: 'Task O',
title: 'O. Task O',
},
{
id: 'awc0005_e',
contest_id: 'awc0005',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -836,7 +836,7 @@ export const taskResultsForACLProvider: TaskResults = [
acl1_f,
];

// AWC 0001 onwards: 5 tasks (A, B, C, D, E)
// AWC 0001-0099: 5 tasks (A, B, C, D, E)
// Multiple contests to test range filtering
const [awc0001_a, awc0001_b, awc0001_c, awc0001_d, awc0001_e] = createContestTasks('awc0001', [
{ taskTableIndex: 'A', statusName: AC },
Expand All @@ -862,7 +862,7 @@ const [awc0099_a, awc0099_b, awc0099_c, awc0099_d, awc0099_e] = createContestTas
{ taskTableIndex: 'E', statusName: AC_WITH_EDITORIAL },
]);

export const taskResultsForAWC0001OnwardsProvider: TaskResults = [
export const taskResultsForAWC0001To0099Provider: TaskResults = [
awc0001_a,
awc0001_b,
awc0001_c,
Expand All @@ -879,3 +879,56 @@ export const taskResultsForAWC0001OnwardsProvider: TaskResults = [
awc0099_d,
awc0099_e,
];

// AWC0100 (special edition): 15 tasks (A-O)
const [
awc0100_a,
awc0100_b,
awc0100_c,
awc0100_d,
awc0100_e,
awc0100_f,
awc0100_g,
awc0100_h,
awc0100_i,
awc0100_j,
awc0100_k,
awc0100_l,
awc0100_m,
awc0100_n,
awc0100_o,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
] = createContestTasks('awc0100', [
{ taskTableIndex: 'A', statusName: AC },
{ taskTableIndex: 'B', statusName: AC },
{ taskTableIndex: 'C', statusName: AC_WITH_EDITORIAL },
{ taskTableIndex: 'D', statusName: AC },
{ taskTableIndex: 'E', statusName: TRYING },
{ taskTableIndex: 'F', statusName: AC_WITH_EDITORIAL },
{ taskTableIndex: 'G', statusName: TRYING },
{ taskTableIndex: 'H', statusName: PENDING },
{ taskTableIndex: 'I', statusName: PENDING },
{ taskTableIndex: 'J', statusName: PENDING },
{ taskTableIndex: 'K', statusName: PENDING },
{ taskTableIndex: 'L', statusName: PENDING },
{ taskTableIndex: 'M', statusName: PENDING },
{ taskTableIndex: 'N', statusName: PENDING },
{ taskTableIndex: 'O', statusName: PENDING },
]);

export const taskResultsForAWC0100Provider: TaskResults = [
awc0100_a,
awc0100_b,
awc0100_c,
awc0100_d,
awc0100_e,
awc0100_f,
awc0100_g,
awc0100_h,
awc0100_i,
awc0100_j,
awc0100_k,
awc0100_l,
awc0100_m,
awc0100_n,
awc0100_o,
];
Loading
Loading