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
191 changes: 87 additions & 104 deletions src/lib/components/TaskTables/TaskTable.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,20 @@
import UpdatingModal from '$lib/components/SubmissionStatus/UpdatingModal.svelte';
import TaskTableBodyCell from '$lib/components/TaskTables/TaskTableBodyCell.svelte';
import { classifyContest, getContestNameLabel } from '$lib/utils/contest';
import { getTaskTableHeaderName } from '$lib/utils/task';
import {
type TaskResultsFilter,
taskResultsForABCLatest20,
taskResultsFromABC319Onwards,
taskResultsFromABC212ToABC318,
} from '$lib/utils/task_results_filter';
import {
type TaskTableGenerator,
taskTableGeneratorForABCLatest20,
taskTableGeneratorFromABC319Onwards,
taskTableGeneratorFromABC212ToABC318,
} from '$lib/utils/task_table_generator';
import { getBackgroundColorFrom } from '$lib/services/submission_status';
interface Props {
Expand All @@ -28,22 +40,57 @@
let { taskResults, isLoggedIn }: Props = $props();
// TODO: 任意のコンテスト種別に拡張
// Note:
// Before and from ABC212 onwards, the number and tendency of tasks are very different.
const fromABC212_Onwards = (taskResult: TaskResult) =>
classifyContest(taskResult.contest_id) === ContestType.ABC && taskResult.contest_id >= 'abc212';
let selectedTaskResults: TaskResults = $derived(
filterTaskResultsByContestType(taskResults, fromABC212_Onwards),
);
let contestIds: Array<string> = $derived(getContestIds(selectedTaskResults));
let taskTableIndices: Array<string> = $derived(
getTaskTableIndices(selectedTaskResults, ContestType.ABC),
// TODO: 任意のコンテスト種別を追加
// TODO: コンテスト種別の並び順を決める
const contestFilterConfigs = {
abcLatest20Rounds: {
filter: () => taskResultsForABCLatest20(taskResults),
table: (results: TaskResults) => taskTableGeneratorForABCLatest20(results, ContestType.ABC),
buttonLabel: 'ABC 最新 20 回',
ariaLabel: 'Filter ABC latest 20 rounds',
},
abc319Onwards: {
filter: () => taskResultsFromABC319Onwards(taskResults),
table: (results: TaskResults) =>
taskTableGeneratorFromABC319Onwards(results, ContestType.ABC),
buttonLabel: 'ABC319 〜',
ariaLabel: 'Filter contests from ABC 319 onwards',
},
abc212To318: {
filter: () => taskResultsFromABC212ToABC318(taskResults),
table: (results: TaskResults) =>
taskTableGeneratorFromABC212ToABC318(results, ContestType.ABC),
buttonLabel: 'ABC212 〜 318',
ariaLabel: 'Filter contests from ABC 212 to ABC 318',
},
};
type ContestTypeFilter = 'abcLatest20Rounds' | 'abc319Onwards' | 'abc212To318';
let activeContestType = $state<ContestTypeFilter>('abcLatest20Rounds');
// Select the task results based on the active contest type.
let taskResultsFilter: TaskResultsFilter = $derived(
contestFilterConfigs[activeContestType].filter(),
);
let taskTable: Record<string, Record<string, TaskResult>> = $derived(
prepareTaskTable(selectedTaskResults, ContestType.ABC),
let selectedTaskResults: TaskResults = $derived(taskResultsFilter.run());
// Generate the task table based on the selected task results.
let taskTableGenerator: TaskTableGenerator = $derived(
contestFilterConfigs[activeContestType].table(selectedTaskResults),
);
let taskTable: Record<string, Record<string, TaskResult>> = $derived(taskTableGenerator.run());
let taskTableHeaderIds: Array<string> = $derived(taskTableGenerator.getHeaderIdsForTask());
let contestIds: Array<string> = $derived(taskTableGenerator.getContestRoundIds());
function getTaskTableTitle(taskTableGenerator: TaskTableGenerator): string {
return taskTableGenerator.getTitle() ?? '';
}
function getContestRoundLabel(taskTableGenerator: TaskTableGenerator, contestId: string): string {
return taskTableGenerator.getContestRoundLabel(contestId);
}
// FIXME: 他のコンポーネントと完全に重複しているので、コンポーネントとして切り出す。
let updatingModal: UpdatingModal | null = null;
Expand All @@ -56,71 +103,6 @@
}
}
function filterTaskResultsByContestType(
taskResults: TaskResults,
condition: (taskResult: TaskResult) => boolean,
): TaskResults {
return taskResults.filter(condition);
}
function getContestIds(selectedTaskResults: TaskResults): Array<string> {
const contestList = selectedTaskResults.map((taskResult: TaskResult) => taskResult.contest_id);
return Array.from(new Set(contestList)).sort().reverse();
}
function getTaskTableIndices(
selectedTaskResults: TaskResults,
selectedContestType: ContestType,
): Array<string> {
const headerList = selectedTaskResults.map((taskResult: TaskResult) =>
getTaskTableHeaderName(selectedContestType, taskResult),
);
return Array.from(new Set(headerList)).sort();
}
/**
* Prepare a table for task and submission statuses.
*
* Computational complexity of preparation table: O(N), where N is the number of task results.
* Computational complexity of accessing table: O(1).
*
* @param selectedTaskResults Task results to be shown in the table.
* @param selectedContestType Contest type of the task results.
* @returns A table for task and submission statuses.
*/
function prepareTaskTable(
selectedTaskResults: TaskResults,
selectedContestType: ContestType,
): Record<string, Record<string, TaskResult>> {
const table: Record<string, Record<string, TaskResult>> = {};
selectedTaskResults.forEach((taskResult: TaskResult) => {
const contestId = taskResult.contest_id;
const taskTableIndex = getTaskTableHeaderName(selectedContestType, taskResult);
if (!table[contestId]) {
table[contestId] = {};
}
table[contestId][taskTableIndex] = taskResult;
});
return table;
}
function getContestNameLabelForTaskTable(contestId: string): string {
let contestNameLabel = getContestNameLabel(contestId);
const contestType = classifyContest(contestId);
switch (contestType) {
case ContestType.ABC:
return contestNameLabel.replace('ABC ', '');
// TODO: Add cases for other contest types.
default:
return contestNameLabel;
}
}
function getBodyCellClasses(contestId: string, taskIndex: string): string {
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';
const backgroundColor = getBackgroundColor(taskTable[contestId][taskIndex]);
Expand All @@ -139,21 +121,22 @@
}
</script>

<!-- TODO: コンテスト種別のボタンの並び順を決める -->
<!-- See: -->
<!-- https://flowbite-svelte.com/docs/components/button-group -->
<ButtonGroup class="m-4 contents-center">
<Button
onclick={() => filterTaskResultsByContestType(taskResults, fromABC212_Onwards)}
aria-label="Filter contests from ABC212 onwards"
>
ABC212〜
</Button>
{#each Object.entries(contestFilterConfigs) as [type, config]}
<Button
onclick={() => (activeContestType = type as ContestTypeFilter)}
class={activeContestType === type ? 'active-button-class' : ''}
aria-label={config.ariaLabel}
>
{config.buttonLabel}
</Button>
{/each}
</ButtonGroup>

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

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

{#if taskTableIndices.length}
{#each taskTableIndices as taskTableIndex}
<TableHeadCell class="text-center border" scope="col">{taskTableIndex}</TableHeadCell>
{#if taskTableHeaderIds.length}
{#each taskTableHeaderIds as taskTableHeaderId}
<TableHeadCell class="text-center border" scope="col">{taskTableHeaderId}</TableHeadCell>
{/each}
{/if}
</TableHead>

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

{#each taskTableIndices as taskIndex}
{#each taskTableHeaderIds as taskTableHeaderId}
<TableBodyCell
id={contestId + '-' + taskIndex}
class={getBodyCellClasses(contestId, taskIndex)}
id={contestId + '-' + taskTableHeaderId}
class={getBodyCellClasses(contestId, taskTableHeaderId)}
>
{#if taskTable[contestId][taskIndex]}
{#if taskTable[contestId][taskTableHeaderId]}
<TaskTableBodyCell
taskResult={taskTable[contestId][taskIndex]}
taskResult={taskTable[contestId][taskTableHeaderId]}
{isLoggedIn}
onClick={() => openModal(taskTable[contestId][taskIndex])}
onClick={() => openModal(taskTable[contestId][taskTableHeaderId])}
/>
{/if}
</TableBodyCell>
Expand Down
88 changes: 88 additions & 0 deletions src/lib/utils/task_results_filter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import type { TaskResults, TaskResult } from '$lib/types/task';
import { ContestType } from '$lib/types/contest';

import { classifyContest } from '$lib/utils/contest';

export const taskResultsForABCLatest20 = (taskResults: TaskResults) => {
return new TaskResultsForABCLatest20(taskResults);
};

export const taskResultsFromABC319Onwards = (taskResults: TaskResults) => {
return new TaskResultsFromABC319Onwards(taskResults);
};

// Note:
// Before and from ABC212 onwards, the number and tendency of tasks are very different.
export const taskResultsFromABC212ToABC318 = (taskResults: TaskResults) => {
return new TaskResultsFromABC212ToABC318(taskResults);
};

export abstract class TaskResultsFilter {
protected taskResults: TaskResults;

constructor(taskResults: TaskResults) {
this.taskResults = taskResults;
}

/**
* Filter the taskResults using setCondition().
*
* @returns {TaskResults} The results of the filtered taskResults.
*/
run(): TaskResults {
return this.taskResults.filter(this.setCondition());
}

/**
* This is an abstract method that must be implemented by any subclass.
* It is intended to set a condition that will be used to filter task results.
*
* @abstract
* @protected
* @returns {(taskResult: TaskResult) => boolean} A function that takes a TaskResult
* and returns a boolean indicating whether the task result meets the condition.
*/
protected abstract setCondition(): (taskResult: TaskResult) => boolean;
}

// ABC Latest 20 rounds
class TaskResultsForABCLatest20 extends TaskResultsFilter {
run(): TaskResults {
const CONTEST_ROUND_COUNT = 20;

const taskResultsOnlyABC = this.taskResults.filter(this.setCondition());
const latest20ContestIds = Array.from(
new Set(taskResultsOnlyABC.map((taskResult: TaskResult) => taskResult.contest_id)),
)
.sort()
.reverse()
.slice(0, CONTEST_ROUND_COUNT);

return taskResultsOnlyABC.filter((taskResult: TaskResult) =>
latest20ContestIds.includes(taskResult.contest_id),
);
}

// Note: Narrow down taskResults in advance to reduce time to display.
protected setCondition(): (taskResult: TaskResult) => boolean {
return (taskResult: TaskResult) => classifyContest(taskResult.contest_id) === ContestType.ABC;
}
}

// ABC319 〜 (2023/09/09 〜 )
// 7 tasks per contest
class TaskResultsFromABC319Onwards extends TaskResultsFilter {
protected setCondition(): (taskResult: TaskResult) => boolean {
return (taskResult: TaskResult) =>
taskResult.contest_id >= 'abc319' && taskResult.contest_id <= 'abc999';
}
}

// ABC212 〜 ABC318 (2021/07/31 〜 2023/09/02)
// 8 tasks per contest
class TaskResultsFromABC212ToABC318 extends TaskResultsFilter {
protected setCondition(): (taskResult: TaskResult) => boolean {
return (taskResult: TaskResult) =>
taskResult.contest_id >= 'abc212' && taskResult.contest_id <= 'abc318';
}
}
Loading
Loading