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
64 changes: 55 additions & 9 deletions docs/guides/how-to-add-contest-table-provider.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,48 @@

---

## 0. 実装前確認フェーズ

新しい Provider を実装する前に、必ず以下の事項を確認してください。

### 事前確認チェックリスト

- [ ] **ContestType の選択**
- 新規追加が必要か? → `src/lib/types/contest.ts` を確認
- 既存の `ContestType` で対応できないか? → 「各コンテスト種別の特有仕様」を参照
- 判断基準: 複数の異なる contest_id を統一表示するなら「複合型」の可能性

- [ ] **データ存在確認**
- `prisma/tasks.ts` に該当 `contest_id` のタスクが存在するか確認
- 複合型の場合は `prisma/contest_task_pairs.ts` で共有問題を確認

- [ ] **実装パターン判定**
- **パターン1(範囲フィルタ型)**: ABC 001~041、ARC 058~103 など → 数値範囲でフィルタ
- **パターン2(単一ソース型)**: EDPC、TDPC、ACL_PRACTICE など → 単一 contest_id のみ
- **パターン3(複合ソース型)**: ABS、ABC-Like など → 複数 contest_id を統一表示
- 対応セクション: [実装パターン](#実装パターン)

- [ ] **ガイドの実装例の確認**
- 判定したパターンの実装例を確認してテンプレート理解
- モック設定時に必要な `classifyContest()` の戻り値を確認

### 記入例

```markdown
**新規 Provider 名**: ACLBeginnerProvider

事前確認結果:

- ContestType: ABC_LIKE(既存流用)
- contest_id: abl(prisma/tasks.ts に 6 つのタスク存在確認)
- パターン: パターン2(単一ソース型)
- テンプレート: EDPC と同一構造

→ 実装フェーズ開始
```

---

## Test Driven Development (TDD) 設計ガイド

新しい Provider を実装する際は、**テストファースト** のアプローチを推奨します。
Expand Down Expand Up @@ -207,12 +249,16 @@ class TessokuBookSectionProvider extends TessokuBookProvider {

### 単一ソース型

| コンテスト | contest_id | セクション | フォーマット |
| ------------ | ------------- | ---------- | ------------ |
| EDPC | `'dp'` | 26問 | A~Z |
| TDPC | `'tdpc'` | 26問 | A~Z |
| FPS_24 | `'fps-24'` | 24問 | A~X |
| ACL_PRACTICE | `'practice2'` | 12問 | A~L |
| コンテスト | contest_id | セクション | フォーマット |
| -------------- | ------------- | ---------- | ------------ |
| EDPC | `'dp'` | 26問 | A~Z |
| TDPC | `'tdpc'` | 26問 | A~Z |
| FPS_24 | `'fps-24'` | 24問 | A~X |
| ACL_PRACTICE | `'practice2'` | 12問 | A~L |
| ACL_BEGINNER\* | `'abl'` | 6問 | A~F |
| ACL_CONTEST1\* | `'acl1'` | 6問 | A~F |

\*注: ACL_PRACTICE、ACL_BEGINNER、ACL_CONTEST1 は `Acl` グループの下で 3 つのコンテストが統一管理されています。

### 複合ソース型

Expand Down Expand Up @@ -419,11 +465,11 @@ describe('CustomProvider with unique config', () => {
- [#2835](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/2835) - ARC104OnwardsProvider
- [#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) - ABCLikeProvider
- [#2840](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/2840)、[#3108](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/3108) - ABCLikeProvider
- [#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
- [#2920](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/2920) - ACLPracticeProvider
- [#2920](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/2920)、[#3120](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/3120) - ACLPracticeProvider、ACLBeginnerProvider、ACLProvider

### 実装ファイル

Expand All @@ -433,4 +479,4 @@ describe('CustomProvider with unique config', () => {

---

**最終更新**: 2026-01-26
**最終更新**: 2026-02-01
42 changes: 42 additions & 0 deletions prisma/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7976,6 +7976,48 @@ export const tasks = [
name: 'Accepted...?',
title: 'A. Accepted...?',
},
{
id: 'acl1_f',
contest_id: 'acl1',
problem_index: 'F',
name: 'Center Rearranging',
title: 'F. Center Rearranging',
},
{
id: 'acl1_e',
contest_id: 'acl1',
problem_index: 'E',
name: 'Shuffle Window',
title: 'E. Shuffle Window',
},
{
id: 'acl1_d',
contest_id: 'acl1',
problem_index: 'D',
name: 'Keep Distances',
title: 'D. Keep Distances',
},
{
id: 'acl1_c',
contest_id: 'acl1',
problem_index: 'C',
name: 'Moving Pieces',
title: 'C. Moving Pieces',
},
{
id: 'acl1_b',
contest_id: 'acl1',
problem_index: 'B',
name: 'Sum is Multiple',
title: 'B. Sum is Multiple',
},
{
id: 'acl1_a',
contest_id: 'acl1',
problem_index: 'A',
name: 'Reachable Towns',
title: 'A. Reachable Towns',
},
{
id: 'tenka1_2019_f',
contest_id: 'tenka1-2019',
Expand Down
1 change: 1 addition & 0 deletions src/lib/utils/contest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ const ARC_LIKE: ContestPrefix = {
keyence2021: 'キーエンス プログラミング コンテスト 2021',
'jsc2019-qual': '第一回日本最強プログラマー学生選手権-予選-',
'nikkei2019-qual': '全国統一プログラミング王決定戦予選',
acl1: 'ACL Contest 1',
} as const;
const arcLikePrefixes = new Set(getContestPrefixes(ARC_LIKE));

Expand Down
83 changes: 76 additions & 7 deletions src/lib/utils/contest_table_provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -796,6 +796,70 @@ export class ACLPracticeProvider extends ContestTableProviderBase {
}
}

export class ACLBeginnerProvider extends ContestTableProviderBase {
protected setFilterCondition(): (taskResult: TaskResult) => boolean {
return (taskResult: TaskResult) => {
if (classifyContest(taskResult.contest_id) !== this.contestType) {
return false;
}
return taskResult.contest_id === 'abl';
};
}

getMetadata(): ContestTableMetaData {
return {
title: 'ACL Beginner Contest',
abbreviationName: 'ABL',
};
}

getDisplayConfig(): ContestTableDisplayConfig {
return {
isShownHeader: false,
isShownRoundLabel: false,
roundLabelWidth: '',
tableBodyCellsWidth: 'w-1/2 xs:w-1/3 sm:w-1/4 md:w-1/5 lg:w-1/6 px-1 py-2',
isShownTaskIndex: true,
};
}

getContestRoundLabel(_contestId: string): string {
return '';
}
}

export class ACLProvider extends ContestTableProviderBase {
protected setFilterCondition(): (taskResult: TaskResult) => boolean {
return (taskResult: TaskResult) => {
if (classifyContest(taskResult.contest_id) !== this.contestType) {
return false;
}
return taskResult.contest_id === 'acl1';
};
}

getMetadata(): ContestTableMetaData {
return {
title: 'ACL Contest 1',
abbreviationName: 'ACL',
};
}

getDisplayConfig(): ContestTableDisplayConfig {
return {
isShownHeader: false,
isShownRoundLabel: false,
roundLabelWidth: '',
tableBodyCellsWidth: 'w-1/2 xs:w-1/3 sm:w-1/4 md:w-1/5 lg:w-1/6 px-1 py-2',
isShownTaskIndex: true,
};
}

getContestRoundLabel(_contestId: string): string {
return '';
}
}

const regexForJoiFirstQualRound = /^(joi)(\d{4})(yo1)(a|b|c)$/i;

export class JOIFirstQualRoundProvider extends ContestTableProviderBase {
Expand Down Expand Up @@ -1209,13 +1273,18 @@ export const prepareContestProviderPresets = () => {
),

/**
* Single group for ACL Practice Contest
* Group for AtCoder Library Contests (ACL)
* Includes ACL Practice, ACL Beginner, and ACL Contest 1
*/
AclPractice: () =>
new ContestTableProviderGroup(`AtCoder Library Practice Contest`, {
buttonLabel: 'ACL Practice',
ariaLabel: 'Filter ACL Practice Contest',
}).addProvider(new ACLPracticeProvider(ContestType.ACL_PRACTICE)),
Acl: () =>
new ContestTableProviderGroup(`AtCoder Library Contests`, {
buttonLabel: 'ACL',
ariaLabel: 'Filter ACL Contests',
}).addProviders(
new ACLPracticeProvider(ContestType.ACL_PRACTICE),
new ACLBeginnerProvider(ContestType.ABC_LIKE),
new ACLProvider(ContestType.ARC_LIKE),
),

JOIFirstQualRound: () =>
new ContestTableProviderGroup(`JOI 一次予選`, {
Expand Down Expand Up @@ -1251,7 +1320,7 @@ export const contestTableProviderGroups = {
tessokuBook: prepareContestProviderPresets().TessokuBook(),
mathAndAlgorithm: prepareContestProviderPresets().MathAndAlgorithm(),
dps: prepareContestProviderPresets().dps(), // Dynamic Programming (DP) Contests
aclPractice: prepareContestProviderPresets().AclPractice(),
acl: prepareContestProviderPresets().Acl(),
joiFirstQualRound: prepareContestProviderPresets().JOIFirstQualRound(),
joiSecondQualAndSemiFinalRound: prepareContestProviderPresets().JOISecondQualAndSemiFinalRound(),
};
Expand Down
Loading
Loading