Skip to content

Commit 96b91c5

Browse files
authored
Merge pull request #1800 from AtCoder-NoviSteps/#1644
🎨 Show tasks by contest types (#1644)
2 parents 79b5ce3 + 8dd4933 commit 96b91c5

File tree

3 files changed

+317
-104
lines changed

3 files changed

+317
-104
lines changed

src/lib/components/TaskTables/TaskTable.svelte

Lines changed: 87 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,20 @@
1717
import UpdatingModal from '$lib/components/SubmissionStatus/UpdatingModal.svelte';
1818
import TaskTableBodyCell from '$lib/components/TaskTables/TaskTableBodyCell.svelte';
1919
20-
import { classifyContest, getContestNameLabel } from '$lib/utils/contest';
21-
import { getTaskTableHeaderName } from '$lib/utils/task';
20+
import {
21+
type TaskResultsFilter,
22+
taskResultsForABCLatest20,
23+
taskResultsFromABC319Onwards,
24+
taskResultsFromABC212ToABC318,
25+
} from '$lib/utils/task_results_filter';
26+
27+
import {
28+
type TaskTableGenerator,
29+
taskTableGeneratorForABCLatest20,
30+
taskTableGeneratorFromABC319Onwards,
31+
taskTableGeneratorFromABC212ToABC318,
32+
} from '$lib/utils/task_table_generator';
33+
2234
import { getBackgroundColorFrom } from '$lib/services/submission_status';
2335
2436
interface Props {
@@ -28,22 +40,57 @@
2840
2941
let { taskResults, isLoggedIn }: Props = $props();
3042
31-
// TODO: 任意のコンテスト種別に拡張
32-
// Note:
33-
// Before and from ABC212 onwards, the number and tendency of tasks are very different.
34-
const fromABC212_Onwards = (taskResult: TaskResult) =>
35-
classifyContest(taskResult.contest_id) === ContestType.ABC && taskResult.contest_id >= 'abc212';
36-
37-
let selectedTaskResults: TaskResults = $derived(
38-
filterTaskResultsByContestType(taskResults, fromABC212_Onwards),
39-
);
40-
let contestIds: Array<string> = $derived(getContestIds(selectedTaskResults));
41-
let taskTableIndices: Array<string> = $derived(
42-
getTaskTableIndices(selectedTaskResults, ContestType.ABC),
43+
// TODO: 任意のコンテスト種別を追加
44+
// TODO: コンテスト種別の並び順を決める
45+
const contestFilterConfigs = {
46+
abcLatest20Rounds: {
47+
filter: () => taskResultsForABCLatest20(taskResults),
48+
table: (results: TaskResults) => taskTableGeneratorForABCLatest20(results, ContestType.ABC),
49+
buttonLabel: 'ABC 最新 20 回',
50+
ariaLabel: 'Filter ABC latest 20 rounds',
51+
},
52+
abc319Onwards: {
53+
filter: () => taskResultsFromABC319Onwards(taskResults),
54+
table: (results: TaskResults) =>
55+
taskTableGeneratorFromABC319Onwards(results, ContestType.ABC),
56+
buttonLabel: 'ABC319 〜',
57+
ariaLabel: 'Filter contests from ABC 319 onwards',
58+
},
59+
abc212To318: {
60+
filter: () => taskResultsFromABC212ToABC318(taskResults),
61+
table: (results: TaskResults) =>
62+
taskTableGeneratorFromABC212ToABC318(results, ContestType.ABC),
63+
buttonLabel: 'ABC212 〜 318',
64+
ariaLabel: 'Filter contests from ABC 212 to ABC 318',
65+
},
66+
};
67+
68+
type ContestTypeFilter = 'abcLatest20Rounds' | 'abc319Onwards' | 'abc212To318';
69+
70+
let activeContestType = $state<ContestTypeFilter>('abcLatest20Rounds');
71+
72+
// Select the task results based on the active contest type.
73+
let taskResultsFilter: TaskResultsFilter = $derived(
74+
contestFilterConfigs[activeContestType].filter(),
4375
);
44-
let taskTable: Record<string, Record<string, TaskResult>> = $derived(
45-
prepareTaskTable(selectedTaskResults, ContestType.ABC),
76+
let selectedTaskResults: TaskResults = $derived(taskResultsFilter.run());
77+
78+
// Generate the task table based on the selected task results.
79+
let taskTableGenerator: TaskTableGenerator = $derived(
80+
contestFilterConfigs[activeContestType].table(selectedTaskResults),
4681
);
82+
let taskTable: Record<string, Record<string, TaskResult>> = $derived(taskTableGenerator.run());
83+
let taskTableHeaderIds: Array<string> = $derived(taskTableGenerator.getHeaderIdsForTask());
84+
let contestIds: Array<string> = $derived(taskTableGenerator.getContestRoundIds());
85+
86+
function getTaskTableTitle(taskTableGenerator: TaskTableGenerator): string {
87+
return taskTableGenerator.getTitle() ?? '';
88+
}
89+
90+
function getContestRoundLabel(taskTableGenerator: TaskTableGenerator, contestId: string): string {
91+
return taskTableGenerator.getContestRoundLabel(contestId);
92+
}
93+
4794
// FIXME: 他のコンポーネントと完全に重複しているので、コンポーネントとして切り出す。
4895
let updatingModal: UpdatingModal | null = null;
4996
@@ -56,71 +103,6 @@
56103
}
57104
}
58105
59-
function filterTaskResultsByContestType(
60-
taskResults: TaskResults,
61-
condition: (taskResult: TaskResult) => boolean,
62-
): TaskResults {
63-
return taskResults.filter(condition);
64-
}
65-
66-
function getContestIds(selectedTaskResults: TaskResults): Array<string> {
67-
const contestList = selectedTaskResults.map((taskResult: TaskResult) => taskResult.contest_id);
68-
return Array.from(new Set(contestList)).sort().reverse();
69-
}
70-
71-
function getTaskTableIndices(
72-
selectedTaskResults: TaskResults,
73-
selectedContestType: ContestType,
74-
): Array<string> {
75-
const headerList = selectedTaskResults.map((taskResult: TaskResult) =>
76-
getTaskTableHeaderName(selectedContestType, taskResult),
77-
);
78-
return Array.from(new Set(headerList)).sort();
79-
}
80-
81-
/**
82-
* Prepare a table for task and submission statuses.
83-
*
84-
* Computational complexity of preparation table: O(N), where N is the number of task results.
85-
* Computational complexity of accessing table: O(1).
86-
*
87-
* @param selectedTaskResults Task results to be shown in the table.
88-
* @param selectedContestType Contest type of the task results.
89-
* @returns A table for task and submission statuses.
90-
*/
91-
function prepareTaskTable(
92-
selectedTaskResults: TaskResults,
93-
selectedContestType: ContestType,
94-
): Record<string, Record<string, TaskResult>> {
95-
const table: Record<string, Record<string, TaskResult>> = {};
96-
97-
selectedTaskResults.forEach((taskResult: TaskResult) => {
98-
const contestId = taskResult.contest_id;
99-
const taskTableIndex = getTaskTableHeaderName(selectedContestType, taskResult);
100-
101-
if (!table[contestId]) {
102-
table[contestId] = {};
103-
}
104-
105-
table[contestId][taskTableIndex] = taskResult;
106-
});
107-
108-
return table;
109-
}
110-
111-
function getContestNameLabelForTaskTable(contestId: string): string {
112-
let contestNameLabel = getContestNameLabel(contestId);
113-
const contestType = classifyContest(contestId);
114-
115-
switch (contestType) {
116-
case ContestType.ABC:
117-
return contestNameLabel.replace('ABC ', '');
118-
// TODO: Add cases for other contest types.
119-
default:
120-
return contestNameLabel;
121-
}
122-
}
123-
124106
function getBodyCellClasses(contestId: string, taskIndex: string): string {
125107
const baseClasses = 'w-1/2 xs:w-1/3 sm:w-1/4 md:w-1/5 lg:w-1/6 px-1 py-1 border';
126108
const backgroundColor = getBackgroundColor(taskTable[contestId][taskIndex]);
@@ -139,21 +121,22 @@
139121
}
140122
</script>
141123

142-
<!-- TODO: コンテスト種別のボタンの並び順を決める -->
143124
<!-- See: -->
144125
<!-- https://flowbite-svelte.com/docs/components/button-group -->
145126
<ButtonGroup class="m-4 contents-center">
146-
<Button
147-
onclick={() => filterTaskResultsByContestType(taskResults, fromABC212_Onwards)}
148-
aria-label="Filter contests from ABC212 onwards"
149-
>
150-
ABC212〜
151-
</Button>
127+
{#each Object.entries(contestFilterConfigs) as [type, config]}
128+
<Button
129+
onclick={() => (activeContestType = type as ContestTypeFilter)}
130+
class={activeContestType === type ? 'active-button-class' : ''}
131+
aria-label={config.ariaLabel}
132+
>
133+
{config.buttonLabel}
134+
</Button>
135+
{/each}
152136
</ButtonGroup>
153137

154-
<!-- TODO: コンテスト種別に応じて動的に変更できるようにする -->
155138
<Heading tag="h2" class="text-2xl pb-3 text-gray-900 dark:text-white">
156-
{'AtCoder Beginners Contest 212 〜'}
139+
{getTaskTableTitle(taskTableGenerator)}
157140
</Heading>
158141

159142
<!-- TODO: ページネーションを実装 -->
@@ -163,35 +146,35 @@
163146
<div class="container w-full overflow-auto border rounded-md">
164147
<Table shadow id="task-table" class="text-md table-fixed" aria-label="Task table">
165148
<TableHead class="text-sm bg-gray-100">
166-
<TableHeadCell class="w-full xl:w-16 px-2 text-center border" scope="col">Round</TableHeadCell
167-
>
149+
<TableHeadCell class="w-full xl:w-16 px-2 text-center border" scope="col">
150+
Round
151+
</TableHeadCell>
168152

169-
{#if taskTableIndices.length}
170-
{#each taskTableIndices as taskTableIndex}
171-
<TableHeadCell class="text-center border" scope="col">{taskTableIndex}</TableHeadCell>
153+
{#if taskTableHeaderIds.length}
154+
{#each taskTableHeaderIds as taskTableHeaderId}
155+
<TableHeadCell class="text-center border" scope="col">{taskTableHeaderId}</TableHeadCell>
172156
{/each}
173157
{/if}
174158
</TableHead>
175159

176160
<TableBody class="divide-y">
177-
{#if contestIds.length && taskTableIndices.length}
161+
{#if contestIds.length && taskTableHeaderIds.length}
178162
{#each contestIds as contestId}
179163
<TableBodyRow class="flex flex-wrap xl:table-row">
180164
<TableBodyCell class="w-full xl:w-16 truncate px-2 py-2 text-center border">
181-
<!-- FIXME: コンテスト種別に合わせて修正できるようにする -->
182-
{getContestNameLabelForTaskTable(contestId)}
165+
{getContestRoundLabel(taskTableGenerator, contestId)}
183166
</TableBodyCell>
184167

185-
{#each taskTableIndices as taskIndex}
168+
{#each taskTableHeaderIds as taskTableHeaderId}
186169
<TableBodyCell
187-
id={contestId + '-' + taskIndex}
188-
class={getBodyCellClasses(contestId, taskIndex)}
170+
id={contestId + '-' + taskTableHeaderId}
171+
class={getBodyCellClasses(contestId, taskTableHeaderId)}
189172
>
190-
{#if taskTable[contestId][taskIndex]}
173+
{#if taskTable[contestId][taskTableHeaderId]}
191174
<TaskTableBodyCell
192-
taskResult={taskTable[contestId][taskIndex]}
175+
taskResult={taskTable[contestId][taskTableHeaderId]}
193176
{isLoggedIn}
194-
onClick={() => openModal(taskTable[contestId][taskIndex])}
177+
onClick={() => openModal(taskTable[contestId][taskTableHeaderId])}
195178
/>
196179
{/if}
197180
</TableBodyCell>
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import type { TaskResults, TaskResult } from '$lib/types/task';
2+
import { ContestType } from '$lib/types/contest';
3+
4+
import { classifyContest } from '$lib/utils/contest';
5+
6+
export const taskResultsForABCLatest20 = (taskResults: TaskResults) => {
7+
return new TaskResultsForABCLatest20(taskResults);
8+
};
9+
10+
export const taskResultsFromABC319Onwards = (taskResults: TaskResults) => {
11+
return new TaskResultsFromABC319Onwards(taskResults);
12+
};
13+
14+
// Note:
15+
// Before and from ABC212 onwards, the number and tendency of tasks are very different.
16+
export const taskResultsFromABC212ToABC318 = (taskResults: TaskResults) => {
17+
return new TaskResultsFromABC212ToABC318(taskResults);
18+
};
19+
20+
export abstract class TaskResultsFilter {
21+
protected taskResults: TaskResults;
22+
23+
constructor(taskResults: TaskResults) {
24+
this.taskResults = taskResults;
25+
}
26+
27+
/**
28+
* Filter the taskResults using setCondition().
29+
*
30+
* @returns {TaskResults} The results of the filtered taskResults.
31+
*/
32+
run(): TaskResults {
33+
return this.taskResults.filter(this.setCondition());
34+
}
35+
36+
/**
37+
* This is an abstract method that must be implemented by any subclass.
38+
* It is intended to set a condition that will be used to filter task results.
39+
*
40+
* @abstract
41+
* @protected
42+
* @returns {(taskResult: TaskResult) => boolean} A function that takes a TaskResult
43+
* and returns a boolean indicating whether the task result meets the condition.
44+
*/
45+
protected abstract setCondition(): (taskResult: TaskResult) => boolean;
46+
}
47+
48+
// ABC Latest 20 rounds
49+
class TaskResultsForABCLatest20 extends TaskResultsFilter {
50+
run(): TaskResults {
51+
const CONTEST_ROUND_COUNT = 20;
52+
53+
const taskResultsOnlyABC = this.taskResults.filter(this.setCondition());
54+
const latest20ContestIds = Array.from(
55+
new Set(taskResultsOnlyABC.map((taskResult: TaskResult) => taskResult.contest_id)),
56+
)
57+
.sort()
58+
.reverse()
59+
.slice(0, CONTEST_ROUND_COUNT);
60+
61+
return taskResultsOnlyABC.filter((taskResult: TaskResult) =>
62+
latest20ContestIds.includes(taskResult.contest_id),
63+
);
64+
}
65+
66+
// Note: Narrow down taskResults in advance to reduce time to display.
67+
protected setCondition(): (taskResult: TaskResult) => boolean {
68+
return (taskResult: TaskResult) => classifyContest(taskResult.contest_id) === ContestType.ABC;
69+
}
70+
}
71+
72+
// ABC319 〜 (2023/09/09 〜 )
73+
// 7 tasks per contest
74+
class TaskResultsFromABC319Onwards extends TaskResultsFilter {
75+
protected setCondition(): (taskResult: TaskResult) => boolean {
76+
return (taskResult: TaskResult) =>
77+
taskResult.contest_id >= 'abc319' && taskResult.contest_id <= 'abc999';
78+
}
79+
}
80+
81+
// ABC212 〜 ABC318 (2021/07/31 〜 2023/09/02)
82+
// 8 tasks per contest
83+
class TaskResultsFromABC212ToABC318 extends TaskResultsFilter {
84+
protected setCondition(): (taskResult: TaskResult) => boolean {
85+
return (taskResult: TaskResult) =>
86+
taskResult.contest_id >= 'abc212' && taskResult.contest_id <= 'abc318';
87+
}
88+
}

0 commit comments

Comments
 (0)