Skip to content

Commit 7b1ae12

Browse files
authored
Merge pull request #3265 from AtCoder-NoviSteps/#3264
feat: Add tasks for S8PC #4 (#3264)
2 parents 7f225f7 + 0bb07aa commit 7b1ae12

5 files changed

Lines changed: 108 additions & 0 deletions

File tree

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# 全方位木DPの問題追加 (Issue #3264)
2+
3+
[Issue #3264](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/3264) で、s8pc-4 (square869120Contest #4) の問題 D を追加。
4+
5+
## 変更概要
6+
7+
- `ATCODER_OTHERS``'s8pc-4': 'square869120Contest #4'` を追加
8+
- `getContestNameLabel` に辞書ルックアップを追加(`regexForAtCoderUniversity` の直後、`chokudai_S` の直前)
9+
- テストケースを `contest_type.ts``contest_name_labels.ts` に追加
10+
- `prisma/tasks.ts``s8pc_4_d`(Driving on a Tree、grade なし)を追加
11+
12+
## 教訓
13+
14+
### `ATCODER_OTHERS` は分類辞書であり、ラベル辞書でもある
15+
16+
変更前は `getContestNameLabel``ATCODER_OTHERS` を参照しておらず、辞書登録済みのコンテストでも `toUpperCase()` のフォールバックに落ちていた。今回の汎用化で辞書1本が「分類」と「表示名解決」を兼ねるようになった。**新コンテストは辞書に1エントリ追加するだけで両方が自動的に有効になる。**
17+
18+
### ルックアップの挿入位置が正確性を決める
19+
20+
`getContestNameLabel` は上から順に評価する if チェーンであるため、挿入位置が重要。
21+
22+
- `chokudai_S` はプレフィックス一致(`chokudai_S001` 等)で辞書キーと完全一致しない → 専用ブランチを辞書ルックアップの****に残す
23+
- `atc001``regexForAxc``/^(abc|arc|agc|atc)\d{3}$/i`)に**先に**マッチするため辞書ルックアップに到達しない → 既存の表示 `'ATC 001'` は維持される
24+
25+
### 一般化できる知見
26+
27+
新コンテストを追加する際は、分類ロジック(`classifyContest`)と表示名ロジック(`getContestNameLabel`)の**両方**への反映が必要かを確認する。共通辞書で両方を賄える場合はそうする。プレフィックス一致が必要な場合のみ専用ブランチを追加する。
28+
29+
## 将来的なリファクタリング候補:ContestHandler による if チェーンの解消
30+
31+
### 概略
32+
33+
`classifyContest``getContestNameLabel` の if チェーンを、コンテスト種別ごとの handler 配列に置き換える。
34+
35+
```ts
36+
type ContestHandler = {
37+
type: ContestType;
38+
matches: (contestId: string) => boolean;
39+
label: (contestId: string) => string;
40+
};
41+
42+
const handlers: ContestHandler[] = [
43+
{
44+
type: ContestType.ABC,
45+
matches: (id) => /^abc\d{3}$/.test(id),
46+
label: (id) =>
47+
id.replace(
48+
regexForAxc,
49+
(_, contestType, contestNumber) => `${contestType.toUpperCase()} ${contestNumber}`,
50+
),
51+
},
52+
{
53+
type: ContestType.JOI,
54+
matches: (id) => id.startsWith('joi'),
55+
label: getJoiContestLabel,
56+
},
57+
{
58+
type: ContestType.OTHERS,
59+
matches: (id) => atCoderOthersPrefixes.some((prefix) => id.startsWith(prefix)),
60+
label: (id) =>
61+
ATCODER_OTHERS[id as keyof typeof ATCODER_OTHERS] ??
62+
id.replace('chokudai_S', 'Chokudai SpeedRun '),
63+
},
64+
// ...
65+
];
66+
67+
// classifyContest / getContestNameLabel はどちらも handlers を1回スキャンするだけになる
68+
```
69+
70+
マッチング戦略(正規表現・プレフィックス・辞書引き)の異質さは各 handler の `matches` / `label` 内部に閉じ込められ、呼び出し側から消える。
71+
72+
### 設計上の判断
73+
74+
- 順序に依存するため `Map` ではなく**順序付き配列**が必要(先勝ち)
75+
- `ATCODER_OTHERS``chokudai_S` は辞書キーでもあり、OTHERS handler 内部で両方を吸収する
76+
- 既存の単体テストがそのまま安全網として機能する
77+
78+
### 踏み切れない理由(現状)
79+
80+
- `classifyContest` / `getContestNameLabel``contest-table/` providers・URL生成・表示ラベルなど**広範囲から参照**されており、影響範囲が大きい
81+
- リファクタリング自体は機械的だが「同じ動作を別の構造で書き直す」変更はテストが通っても見落としが出やすい
82+
- 現状の if チェーンは「読みにくいが壊れていない」状態であり、コストが便益を上回る
83+
84+
### 実行の目安
85+
86+
JOI のような複雑な変換ロジックを持つ新カテゴリが増え、if チェーンの同期ミスによるバグが実際に発生したとき。

prisma/tasks.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8501,4 +8501,11 @@ export const tasks = [
85018501
name: 'お菓子',
85028502
title: 'A. お菓子',
85038503
},
8504+
{
8505+
id: 's8pc_4_d',
8506+
contest_id: 's8pc-4',
8507+
problem_index: 'D',
8508+
name: 'Driving on a Tree',
8509+
title: 'D. Driving on a Tree',
8510+
},
85048511
];

src/lib/utils/contest.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ const atCoderUniversityPrefixes = getContestPrefixes(ATCODER_UNIVERSITIES);
194194
const ATCODER_OTHERS: ContestPrefix = {
195195
chokudai_S: 'Chokudai SpeedRun',
196196
atc001: 'AtCoder Typical Contest 001',
197+
's8pc-4': 'square869120Contest #4',
197198
'code-festival-2014-quala': 'Code Festival 2014 予選 A',
198199
'code-festival-2014-qualb': 'Code Festival 2014 予選 B',
199200
'code-festival-2014-final': 'Code Festival 2014 決勝',
@@ -404,6 +405,12 @@ export const getContestNameLabel = (contestId: string) => {
404405
return getAtCoderUniversityContestLabel(contestId);
405406
}
406407

408+
const othersLabel = ATCODER_OTHERS[contestId as keyof typeof ATCODER_OTHERS];
409+
410+
if (othersLabel) {
411+
return othersLabel;
412+
}
413+
407414
if (contestId.startsWith('chokudai_S')) {
408415
return contestId.replace('chokudai_S', 'Chokudai SpeedRun ');
409416
}

src/test/lib/utils/test_cases/contest_name_labels.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ export const atCoderOthers = [
4141
contestId: 'atc001',
4242
expected: 'ATC 001',
4343
}),
44+
createTestCaseForContestNameLabel('square869120Contest #4')({
45+
contestId: 's8pc-4',
46+
expected: 'square869120Contest #4',
47+
}),
4448
];
4549

4650
export const mathAndAlgorithm = [

src/test/lib/utils/test_cases/contest_type.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,10 @@ export const atCoderOthers = [
402402
contestId: 'atc001',
403403
expected: ContestType.OTHERS,
404404
}),
405+
createTestCaseForContestType('square869120Contest #4')({
406+
contestId: 's8pc-4',
407+
expected: ContestType.OTHERS,
408+
}),
405409
createTestCaseForContestType('CODE FESTIVAL 2014 qual A')({
406410
contestId: 'code-festival-2014-quala',
407411
expected: ContestType.OTHERS,

0 commit comments

Comments
 (0)