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
5 changes: 3 additions & 2 deletions docs/guides/how-to-add-contest-table-provider.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,11 +116,12 @@ AOJ/JAG 固有の詳細(`AOJ_LABEL_OVERRIDES`、`titleStyle`、同一年2回
| NDPC | `'ndpc'` | 20問 | A~T |
| FPS_24 | `'fps-24'` | 24問 | A~X |
| GAME_24‡ | `'game-24'` | 24問 | A~X |
| AALC | `'aalc001'` | 24問 | A~X |
| ACL_PRACTICE | `'practice2'` | 12問 | A~L |
| ACL_BEGINNER / ACL_CONTEST1 | `'abl'`/`'acl1'` | 6問 | A~F |
| AWC0100 / AWC0150† | `'awc0100'`/`'awc0150'` | 15問 | A~O |

†注: ContestType.AWC を再利用し、section(`'0100'`, `'0150'`, `'0151Onwards'`)で provider key を一意化。ACL 系は `Acl` グループ、DP 系は `dps` グループで統一管理。
†注: ContestType.AWC を再利用し、section(`'0100'`, `'0150'`, `'0151Onwards'`)で provider key を一意化。ACL 系は `Acl` グループ、DP 系と x 24 題系(FPS_24 / GAME_24 / AALC)は `dps` グループで統一管理。

‡注: task_id は `game_a`〜`game_x` で、contest_id の数字を含まない(`fps-24` は `fps_24_a`)。

Expand Down Expand Up @@ -169,4 +170,4 @@ AOJ/JAG 固有の詳細(`AOJ_LABEL_OVERRIDES`、`titleStyle`、同一年2回

---

**最終更新**: 2026-09-05
**最終更新**: 2026-10-13
5 changes: 4 additions & 1 deletion e2e/problems_contest_table.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@ const XDPC_GROUP_TABLE_TITLES = [
'Next DP Contest',
'FPS 24 題',
'組合せゲーム 24 題',
"AAL Contest 001: Let's use segtree!",
];

test.describe('xDPC group in contest table', () => {
test('shows Game 24 table right after FPS 24 in the xDPC group', async ({ page }) => {
test('shows Game 24 and AAL Contest 001 tables after FPS 24 in the xDPC group', async ({
page,
}) => {
await page.goto('/problems');

await page.getByRole('button', { name: XDPC_GROUP_ARIA_LABEL }).click();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "ContestType" ADD VALUE 'AALC';
1 change: 1 addition & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,7 @@ enum ContestType {
UNIVERSITY // University Programming Contests in AtCoder (e.g., UTPC)
FPS_24 // 24 Problems on Formal Power Series
GAME_24 // 24 Problems on Combinatorial Games
AALC // AAL Contest 001: Let's use segtree!
ATCODER_MAIN_OFFICIAL_ONSITE // AtCoder World Tour Finals (official onsite finals)
OTHERS // AtCoder (その他)
AOJ_COURSES // AIZU ONLINE JUDGE Courses
Expand Down
168 changes: 168 additions & 0 deletions prisma/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8935,6 +8935,174 @@ export const tasks = [
name: 'Bacteria Growth Experiment',
title: 'A. Bacteria Growth Experiment',
},
{
id: 'aalc001_x',
contest_id: 'aalc001',
problem_index: 'X',
name: 'Unevenness',
title: 'X. Unevenness',
},
{
id: 'aalc001_w',
contest_id: 'aalc001',
problem_index: 'W',
name: 'Adjacent Add',
title: 'W. Adjacent Add',
},
{
id: 'aalc001_v',
contest_id: 'aalc001',
problem_index: 'V',
name: 'Connectivity 2',
title: 'V. Connectivity 2',
},
{
id: 'aalc001_u',
contest_id: 'aalc001',
problem_index: 'U',
name: 'Second',
title: 'U. Second',
},
{
id: 'aalc001_t',
contest_id: 'aalc001',
problem_index: 'T',
name: 'K-Inversion Count',
title: 'T. K-Inversion Count',
},
{
id: 'aalc001_s',
contest_id: 'aalc001',
problem_index: 'S',
name: 'Absolute Value',
title: 'S. Absolute Value',
},
{
id: 'aalc001_r',
contest_id: 'aalc001',
problem_index: 'R',
name: 'Connectivity',
title: 'R. Connectivity',
},
{
id: 'aalc001_q',
contest_id: 'aalc001',
problem_index: 'Q',
name: 'Number of Moves',
title: 'Q. Number of Moves',
},
{
id: 'aalc001_p',
contest_id: 'aalc001',
problem_index: 'P',
name: 'Connect the Dots',
title: 'P. Connect the Dots',
},
{
id: 'aalc001_o',
contest_id: 'aalc001',
problem_index: 'O',
name: 'Minimum Value',
title: 'O. Minimum Value',
},
{
id: 'aalc001_n',
contest_id: 'aalc001',
problem_index: 'N',
name: 'Times Tables',
title: 'N. Times Tables',
},
{
id: 'aalc001_m',
contest_id: 'aalc001',
problem_index: 'M',
name: 'NAND',
title: 'M. NAND',
},
{
id: 'aalc001_l',
contest_id: 'aalc001',
problem_index: 'L',
name: 'Swap between Arrays',
title: 'L. Swap between Arrays',
},
{
id: 'aalc001_k',
contest_id: 'aalc001',
problem_index: 'K',
name: 'Multiset',
title: 'K. Multiset',
},
{
id: 'aalc001_j',
contest_id: 'aalc001',
problem_index: 'J',
name: 'Long Binary String',
title: 'J. Long Binary String',
},
{
id: 'aalc001_i',
contest_id: 'aalc001',
problem_index: 'I',
name: 'Subarray',
title: 'I. Subarray',
},
{
id: 'aalc001_h',
contest_id: 'aalc001',
problem_index: 'H',
name: 'Add and Mod',
title: 'H. Add and Mod',
},
{
id: 'aalc001_g',
contest_id: 'aalc001',
problem_index: 'G',
name: 'Sum of k-th Powers',
title: 'G. Sum of k-th Powers',
},
{
id: 'aalc001_f',
contest_id: 'aalc001',
problem_index: 'F',
name: 'Run',
title: 'F. Run',
},
{
id: 'aalc001_e',
contest_id: 'aalc001',
problem_index: 'E',
name: 'RGB',
title: 'E. RGB',
},
{
id: 'aalc001_d',
contest_id: 'aalc001',
problem_index: 'D',
name: 'Inversion Count',
title: 'D. Inversion Count',
},
{
id: 'aalc001_c',
contest_id: 'aalc001',
problem_index: 'C',
name: 'Binary Search on a Segment Tree',
title: 'C. Binary Search on a Segment Tree',
},
{
id: 'aalc001_b',
contest_id: 'aalc001',
problem_index: 'B',
name: 'Lazy Segment Tree',
title: 'B. Lazy Segment Tree',
},
{
id: 'aalc001_a',
contest_id: 'aalc001',
problem_index: 'A',
name: 'Segment Tree',
title: 'A. Segment Tree',
},
{
id: 'game_x',
contest_id: 'game-24',
Expand Down
108 changes: 108 additions & 0 deletions src/features/tasks/utils/contest-table/aalc_provider.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { describe, test, expect } from 'vitest';

import { ContestType } from '$lib/contests/types/contest';
import type { TaskResults } from '$lib/types/task';

import { AALCProvider } from './aalc_provider';

describe('AALCProvider', () => {
test('expects to get correct metadata', () => {
const provider = new AALCProvider(ContestType.AALC);
const metadata = provider.getMetadata();

expect(metadata.title).toBe("AAL Contest 001: Let's use segtree!");
expect(metadata.abbreviationName).toBe('aalc001');
});

test('expects to get correct display configuration', () => {
const provider = new AALCProvider(ContestType.AALC);
const displayConfig = provider.getDisplayConfig();

expect(displayConfig.isShownHeader).toBe(false);
expect(displayConfig.isShownRoundLabel).toBe(false);
expect(displayConfig.roundLabelWidth).toBe('');
expect(displayConfig.tableBodyCellsWidth).toBe(
'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',
);
expect(displayConfig.isShownTaskIndex).toBe(true);
});

test('expects to format contest round label correctly', () => {
const provider = new AALCProvider(ContestType.AALC);
const label = provider.getContestRoundLabel('');

expect(label).toBe('');
});

test('expects to filter tasks to include only aalc001 contest', () => {
const provider = new AALCProvider(ContestType.AALC);
const mixedTasks = [
{ contest_id: 'abc123', task_id: 'abc123_a', task_table_index: 'A' },
{ contest_id: 'aalc001', task_id: 'aalc001_a', task_table_index: 'A' },
{ contest_id: 'game-24', task_id: 'game_a', task_table_index: 'A' },
{ contest_id: 'aalc001', task_id: 'aalc001_b', task_table_index: 'B' },
];
const filtered = provider.filter(mixedTasks as TaskResults);

expect(filtered?.map((task) => task.task_id)).toEqual(['aalc001_a', 'aalc001_b']);
});

test('expects to generate correct table structure', () => {
const provider = new AALCProvider(ContestType.AALC);
const tasks = [
{ contest_id: 'aalc001', task_id: 'aalc001_a', task_table_index: 'A' },
{ contest_id: 'aalc001', task_id: 'aalc001_b', task_table_index: 'B' },
{ contest_id: 'aalc001', task_id: 'aalc001_x', task_table_index: 'X' },
];
const table = provider.generateTable(tasks as TaskResults);

expect(table).toHaveProperty('aalc001');
expect(table['aalc001']).toHaveProperty('A');
expect(table['aalc001']).toHaveProperty('B');
expect(table['aalc001']).toHaveProperty('X');
expect(table['aalc001']['A']).toEqual(expect.objectContaining({ task_id: 'aalc001_a' }));
});

test('expects to get contest round IDs correctly', () => {
const provider = new AALCProvider(ContestType.AALC);
const tasks = [
{ contest_id: 'aalc001', task_id: 'aalc001_a', task_table_index: 'A' },
{ contest_id: 'aalc001', task_id: 'aalc001_x', task_table_index: 'X' },
];
const roundIds = provider.getContestRoundIds(tasks as TaskResults);

expect(roundIds).toEqual(['aalc001']);
});

test('expects to get header IDs for tasks correctly in ascending order', () => {
const provider = new AALCProvider(ContestType.AALC);
const tasks = [
{ contest_id: 'aalc001', task_id: 'aalc001_a', task_table_index: 'A' },
{ contest_id: 'aalc001', task_id: 'aalc001_x', task_table_index: 'X' },
{ contest_id: 'aalc001', task_id: 'aalc001_m', task_table_index: 'M' },
{ contest_id: 'aalc001', task_id: 'aalc001_b', task_table_index: 'B' },
];
const headerIds = provider.getHeaderIdsForTask(tasks as TaskResults);

expect(headerIds).toEqual(['A', 'B', 'M', 'X']);
});

test('expects to handle empty task results', () => {
const provider = new AALCProvider(ContestType.AALC);
const filtered = provider.filter([] as TaskResults);

expect(filtered).toEqual([] as TaskResults);
});

test('expects to handle task results with different contest types', () => {
const provider = new AALCProvider(ContestType.AALC);
const mockMixedTasks = [
{ contest_id: 'abc123', task_id: 'abc123_a', task_table_index: 'A' },
{ contest_id: 'practice2', task_id: 'practice2_a', task_table_index: 'A' },
{ contest_id: 'game-24', task_id: 'game_a', task_table_index: 'A' },
];
const filtered = provider.filter(mockMixedTasks as TaskResults);

expect(filtered).toEqual([] as TaskResults);
});
});
42 changes: 42 additions & 0 deletions src/features/tasks/utils/contest-table/aalc_provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {
type ContestTableMetaData,
type ContestTableDisplayConfig,
} from '$features/tasks/types/contest-table/contest_table_provider';
import type { TaskResult } from '$lib/types/task';

import { classifyContest } from '$lib/contests';

import { ContestTableProviderBase } from './contest_table_provider_base';

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

return taskResult.contest_id === 'aalc001';
};
}

getMetadata(): ContestTableMetaData {
return {
title: "AAL Contest 001: Let's use segtree!",
abbreviationName: 'aalc001',
};
}

getDisplayConfig(): ContestTableDisplayConfig {
return {
isShownHeader: false,
isShownRoundLabel: false,
roundLabelWidth: '', // No specific width for task index in AAL Contest 001
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 '';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -168,22 +168,23 @@ describe('prepareContestProviderPresets', () => {
}
});

test('dps registers EDPC, TDPC, NDPC, FPS 24, and Game 24 providers', () => {
test('dps registers EDPC, TDPC, NDPC, FPS 24, Game 24, and AAL Contest 001 providers', () => {
const group = presets.dps();

expect(group.getGroupName()).toBe('xDPC・x 24 題');
expect(group.getMetadata()).toEqual({
buttonLabel: 'xDPC・x 24 題',
ariaLabel: 'xDPC and x 24 contests',
});
expect(group.getSize()).toBe(5);
expect(group.getSize()).toBe(6);

const contestTypes = [
ContestType.EDPC,
ContestType.TDPC,
ContestType.NDPC,
ContestType.FPS_24,
ContestType.GAME_24,
ContestType.AALC,
];

for (const contestType of contestTypes) {
Expand Down
Loading
Loading