diff --git a/src/lib/components/TaskTables/TaskTable.svelte b/src/lib/components/TaskTables/TaskTable.svelte index e53f957c4..08312674c 100644 --- a/src/lib/components/TaskTables/TaskTable.svelte +++ b/src/lib/components/TaskTables/TaskTable.svelte @@ -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 { @@ -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 = $derived(getContestIds(selectedTaskResults)); - let taskTableIndices: Array = $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('abcLatest20Rounds'); + + // Select the task results based on the active contest type. + let taskResultsFilter: TaskResultsFilter = $derived( + contestFilterConfigs[activeContestType].filter(), ); - let taskTable: Record> = $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> = $derived(taskTableGenerator.run()); + let taskTableHeaderIds: Array = $derived(taskTableGenerator.getHeaderIdsForTask()); + let contestIds: Array = $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; @@ -56,71 +103,6 @@ } } - function filterTaskResultsByContestType( - taskResults: TaskResults, - condition: (taskResult: TaskResult) => boolean, - ): TaskResults { - return taskResults.filter(condition); - } - - function getContestIds(selectedTaskResults: TaskResults): Array { - const contestList = selectedTaskResults.map((taskResult: TaskResult) => taskResult.contest_id); - return Array.from(new Set(contestList)).sort().reverse(); - } - - function getTaskTableIndices( - selectedTaskResults: TaskResults, - selectedContestType: ContestType, - ): Array { - 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> { - const table: Record> = {}; - - 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]); @@ -139,21 +121,22 @@ } - - + {#each Object.entries(contestFilterConfigs) as [type, config]} + + {/each} - - {'AtCoder Beginners Contest 212 〜'} + {getTaskTableTitle(taskTableGenerator)} @@ -163,35 +146,35 @@
- Round + + Round + - {#if taskTableIndices.length} - {#each taskTableIndices as taskTableIndex} - {taskTableIndex} + {#if taskTableHeaderIds.length} + {#each taskTableHeaderIds as taskTableHeaderId} + {taskTableHeaderId} {/each} {/if} - {#if contestIds.length && taskTableIndices.length} + {#if contestIds.length && taskTableHeaderIds.length} {#each contestIds as contestId} - - {getContestNameLabelForTaskTable(contestId)} + {getContestRoundLabel(taskTableGenerator, contestId)} - {#each taskTableIndices as taskIndex} + {#each taskTableHeaderIds as taskTableHeaderId} - {#if taskTable[contestId][taskIndex]} + {#if taskTable[contestId][taskTableHeaderId]} openModal(taskTable[contestId][taskIndex])} + onClick={() => openModal(taskTable[contestId][taskTableHeaderId])} /> {/if} diff --git a/src/lib/utils/task_results_filter.ts b/src/lib/utils/task_results_filter.ts new file mode 100644 index 000000000..a1e86fecd --- /dev/null +++ b/src/lib/utils/task_results_filter.ts @@ -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'; + } +} diff --git a/src/lib/utils/task_table_generator.ts b/src/lib/utils/task_table_generator.ts new file mode 100644 index 000000000..67587d622 --- /dev/null +++ b/src/lib/utils/task_table_generator.ts @@ -0,0 +1,142 @@ +import type { TaskResults, TaskResult } from '$lib/types/task'; +import { ContestType } from '$lib/types/contest'; + +import { getContestNameLabel } from '$lib/utils/contest'; +import { getTaskTableHeaderName } from '$lib/utils/task'; + +export const taskTableGeneratorForABCLatest20 = ( + taskResults: TaskResults, + contestType: ContestType, +) => { + return new TaskTableGeneratorForABCLatest20(taskResults, contestType); +}; + +export const taskTableGeneratorFromABC319Onwards = ( + taskResults: TaskResults, + contestType: ContestType, +) => { + return new TaskTableGeneratorFromABC319Onwards(taskResults, contestType); +}; + +export const taskTableGeneratorFromABC212ToABC318 = ( + taskResults: TaskResults, + contestType: ContestType, +) => { + return new TaskTableGeneratorFromABC212ToABC318(taskResults, contestType); +}; + +export abstract class TaskTableGenerator { + protected selectedTaskResults: TaskResults; + protected contestType: ContestType; + + /** + * Creates a new TaskTableGenerator instance. + * + * @param {TaskResults} selectedTaskResults - The task results to be displayed in the table. + * @param {ContestType} contestType - The type of contest associated with these tasks. + */ + constructor(selectedTaskResults: TaskResults, contestType: ContestType) { + this.selectedTaskResults = selectedTaskResults; + this.contestType = contestType; + } + + /** + * 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). + * + * @returns A table for task and submission statuses. + */ + run(): Record> { + const table: Record> = {}; + + this.selectedTaskResults.forEach((taskResult: TaskResult) => { + const contestId = taskResult.contest_id; + const taskTableIndex = getTaskTableHeaderName(this.contestType, taskResult); + + if (!table[contestId]) { + table[contestId] = {}; + } + + table[contestId][taskTableIndex] = taskResult; + }); + + return table; + } + + /** + * Retrieves the unique identifiers for all contest rounds. + * + * @returns {Array} An array of contest round identifiers. + */ + getContestRoundIds(): Array { + const contestList = this.selectedTaskResults.map( + (taskResult: TaskResult) => taskResult.contest_id, + ); + return Array.from(new Set(contestList)).sort().reverse(); + } + + /** + * Retrieves an array of header IDs associated with the current contest tasks. + * These IDs are used to identify and display the relevant columns in the task table. + * + * @returns {Array} An array of string IDs corresponding to the header columns for the task. + */ + getHeaderIdsForTask(): Array { + const headerList = this.selectedTaskResults.map((taskResult: TaskResult) => + getTaskTableHeaderName(this.contestType, taskResult), + ); + return Array.from(new Set(headerList)).sort(); + } + + /** + * Returns a formatted label for the contest round. + * + * This abstract method must be implemented by subclasses to provide + * a string representation of the contest round that can be displayed + * in the task table. + * + * @param contestId - The ID of the contest. + * + * @returns {string} The formatted label string for the contest round. + */ + abstract getContestRoundLabel(contestId: string): string; + + /** + * Returns the title of the task table. + * + * @abstract This method must be implemented by subclasses. + * @returns A string representing the title to be displayed for the task table. + */ + abstract getTitle(): string; +} + +class TaskTableGeneratorForABC extends TaskTableGenerator { + getContestRoundLabel(contestId: string): string { + const contestNameLabel = getContestNameLabel(contestId); + return contestNameLabel.replace('ABC ', ''); + } + + getTitle(): string { + return ''; + } +} + +class TaskTableGeneratorForABCLatest20 extends TaskTableGeneratorForABC { + getTitle(): string { + return 'AtCoder Beginners Contest latest 20 rounds'; + } +} + +class TaskTableGeneratorFromABC319Onwards extends TaskTableGeneratorForABC { + getTitle(): string { + return 'AtCoder Beginners Contest 319 〜 '; + } +} + +class TaskTableGeneratorFromABC212ToABC318 extends TaskTableGeneratorForABC { + getTitle(): string { + return 'AtCoder Beginners Contest 212 〜 318'; + } +}