From 88ab96c90310fc2c97d6097f46154d9922e260d3 Mon Sep 17 00:00:00 2001 From: Kato Hiroki Date: Sat, 14 Mar 2026 06:29:12 +0000 Subject: [PATCH 1/3] docs: Create plan (#3264) --- .../add-rerooting-dp-to-contests/plan.md | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 docs/dev-notes/2026-03-14/add-rerooting-dp-to-contests/plan.md diff --git a/docs/dev-notes/2026-03-14/add-rerooting-dp-to-contests/plan.md b/docs/dev-notes/2026-03-14/add-rerooting-dp-to-contests/plan.md new file mode 100644 index 000000000..e5abff718 --- /dev/null +++ b/docs/dev-notes/2026-03-14/add-rerooting-dp-to-contests/plan.md @@ -0,0 +1,79 @@ +# 全方位木DPの問題追加 (Issue #3264) + +[Issue #3264](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/3264) で、s8pc-4 (square869120Contest #4) の問題 D を追加する。PR #3243 (atc001 追加) と同じパターン。 + +## 変更対象 + +### 1. `src/lib/utils/contest.ts` + +**`ATCODER_OTHERS` に追加**: + +```ts +'s8pc-4': 'square869120Contest #4', +``` + +**`getContestNameLabel` を汎用化**: + +`ATCODER_OTHERS` の辞書を完全一致でルックアップし、登録済みならその名前を返す。既存の `chokudai_S` はプレフィックス一致(`chokudai_S001` のような ID)のため辞書には存在せず、この汎用処理と競合しない。`chokudai_S` の専用分岐はそのまま残す。 + +追加箇所は `regexForAtCoderUniversity` の直後、`chokudai_S` の直前: + +```ts +const othersLabel = ATCODER_OTHERS[contestId as keyof typeof ATCODER_OTHERS]; +if (othersLabel) { + return othersLabel; +} +``` + +これにより `atc001` など既存の ATCODER_OTHERS 登録コンテストも自動でラベルが返るようになる(現状はフォールバックの `toUpperCase()` に落ちていたものを正式に対応)。 + +### 2. テストケース + +**`src/test/lib/utils/test_cases/contest_type.ts`** の `atCoderOthers` 配列に追加: + +```ts +createTestCaseForContestType('square869120Contest #4')({ + contestId: 's8pc-4', + expected: ContestType.OTHERS, +}), +``` + +**`src/test/lib/utils/test_cases/contest_name_labels.ts`** の `atCoderOthers` 配列に追加: + +```ts +createTestCaseForContestNameLabel('square869120Contest #4')({ + contestId: 's8pc-4', + expected: 'square869120Contest #4', +}), +``` + +### 3. `prisma/tasks.ts` + +末尾に追加: + +```ts +{ + id: 's8pc_4_d', + contest_id: 's8pc-4', + problem_index: 'D', + name: 'Driving on a Tree', + title: 'D. Driving on a Tree', +}, +``` + +`grade` はなし(`PENDING` のまま)。 + +## 変更しないもの + +- `prisma/schema.prisma` — `ContestType.OTHERS` を使うため変更不要 +- `contest_task_pairs.ts` — 共有問題なし +- コンテストテーブル表示用コンポーネント — OTHERS として既存の表示に乗る + +## 作業順序 + +1. `contest.ts` の `ATCODER_OTHERS` に `'s8pc-4'` を追加 +2. `getContestNameLabel` を汎用化(辞書ルックアップ追加) +3. テストケースを追加 +4. `prisma/tasks.ts` にタスクを追加 +5. `pnpm test:unit` でテスト通過を確認 +6. `pnpm format` を実行 From 072862d02725be2a516e3cb0093c85b59fabd24e Mon Sep 17 00:00:00 2001 From: Kato Hiroki Date: Sat, 14 Mar 2026 07:04:50 +0000 Subject: [PATCH 2/3] feat: Add tasks for S8PC #4 (#3264) --- .../add-rerooting-dp-to-contests/plan.md | 82 ++++--------------- prisma/tasks.ts | 7 ++ src/lib/utils/contest.ts | 7 ++ .../utils/test_cases/contest_name_labels.ts | 4 + src/test/lib/utils/test_cases/contest_type.ts | 4 + 5 files changed, 37 insertions(+), 67 deletions(-) diff --git a/docs/dev-notes/2026-03-14/add-rerooting-dp-to-contests/plan.md b/docs/dev-notes/2026-03-14/add-rerooting-dp-to-contests/plan.md index e5abff718..e930f07aa 100644 --- a/docs/dev-notes/2026-03-14/add-rerooting-dp-to-contests/plan.md +++ b/docs/dev-notes/2026-03-14/add-rerooting-dp-to-contests/plan.md @@ -1,79 +1,27 @@ # 全方位木DPの問題追加 (Issue #3264) -[Issue #3264](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/3264) で、s8pc-4 (square869120Contest #4) の問題 D を追加する。PR #3243 (atc001 追加) と同じパターン。 +[Issue #3264](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/3264) で、s8pc-4 (square869120Contest #4) の問題 D を追加。 -## 変更対象 +## 変更概要 -### 1. `src/lib/utils/contest.ts` +- `ATCODER_OTHERS` に `'s8pc-4': 'square869120Contest #4'` を追加 +- `getContestNameLabel` に辞書ルックアップを追加(`regexForAtCoderUniversity` の直後、`chokudai_S` の直前) +- テストケースを `contest_type.ts` と `contest_name_labels.ts` に追加 +- `prisma/tasks.ts` に `s8pc_4_d`(Driving on a Tree、grade なし)を追加 -**`ATCODER_OTHERS` に追加**: +## 教訓 -```ts -'s8pc-4': 'square869120Contest #4', -``` +### `ATCODER_OTHERS` は分類辞書であり、ラベル辞書でもある -**`getContestNameLabel` を汎用化**: +変更前は `getContestNameLabel` が `ATCODER_OTHERS` を参照しておらず、辞書登録済みのコンテストでも `toUpperCase()` のフォールバックに落ちていた。今回の汎用化で辞書1本が「分類」と「表示名解決」を兼ねるようになった。**新コンテストは辞書に1エントリ追加するだけで両方が自動的に有効になる。** -`ATCODER_OTHERS` の辞書を完全一致でルックアップし、登録済みならその名前を返す。既存の `chokudai_S` はプレフィックス一致(`chokudai_S001` のような ID)のため辞書には存在せず、この汎用処理と競合しない。`chokudai_S` の専用分岐はそのまま残す。 +### ルックアップの挿入位置が正確性を決める -追加箇所は `regexForAtCoderUniversity` の直後、`chokudai_S` の直前: +`getContestNameLabel` は上から順に評価する if チェーンであるため、挿入位置が重要。 -```ts -const othersLabel = ATCODER_OTHERS[contestId as keyof typeof ATCODER_OTHERS]; -if (othersLabel) { - return othersLabel; -} -``` +- `chokudai_S` はプレフィックス一致(`chokudai_S001` 等)で辞書キーと完全一致しない → 専用ブランチを辞書ルックアップの**後**に残す +- `atc001` は `regexForAxc`(`/^(abc|arc|agc|atc)\d{3}$/i`)に**先に**マッチするため辞書ルックアップに到達しない → 既存の表示 `'ATC 001'` は維持される -これにより `atc001` など既存の ATCODER_OTHERS 登録コンテストも自動でラベルが返るようになる(現状はフォールバックの `toUpperCase()` に落ちていたものを正式に対応)。 +### 一般化できる知見 -### 2. テストケース - -**`src/test/lib/utils/test_cases/contest_type.ts`** の `atCoderOthers` 配列に追加: - -```ts -createTestCaseForContestType('square869120Contest #4')({ - contestId: 's8pc-4', - expected: ContestType.OTHERS, -}), -``` - -**`src/test/lib/utils/test_cases/contest_name_labels.ts`** の `atCoderOthers` 配列に追加: - -```ts -createTestCaseForContestNameLabel('square869120Contest #4')({ - contestId: 's8pc-4', - expected: 'square869120Contest #4', -}), -``` - -### 3. `prisma/tasks.ts` - -末尾に追加: - -```ts -{ - id: 's8pc_4_d', - contest_id: 's8pc-4', - problem_index: 'D', - name: 'Driving on a Tree', - title: 'D. Driving on a Tree', -}, -``` - -`grade` はなし(`PENDING` のまま)。 - -## 変更しないもの - -- `prisma/schema.prisma` — `ContestType.OTHERS` を使うため変更不要 -- `contest_task_pairs.ts` — 共有問題なし -- コンテストテーブル表示用コンポーネント — OTHERS として既存の表示に乗る - -## 作業順序 - -1. `contest.ts` の `ATCODER_OTHERS` に `'s8pc-4'` を追加 -2. `getContestNameLabel` を汎用化(辞書ルックアップ追加) -3. テストケースを追加 -4. `prisma/tasks.ts` にタスクを追加 -5. `pnpm test:unit` でテスト通過を確認 -6. `pnpm format` を実行 +新コンテストを追加する際は、分類ロジック(`classifyContest`)と表示名ロジック(`getContestNameLabel`)の**両方**への反映が必要かを確認する。共通辞書で両方を賄える場合はそうする。プレフィックス一致が必要な場合のみ専用ブランチを追加する。 diff --git a/prisma/tasks.ts b/prisma/tasks.ts index 3fe94daac..8f8de9da5 100755 --- a/prisma/tasks.ts +++ b/prisma/tasks.ts @@ -8501,4 +8501,11 @@ export const tasks = [ name: 'お菓子', title: 'A. お菓子', }, + { + id: 's8pc_4_d', + contest_id: 's8pc-4', + problem_index: 'D', + name: 'Driving on a Tree', + title: 'D. Driving on a Tree', + }, ]; diff --git a/src/lib/utils/contest.ts b/src/lib/utils/contest.ts index 01f4fc6ee..186ecf30a 100644 --- a/src/lib/utils/contest.ts +++ b/src/lib/utils/contest.ts @@ -194,6 +194,7 @@ const atCoderUniversityPrefixes = getContestPrefixes(ATCODER_UNIVERSITIES); const ATCODER_OTHERS: ContestPrefix = { chokudai_S: 'Chokudai SpeedRun', atc001: 'AtCoder Typical Contest 001', + 's8pc-4': 'square869120Contest #4', 'code-festival-2014-quala': 'Code Festival 2014 予選 A', 'code-festival-2014-qualb': 'Code Festival 2014 予選 B', 'code-festival-2014-final': 'Code Festival 2014 決勝', @@ -404,6 +405,12 @@ export const getContestNameLabel = (contestId: string) => { return getAtCoderUniversityContestLabel(contestId); } + const othersLabel = ATCODER_OTHERS[contestId as keyof typeof ATCODER_OTHERS]; + + if (othersLabel) { + return othersLabel; + } + if (contestId.startsWith('chokudai_S')) { return contestId.replace('chokudai_S', 'Chokudai SpeedRun '); } 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 6abe638ba..30832f6b9 100644 --- a/src/test/lib/utils/test_cases/contest_name_labels.ts +++ b/src/test/lib/utils/test_cases/contest_name_labels.ts @@ -41,6 +41,10 @@ export const atCoderOthers = [ contestId: 'atc001', expected: 'ATC 001', }), + createTestCaseForContestNameLabel('square869120Contest #4')({ + contestId: 's8pc-4', + expected: 'square869120Contest #4', + }), ]; export const mathAndAlgorithm = [ diff --git a/src/test/lib/utils/test_cases/contest_type.ts b/src/test/lib/utils/test_cases/contest_type.ts index 53283c55c..81b7fdb4d 100644 --- a/src/test/lib/utils/test_cases/contest_type.ts +++ b/src/test/lib/utils/test_cases/contest_type.ts @@ -402,6 +402,10 @@ export const atCoderOthers = [ contestId: 'atc001', expected: ContestType.OTHERS, }), + createTestCaseForContestType('square869120Contest #4')({ + contestId: 's8pc-4', + expected: ContestType.OTHERS, + }), createTestCaseForContestType('CODE FESTIVAL 2014 qual A')({ contestId: 'code-festival-2014-quala', expected: ContestType.OTHERS, From 0bb07aab0b1381061f2e538432aa811215069e07 Mon Sep 17 00:00:00 2001 From: Kato Hiroki Date: Sat, 14 Mar 2026 07:05:10 +0000 Subject: [PATCH 3/3] docs: Add future refactoring plan (#3264) --- .../add-rerooting-dp-to-contests/plan.md | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/docs/dev-notes/2026-03-14/add-rerooting-dp-to-contests/plan.md b/docs/dev-notes/2026-03-14/add-rerooting-dp-to-contests/plan.md index e930f07aa..3ac765bc2 100644 --- a/docs/dev-notes/2026-03-14/add-rerooting-dp-to-contests/plan.md +++ b/docs/dev-notes/2026-03-14/add-rerooting-dp-to-contests/plan.md @@ -25,3 +25,62 @@ ### 一般化できる知見 新コンテストを追加する際は、分類ロジック(`classifyContest`)と表示名ロジック(`getContestNameLabel`)の**両方**への反映が必要かを確認する。共通辞書で両方を賄える場合はそうする。プレフィックス一致が必要な場合のみ専用ブランチを追加する。 + +## 将来的なリファクタリング候補:ContestHandler による if チェーンの解消 + +### 概略 + +`classifyContest` と `getContestNameLabel` の if チェーンを、コンテスト種別ごとの handler 配列に置き換える。 + +```ts +type ContestHandler = { + type: ContestType; + matches: (contestId: string) => boolean; + label: (contestId: string) => string; +}; + +const handlers: ContestHandler[] = [ + { + type: ContestType.ABC, + matches: (id) => /^abc\d{3}$/.test(id), + label: (id) => + id.replace( + regexForAxc, + (_, contestType, contestNumber) => `${contestType.toUpperCase()} ${contestNumber}`, + ), + }, + { + type: ContestType.JOI, + matches: (id) => id.startsWith('joi'), + label: getJoiContestLabel, + }, + { + type: ContestType.OTHERS, + matches: (id) => atCoderOthersPrefixes.some((prefix) => id.startsWith(prefix)), + label: (id) => + ATCODER_OTHERS[id as keyof typeof ATCODER_OTHERS] ?? + id.replace('chokudai_S', 'Chokudai SpeedRun '), + }, + // ... +]; + +// classifyContest / getContestNameLabel はどちらも handlers を1回スキャンするだけになる +``` + +マッチング戦略(正規表現・プレフィックス・辞書引き)の異質さは各 handler の `matches` / `label` 内部に閉じ込められ、呼び出し側から消える。 + +### 設計上の判断 + +- 順序に依存するため `Map` ではなく**順序付き配列**が必要(先勝ち) +- `ATCODER_OTHERS` の `chokudai_S` は辞書キーでもあり、OTHERS handler 内部で両方を吸収する +- 既存の単体テストがそのまま安全網として機能する + +### 踏み切れない理由(現状) + +- `classifyContest` / `getContestNameLabel` は `contest-table/` providers・URL生成・表示ラベルなど**広範囲から参照**されており、影響範囲が大きい +- リファクタリング自体は機械的だが「同じ動作を別の構造で書き直す」変更はテストが通っても見落としが出やすい +- 現状の if チェーンは「読みにくいが壊れていない」状態であり、コストが便益を上回る + +### 実行の目安 + +JOI のような複雑な変換ロジックを持つ新カテゴリが増え、if チェーンの同期ミスによるバグが実際に発生したとき。