diff --git a/.claude/skills/add-contest-table-provider/SKILL.md b/.claude/skills/add-contest-table-provider/SKILL.md new file mode 100644 index 000000000..5df8a11d9 --- /dev/null +++ b/.claude/skills/add-contest-table-provider/SKILL.md @@ -0,0 +1,13 @@ +--- +name: add-contest-table-provider +description: Add a new ContestType and ContestTableProvider across 5 layers using TDD. Covers all 3 patterns. Asks targeted questions to gather pattern-specific requirements before touching code. +argument-hint: ' ' +--- + +Add a new contest table provider for: $ARGUMENTS + +> When in doubt at any step, use AskUserQuestion before proceeding. + +0. **Seed check** — grep `prisma/tasks.ts` for the contest_id(s); report count + task_ids; if absent or incomplete, ask the user to add missing rows (reference: `https://kenkoooo.com/atcoder/resources/problems.json`) +1. **Gather requirements** — infer the implementation pattern; confirm per [instructions.md §Requirements](instructions.md) +2. **Implement** — follow [instructions.md](instructions.md) for the confirmed pattern across 5 layers (TDD) diff --git a/.claude/skills/add-contest-table-provider/instructions.md b/.claude/skills/add-contest-table-provider/instructions.md new file mode 100644 index 000000000..28bdce16f --- /dev/null +++ b/.claude/skills/add-contest-table-provider/instructions.md @@ -0,0 +1,123 @@ +# Add Contest Table Provider — Implementation Checklist + +Reference: `docs/guides/how-to-add-contest-table-provider.md` + +--- + +## Requirements gathering + +Step 0 (seed check) is already done. Confirm the following before touching code: + +**All patterns:** + +- Which pattern? (State your inference from the data, ask to confirm) + - Pattern 1: numeric range filter (e.g. ABC 001–041) + - Pattern 2: single fixed contest_id (e.g. NDPC, TDPC, FPS_24) + - Pattern 3: multiple contest_ids unified in one table (e.g. ABS, ABC-Like) +- Nearest neighbor ContestType for insertion order in `contestTypePriorities`? +- New group or merge into existing? If new: group name / `buttonLabel` / `ariaLabel`? + +**Pattern 1 additional:** + +- Numeric range: start and end (open-ended if no upper bound)? +- Shared problems with another contest (e.g. ARC–ABC overlap)? Which contest_ids appear in both? +- Round label format (e.g. `ABC 042`)? + +**Pattern 3 additional:** + +- Show the full contest_id list found in `prisma/tasks.ts` — any missing or to exclude? +- Does `prisma/contest_task_pairs.ts` need updating (shared task_ids across contests)? +- task_table_index format: numeric (`001–`) or alphabetic (`A–`)? +- Section splits needed? If yes: split key and section names? + +--- + +## Layer 1 — Prisma schema + +- [ ] Add `XXX // Full Contest Name` to `prisma/schema.prisma` ContestType enum (after nearest neighbor) +- [ ] `pnpm exec prisma generate` — non-interactive env; `migrate dev` requires interactive shell +- [ ] `pnpm check` — expect a type error in `src/lib/types/contest.ts` (confirms client regenerated) + +## Layer 2 — TypeScript ContestType constant + +- [ ] Add `XXX: 'XXX', // Full Contest Name` to `ContestType` in `src/lib/types/contest.ts` (same position as schema) +- [ ] `pnpm check` — error should be gone + +## Layer 3 — Contest utilities (TDD) + +### Write tests first + +- [ ] Add export to `src/test/lib/utils/test_cases/contest_type.ts` (after nearest neighbor) +- [ ] Add export to `src/test/lib/utils/test_cases/contest_name_labels.ts` (after nearest neighbor) +- [ ] Add three `describe('when contest_id is xxx')` blocks to `src/test/lib/utils/contest.test.ts`: + - under `classify contest` + - under `get contest priority` + - under `get contest name label` +- [ ] `pnpm test:unit src/test/lib/utils/contest.test.ts` — **expect RED** + +### Implement + +- [ ] Add `classifyContest` branch after nearest neighbor's branch in `src/lib/utils/contest.ts` +- [ ] Insert `[ContestType.XXX, N]` into `contestTypePriorities` after nearest neighbor + - All entries after the insertion point shift by +1 + - **Update the JSDoc numeric ranges** — do NOT rename or split the existing four categories + (Educational / Contests for genius / Special contests / External platforms) + - **Search `src/test/lib/utils/task.test.ts` for hardcoded priority-diff expected values** + and decrement by 1 for every ContestType that shifted +- [ ] Add `getContestNameLabel` branch after nearest neighbor's branch +- [ ] `pnpm test:unit src/test/lib/utils/contest.test.ts` — **expect GREEN** + +--- + +## Layer 4 — Provider class (TDD) + +### Pattern 2: single source + +- [ ] Add entry to `describe.each` array in `dp_providers.test.ts` (or the appropriate `*_providers.test.ts`) +- [ ] Add import of new Provider class +- [ ] `pnpm test:unit ` — **expect RED** +- [ ] Implement Provider class in the appropriate `*_providers.ts` after nearest neighbor +- [ ] `pnpm test:unit ` — **expect GREEN** + +### Pattern 1: range filter + +- [ ] 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 +- [ ] `pnpm test:unit ` — **expect RED** +- [ ] Implement Provider using `parseContestRound()` range check +- [ ] `pnpm test:unit ` — **expect GREEN** + +### Pattern 3: composite + +- [ ] Confirm whether `prisma/contest_task_pairs.ts` needs new entries before writing tests +- [ ] Add test cases for each constituent contest_id, plus a mixed-source test +- [ ] If section splits: add one test per section +- [ ] `pnpm test:unit ` — **expect RED** +- [ ] Implement Provider (filter by `classifyContest` equality; add section subclasses if needed) +- [ ] `pnpm test:unit ` — **expect GREEN** + +--- + +## Layer 5 — Group registration (TDD) + +- [ ] 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 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()` +- [ ] `pnpm test:unit src/features/tasks/utils/contest-table/` — **expect GREEN** + +--- + +## Final verification + +- [ ] `pnpm test:unit` +- [ ] `pnpm check` +- [ ] `pnpm lint` + +Commit Layer 1–3 and Layer 4–5 as separate commits. diff --git a/docs/guides/claude-code.md b/docs/guides/claude-code.md index 36314f313..dbd203b32 100644 --- a/docs/guides/claude-code.md +++ b/docs/guides/claude-code.md @@ -65,12 +65,13 @@ paths: **本プロジェクトの skills(`.claude/skills/` および superpowers plugin):** -| スキル | 用途 | -| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------- | -| `/writing-plans` | 新機能・追加実装の詳細計画を生成(2-5分単位のタスク分解)。superpowers plugin 提供 | -| `/refactor-plan` | Issue 番号またはパスを渡してリファクタリング計画を出力(実装はしない) | -| `/session-close` | セッション終了時のルーティン:テスト確認 → plan.md 更新 → rules 候補提示 → 肥大化チェック → 繰り返し指示検出 | -| `/dep-upgrade` | ライブラリのメジャーバージョンアップ分析:破壊的変更の整理・本プロジェクトへの影響・新機能提案 → plan.md 生成 → アップグレード実行 | +| スキル | 用途 | +| ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `/writing-plans` | 新機能・追加実装の詳細計画を生成(2-5分単位のタスク分解)。superpowers plugin 提供 | +| `/add-contest-table-provider` | 新しい ContestType と ContestTableProvider を TDD で 5 層実装(Prisma → 型 → ユーティリティ → Provider → グループ登録)。3 パターン対応。実装前に要件を確認する | +| `/refactor-plan` | Issue 番号またはパスを渡してリファクタリング計画を出力(実装はしない) | +| `/session-close` | セッション終了時のルーティン:テスト確認 → plan.md 更新 → rules 候補提示 → 肥大化チェック → 繰り返し指示検出 | +| `/dep-upgrade` | ライブラリのメジャーバージョンアップ分析:破壊的変更の整理・本プロジェクトへの影響・新機能提案 → plan.md 生成 → アップグレード実行 | **`/dep-upgrade` の使い方:** diff --git a/docs/guides/how-to-add-contest-table-provider.md b/docs/guides/how-to-add-contest-table-provider.md index 5eba16fa5..cbf5dd60e 100644 --- a/docs/guides/how-to-add-contest-table-provider.md +++ b/docs/guides/how-to-add-contest-table-provider.md @@ -265,12 +265,14 @@ class TessokuBookSectionProvider extends TessokuBookProvider { | -------------- | ------------- | ---------- | ------------ | | EDPC | `'dp'` | 26問 | A~Z | | TDPC | `'tdpc'` | 26問 | A~Z | +| NDPC | `'ndpc'` | 20問 | A~T | | 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 つのコンテストが統一管理されています。 +\*\*注: EDPC・TDPC・NDPC・FPS 24 は `dps` グループ下で 4 つのコンテストが統一管理されています。 ### 複合ソース型 @@ -317,7 +319,7 @@ describe('MyNewProvider', () => { test('filters tasks correctly', () => { const provider = new MyNewProvider(ContestType.MY_NEW); const filtered = provider.filter(taskResultsForMyNew); - expect(filtered.every((t) => t.contest_id === 'my-contest')).toBe(true); + expect(filtered.every((task) => task.contest_id === 'my-contest')).toBe(true); }); test('returns correct metadata', () => { @@ -426,7 +428,7 @@ export const taskResultsForNewProvider: TaskResults = [ --- -## よくあるミス Top 4 +## よくあるミス Top 5 ### 1. **getDisplayConfig() での属性漏れ** @@ -502,6 +504,31 @@ describe('CustomProvider with unique config', () => { --- +### 5. **contestTypePriorities の JSDoc カテゴリ名を変更してしまう** + +**問題**: 新しい ContestType を挿入して数値範囲が変わったとき、既存の4カテゴリ名 +(`Educational` / `Contests for genius` / `Special contests` / `External platforms`)を +意図せず改名・分割・合体してしまい、歴史的経緯や分類上の意味が失われる。 + +**解決策**: **カテゴリ名は絶対に変更しない**。変えてよいのは括弧内の数値範囲だけ。 + +```typescript +// Before: [ContestType.TDPC, 5] ... [ContestType.PAST, 6] +// After inserting NDPC at 6: +// [ContestType.NDPC, 6], [ContestType.PAST, 7], ... + +// ✅ 数値範囲だけ更新 +// Educational contests (0–11, 17) +// Contests for genius (12–16) +// Special contests (18–20) +// External platforms (21–23) + +// ❌ カテゴリを改名・分割・合体しない +// Educational / DP contests (0–6) ← NG +``` + +--- + ## 実装完了後 ### ドキュメント更新チェックリスト @@ -532,6 +559,7 @@ describe('CustomProvider with unique config', () => { - [#2797](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/2797) - FPS24Provider - [#2920](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/2920)、[#3120](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/3120) - ACLPracticeProvider、ACLBeginnerProvider、ACLProvider - [#3152](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/3152) - JOISemiFinalRoundProvider(本選 → セミファイナルステージ への対応) +- NDPC実装 - NDPCProvider(パターン2: 単一ソース型、prisma/tasks.ts に 20 問存在) ### 実装ファイル @@ -541,4 +569,4 @@ describe('CustomProvider with unique config', () => { --- -**最終更新**: 2026-02-22 +**最終更新**: 2026-05-10 diff --git a/prisma/migrations/20260510075743_fix_atcoder_account_updated_at_default/migration.sql b/prisma/migrations/20260510075743_fix_atcoder_account_updated_at_default/migration.sql new file mode 100644 index 000000000..fb8dca5be --- /dev/null +++ b/prisma/migrations/20260510075743_fix_atcoder_account_updated_at_default/migration.sql @@ -0,0 +1,4 @@ +-- @updatedAt is managed by Prisma ORM, not the DB layer. The original +-- split_atcoder_account migration incorrectly added DEFAULT CURRENT_TIMESTAMP +-- to updatedAt. This migration aligns the migration history with the actual DB state. +ALTER TABLE "atcoder_account" ALTER COLUMN "updatedAt" DROP DEFAULT; diff --git a/prisma/migrations/20260510080034_add_ndpc_to_contest_type/migration.sql b/prisma/migrations/20260510080034_add_ndpc_to_contest_type/migration.sql new file mode 100644 index 000000000..42b043535 --- /dev/null +++ b/prisma/migrations/20260510080034_add_ndpc_to_contest_type/migration.sql @@ -0,0 +1,2 @@ +-- AlterEnum +ALTER TYPE "ContestType" ADD VALUE 'NDPC'; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 9fcf7c7c5..0f2269f82 100755 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -305,6 +305,7 @@ enum ContestType { PAST // Practical Algorithm Skill Test (アルゴリズム実技検定) EDPC // Educational DP Contest / DP まとめコンテスト TDPC // Typical DP Contest + NDPC // Next DP Contest JOI // Japanese Olympiad in Informatics TYPICAL90 // 競プロ典型 90 問 TESSOKU_BOOK // 競技プログラミングの鉄則 diff --git a/prisma/tasks.ts b/prisma/tasks.ts index 8f8de9da5..e2b795a1a 100755 --- a/prisma/tasks.ts +++ b/prisma/tasks.ts @@ -6346,6 +6346,146 @@ export const tasks = [ title: 'A. コンテスト', grade: 'Q2', }, + { + id: 'ndpc2026_t', + contest_id: 'ndpc', + problem_index: 'T', + name: 'Independent Set', + title: 'T. Independent Set', + }, + { + id: 'ndpc2026_s', + contest_id: 'ndpc', + problem_index: 'S', + name: 'Two doors', + title: 'S. Two doors', + }, + { + id: 'ndpc2026_r', + contest_id: 'ndpc', + problem_index: 'R', + name: 'Triples', + title: 'R. Triples', + }, + { + id: 'ndpc2026_q', + contest_id: 'ndpc', + problem_index: 'Q', + name: 'Union of Intervals', + title: 'Q. Union of Intervals', + }, + { + id: 'ndpc2026_p', + contest_id: 'ndpc', + problem_index: 'P', + name: 'LIS', + title: 'P. LIS', + }, + { + id: 'ndpc2026_o', + contest_id: 'ndpc', + problem_index: 'O', + name: 'Game', + title: 'O. Game', + }, + { + id: 'ndpc2026_n', + contest_id: 'ndpc', + problem_index: 'N', + name: 'Knapsack', + title: 'N. Knapsack', + }, + { + id: 'ndpc2026_m', + contest_id: 'ndpc', + problem_index: 'M', + name: 'Numeral', + title: 'M. Numeral', + }, + { + id: 'ndpc2026_l', + contest_id: 'ndpc', + problem_index: 'L', + name: 'LCM', + title: 'L. LCM', + }, + { + id: 'ndpc2026_k', + contest_id: 'ndpc', + problem_index: 'K', + name: 'Addition and Subtraction', + title: 'K. Addition and Subtraction', + }, + { + id: 'ndpc2026_j', + contest_id: 'ndpc', + problem_index: 'J', + name: 'Number and Total', + title: 'J. Number and Total', + }, + { + id: 'ndpc2026_i', + contest_id: 'ndpc', + problem_index: 'I', + name: 'Update Positions', + title: 'I. Update Positions', + }, + { + id: 'ndpc2026_h', + contest_id: 'ndpc', + problem_index: 'H', + name: 'Coin', + title: 'H. Coin', + }, + { + id: 'ndpc2026_g', + contest_id: 'ndpc', + problem_index: 'G', + name: 'Mouth', + title: 'G. Mouth', + }, + { + id: 'ndpc2026_f', + contest_id: 'ndpc', + problem_index: 'F', + name: 'Set', + title: 'F. Set', + }, + { + id: 'ndpc2026_e', + contest_id: 'ndpc', + problem_index: 'E', + name: 'Summer Vacation', + title: 'E. Summer Vacation', + }, + { + id: 'ndpc2026_d', + contest_id: 'ndpc', + problem_index: 'D', + name: 'Banknote', + title: 'D. Banknote', + }, + { + id: 'ndpc2026_c', + contest_id: 'ndpc', + problem_index: 'C', + name: 'String', + title: 'C. String', + }, + { + id: 'ndpc2026_b', + contest_id: 'ndpc', + problem_index: 'B', + name: 'DAG', + title: 'B. DAG', + }, + { + id: 'ndpc2026_a', + contest_id: 'ndpc', + problem_index: 'A', + name: 'Polyomino', + title: 'A. Polyomino', + }, { id: 'math_and_algorithm_bn', contest_id: 'math-and-algorithm', diff --git a/src/features/tasks/utils/contest-table/contest_table_provider_groups.test.ts b/src/features/tasks/utils/contest-table/contest_table_provider_groups.test.ts index 979271046..40f5861d6 100644 --- a/src/features/tasks/utils/contest-table/contest_table_provider_groups.test.ts +++ b/src/features/tasks/utils/contest-table/contest_table_provider_groups.test.ts @@ -20,6 +20,7 @@ import { ACLProvider, EDPCProvider, TDPCProvider, + NDPCProvider, FPS24Provider, JOIFirstQualRoundProvider, JOISecondQualRound2020OnwardsProvider, @@ -228,14 +229,15 @@ describe('prepareContestProviderPresets', () => { test('expects to create DPs preset correctly', () => { const group = prepareContestProviderPresets().dps(); - expect(group.getGroupName()).toBe('EDPC・TDPC・FPS 24'); + expect(group.getGroupName()).toBe('EDPC・TDPC・NDPC・FPS 24'); expect(group.getMetadata()).toEqual({ - buttonLabel: 'EDPC・TDPC・FPS 24', - ariaLabel: 'EDPC and TDPC and FPS 24 contests', + buttonLabel: 'EDPC・TDPC・NDPC・FPS 24', + ariaLabel: 'EDPC, TDPC, NDPC and FPS 24 contests', }); - expect(group.getSize()).toBe(3); + expect(group.getSize()).toBe(4); expect(group.getProvider(ContestType.EDPC)).toBeInstanceOf(EDPCProvider); expect(group.getProvider(ContestType.TDPC)).toBeInstanceOf(TDPCProvider); + expect(group.getProvider(ContestType.NDPC)).toBeInstanceOf(NDPCProvider); expect(group.getProvider(ContestType.FPS_24)).toBeInstanceOf(FPS24Provider); }); diff --git a/src/features/tasks/utils/contest-table/contest_table_provider_groups.ts b/src/features/tasks/utils/contest-table/contest_table_provider_groups.ts index af8aa961d..4504182ca 100644 --- a/src/features/tasks/utils/contest-table/contest_table_provider_groups.ts +++ b/src/features/tasks/utils/contest-table/contest_table_provider_groups.ts @@ -23,7 +23,7 @@ import { TessokuBookForChallengesProvider, } from './tessoku_book_providers'; import { MathAndAlgorithmProvider } from './math_and_algorithm_provider'; -import { EDPCProvider, TDPCProvider } from './dp_providers'; +import { EDPCProvider, TDPCProvider, NDPCProvider } from './dp_providers'; import { FPS24Provider } from './fps24_provider'; import { ACLPracticeProvider, ACLBeginnerProvider, ACLProvider } from './acl_providers'; import { @@ -182,15 +182,16 @@ export const prepareContestProviderPresets = () => { }).addProvider(new MathAndAlgorithmProvider(ContestType.MATH_AND_ALGORITHM)), /** - * DP group (EDPC and TDPC) + * DP group (EDPC, TDPC, NDPC, and FPS 24) */ dps: () => - new ContestTableProviderGroup(`EDPC・TDPC・FPS 24`, { - buttonLabel: 'EDPC・TDPC・FPS 24', - ariaLabel: 'EDPC and TDPC and FPS 24 contests', + new ContestTableProviderGroup(`EDPC・TDPC・NDPC・FPS 24`, { + buttonLabel: 'EDPC・TDPC・NDPC・FPS 24', + ariaLabel: 'EDPC, TDPC, NDPC and FPS 24 contests', }).addProviders( new EDPCProvider(ContestType.EDPC), new TDPCProvider(ContestType.TDPC), + new NDPCProvider(ContestType.NDPC), new FPS24Provider(ContestType.FPS_24), ), diff --git a/src/features/tasks/utils/contest-table/dp_providers.test.ts b/src/features/tasks/utils/contest-table/dp_providers.test.ts index 68556daaa..92cc9407c 100644 --- a/src/features/tasks/utils/contest-table/dp_providers.test.ts +++ b/src/features/tasks/utils/contest-table/dp_providers.test.ts @@ -3,7 +3,7 @@ import { describe, test, expect } from 'vitest'; import { ContestType } from '$lib/types/contest'; import type { TaskResults } from '$lib/types/task'; -import { EDPCProvider, TDPCProvider } from './dp_providers'; +import { EDPCProvider, TDPCProvider, NDPCProvider } from './dp_providers'; describe('DP providers', () => { describe.each([ @@ -23,6 +23,14 @@ describe('DP providers', () => { abbreviationName: 'tdpc', label: 'TDPC provider', }, + { + providerClass: NDPCProvider, + contestType: ContestType.NDPC, + contestId: 'ndpc', + title: 'Next DP Contest', + abbreviationName: 'ndpc', + label: 'NDPC provider', + }, ])('$label', ({ providerClass, contestType, contestId, title, abbreviationName }) => { test('expects to get correct metadata', () => { const provider = new providerClass(contestType); diff --git a/src/features/tasks/utils/contest-table/dp_providers.ts b/src/features/tasks/utils/contest-table/dp_providers.ts index 8d1d84c91..8ee49b01f 100644 --- a/src/features/tasks/utils/contest-table/dp_providers.ts +++ b/src/features/tasks/utils/contest-table/dp_providers.ts @@ -73,3 +73,36 @@ export class TDPCProvider extends ContestTableProviderBase { return ''; } } + +export class NDPCProvider extends ContestTableProviderBase { + protected setFilterCondition(): (taskResult: TaskResult) => boolean { + return (taskResult: TaskResult) => { + if (classifyContest(taskResult.contest_id) !== this.contestType) { + return false; + } + + return taskResult.contest_id === 'ndpc'; + }; + } + + getMetadata(): ContestTableMetaData { + return { + title: 'Next DP Contest', + abbreviationName: 'ndpc', + }; + } + + 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 2xl:w-1/7 px-1 py-2', + isShownTaskIndex: true, + }; + } + + getContestRoundLabel(_contestId: string): string { + return ''; + } +} diff --git a/src/lib/types/contest.ts b/src/lib/types/contest.ts index c44adbaf4..270ead6e2 100644 --- a/src/lib/types/contest.ts +++ b/src/lib/types/contest.ts @@ -35,6 +35,7 @@ export const ContestType: { [key in ContestTypeOrigin]: key } = { PAST: 'PAST', // Practical Algorithm Skill Test (アルゴリズム実技検定) EDPC: 'EDPC', // Educational DP Contest / DP まとめコンテスト TDPC: 'TDPC', // Typical DP Contest + NDPC: 'NDPC', // Next DP Contest JOI: 'JOI', // Japanese Olympiad in Informatics TYPICAL90: 'TYPICAL90', // 競プロ典型 90 問 TESSOKU_BOOK: 'TESSOKU_BOOK', // 競技プログラミングの鉄則 diff --git a/src/lib/utils/contest.ts b/src/lib/utils/contest.ts index 186ecf30a..ba291fe2f 100644 --- a/src/lib/utils/contest.ts +++ b/src/lib/utils/contest.ts @@ -40,6 +40,10 @@ export const classifyContest = (contest_id: string) => { return ContestType.TDPC; } + if (contest_id === 'ndpc') { + return ContestType.NDPC; + } + if (contest_id.startsWith('past')) { return ContestType.PAST; } @@ -248,13 +252,13 @@ export function getContestPrefixes(contestPrefixes: Record) { } /** - * Contest type priorities (0 = Highest, 21 = Lowest) + * Contest type priorities (0 = Highest, 23 = Lowest) * * Priority assignment rationale: - * - Educational contests (0-10, 16): ABS, ABC, APG4B and AWC etc. - * - Contests for genius (11-15): ARC, AGC, and their variants - * - Special contests (17-19): UNIVERSITY, FPS_24, OTHERS - * - External platforms (20-22): AOJ_COURSES, AOJ_PCK, AOJ_JAG + * - Educational contests (0-11, 17): ABS, ABC, APG4B and AWC etc. + * - Contests for genius (12-16): ARC, AGC, and their variants + * - Special contests (18-20): UNIVERSITY, FPS_24, OTHERS + * - External platforms (21-23): AOJ_COURSES, AOJ_PCK, AOJ_JAG * * @remarks * HACK: The priorities for ARC, AGC, UNIVERSITY, AOJ_COURSES, and AOJ_PCK are temporary @@ -270,23 +274,24 @@ export const contestTypePriorities: Map = new Map([ [ContestType.TYPICAL90, 3], [ContestType.EDPC, 4], [ContestType.TDPC, 5], - [ContestType.PAST, 6], - [ContestType.ACL_PRACTICE, 7], - [ContestType.JOI, 8], - [ContestType.TESSOKU_BOOK, 9], - [ContestType.MATH_AND_ALGORITHM, 10], - [ContestType.ARC, 11], - [ContestType.AGC, 12], - [ContestType.ABC_LIKE, 13], - [ContestType.ARC_LIKE, 14], - [ContestType.AGC_LIKE, 15], - [ContestType.AWC, 16], - [ContestType.UNIVERSITY, 17], - [ContestType.FPS_24, 18], - [ContestType.OTHERS, 19], // AtCoder (その他) - [ContestType.AOJ_COURSES, 20], - [ContestType.AOJ_PCK, 21], - [ContestType.AOJ_JAG, 22], + [ContestType.NDPC, 6], + [ContestType.PAST, 7], + [ContestType.ACL_PRACTICE, 8], + [ContestType.JOI, 9], + [ContestType.TESSOKU_BOOK, 10], + [ContestType.MATH_AND_ALGORITHM, 11], + [ContestType.ARC, 12], + [ContestType.AGC, 13], + [ContestType.ABC_LIKE, 14], + [ContestType.ARC_LIKE, 15], + [ContestType.AGC_LIKE, 16], + [ContestType.AWC, 17], + [ContestType.UNIVERSITY, 18], + [ContestType.FPS_24, 19], + [ContestType.OTHERS, 20], // AtCoder (その他) + [ContestType.AOJ_COURSES, 21], + [ContestType.AOJ_PCK, 22], + [ContestType.AOJ_JAG, 23], ]); export function getContestPriority(contestId: string): number { @@ -377,6 +382,10 @@ export const getContestNameLabel = (contestId: string) => { return 'TDPC'; } + if (contestId === 'ndpc') { + return 'NDPC'; + } + if (contestId.startsWith('past')) { return getPastContestLabel(PAST_TRANSLATIONS, contestId); } diff --git a/src/test/lib/utils/contest.test.ts b/src/test/lib/utils/contest.test.ts index 3267d11aa..d044131cf 100644 --- a/src/test/lib/utils/contest.test.ts +++ b/src/test/lib/utils/contest.test.ts @@ -67,6 +67,14 @@ describe('Contest', () => { }); }); + describe('when contest_id is ndpc', () => { + TestCasesForContestType.ndpc.forEach(({ name, value }) => { + runTests(`${name}`, [value], ({ contestId, expected }: TestCaseForContestType) => { + expect(classifyContest(contestId)).toEqual(expected); + }); + }); + }); + describe('when contest_id contains past', () => { TestCasesForContestType.past.forEach(({ name, value }) => { runTests(`${name}`, [value], ({ contestId, expected }: TestCaseForContestType) => { @@ -257,6 +265,14 @@ describe('Contest', () => { }); }); + describe('when contest_id is ndpc', () => { + TestCasesForContestType.ndpc.forEach(({ name, value }) => { + runTests(`${name}`, [value], ({ contestId, expected }: TestCaseForContestType) => { + expect(getContestPriority(contestId)).toEqual(contestTypePriorities.get(expected)); + }); + }); + }); + describe('when contest_id contains past', () => { TestCasesForContestType.past.forEach(({ name, value }) => { runTests(`${name}`, [value], ({ contestId, expected }: TestCaseForContestType) => { @@ -415,6 +431,14 @@ describe('Contest', () => { }); }); + describe('when contest_id is ndpc', () => { + TestCasesForContestNameLabel.ndpc.forEach(({ name, value }) => { + runTests(`${name}`, [value], ({ contestId, expected }: TestCaseForContestNameLabel) => { + expect(getContestNameLabel(contestId)).toEqual(expected); + }); + }); + }); + describe('when contest_id is practice2 (ACL practice)', () => { TestCasesForContestNameLabel.aclPractice.forEach(({ name, value }) => { runTests(`${name}`, [value], ({ contestId, expected }: TestCaseForContestNameLabel) => { diff --git a/src/test/lib/utils/task.test.ts b/src/test/lib/utils/task.test.ts index 272fd3974..e41680037 100644 --- a/src/test/lib/utils/task.test.ts +++ b/src/test/lib/utils/task.test.ts @@ -363,32 +363,37 @@ describe('Task', () => { { first: tasksForVerificationOfOrder.abc999_a, second: tasksForVerificationOfOrder.tdpc_contest, - expected: -4, // order: abc999_a, tpdc_contest + expected: -4, // order: abc999_a, tdpc_contest + }, + { + first: tasksForVerificationOfOrder.abc999_a, + second: tasksForVerificationOfOrder.ndpc2026_a, + expected: -5, // order: abc999_a, ndpc2026_a }, { first: tasksForVerificationOfOrder.abc999_a, second: tasksForVerificationOfOrder.past202309_a, - expected: -5, // order: abc999_a, past202309_a + expected: -6, // order: abc999_a, past202309_a }, { first: tasksForVerificationOfOrder.abc999_a, second: tasksForVerificationOfOrder.acl_a, - expected: -6, // order: abc999_a, acl_a + expected: -7, // order: abc999_a, acl_a }, { first: tasksForVerificationOfOrder.abc999_a, second: tasksForVerificationOfOrder.joi2023_yo1c, - expected: -7, // order: abc999_a, joi2023_yo1c + expected: -8, // order: abc999_a, joi2023_yo1c }, { first: tasksForVerificationOfOrder.abc999_a, second: tasksForVerificationOfOrder.tessoku_book_a, - expected: -8, // order: abc999_a, tessoku_book_a + expected: -9, // order: abc999_a, tessoku_book_a }, { first: tasksForVerificationOfOrder.abc999_a, second: tasksForVerificationOfOrder.math_and_algorithm_a, - expected: -9, // order: abc999_a, math_and_algorithm_a + expected: -10, // order: abc999_a, math_and_algorithm_a }, ]; diff --git a/src/test/lib/utils/test_cases/contest_name_labels.ts b/src/test/lib/utils/test_cases/contest_name_labels.ts index 30832f6b9..7ce7a8f4a 100644 --- a/src/test/lib/utils/test_cases/contest_name_labels.ts +++ b/src/test/lib/utils/test_cases/contest_name_labels.ts @@ -21,6 +21,13 @@ export const tdpc = [ }), ]; +export const ndpc = [ + createTestCaseForContestNameLabel('NDPC')({ + contestId: 'ndpc', + expected: 'NDPC', + }), +]; + export const aclPractice = [ createTestCaseForContestNameLabel('ACL Practice')({ contestId: 'practice2', diff --git a/src/test/lib/utils/test_cases/contest_type.ts b/src/test/lib/utils/test_cases/contest_type.ts index 81b7fdb4d..7e3d10fe9 100644 --- a/src/test/lib/utils/test_cases/contest_type.ts +++ b/src/test/lib/utils/test_cases/contest_type.ts @@ -62,6 +62,13 @@ export const tdpc = [ }), ]; +export const ndpc = [ + createTestCaseForContestType('NDPC')({ + contestId: 'ndpc', + expected: ContestType.NDPC, + }), +]; + const pastContestData = [ { name: 'PAST 1st', contestId: 'past201912-open' }, { name: 'PAST 2nd', contestId: 'past202004-open' }, diff --git a/src/test/lib/utils/test_cases/task_results.ts b/src/test/lib/utils/test_cases/task_results.ts index 565de0401..253ce4393 100644 --- a/src/test/lib/utils/test_cases/task_results.ts +++ b/src/test/lib/utils/test_cases/task_results.ts @@ -405,6 +405,20 @@ const tdpc_contest: TaskResult = { grade: 'PENDING', updated_at: new Date(), }; +const ndpc2026_a: TaskResult = { + is_ac: false, + user_id: userId2, + status_name: 'ns', + status_id: '4', + submission_status_image_path: 'ns.png', + submission_status_label_name: '未挑戦', + contest_id: 'ndpc', + task_table_index: 'A', + task_id: 'ndpc2026_a', + title: 'A. Polyomino', + grade: 'PENDING', + updated_at: new Date(), +}; const acl_a: TaskResult = { is_ac: false, user_id: userId2, @@ -490,8 +504,9 @@ export const tasksForVerificationOfOrder = { typical90_a, dp_b, tdpc_contest, - acl_a, + ndpc2026_a, past202309_a, + acl_a, joi2023_yo1c, tessoku_book_a, math_and_algorithm_a,