From 9d216e994d82d895813e40d289c4b167b91cd623 Mon Sep 17 00:00:00 2001 From: Kato Hiroki Date: Wed, 16 Jul 2025 14:04:15 +0000 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9C=A8=20Add=20typical=2090=20to=20conte?= =?UTF-8?q?st=20table=20(#2337)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- prisma/tasks.ts | 204 ++++++++++++++++++ src/lib/utils/contest_table_provider.ts | 42 ++++ .../lib/utils/contest_table_provider.test.ts | 159 +++++++++++++- 3 files changed, 404 insertions(+), 1 deletion(-) diff --git a/prisma/tasks.ts b/prisma/tasks.ts index 803e06844..1c1a6cacf 100755 --- a/prisma/tasks.ts +++ b/prisma/tasks.ts @@ -5134,6 +5134,210 @@ export const tasks = [ title: 'A. 科目選択 (Selecting Subjects)', grade: 'Q9', }, + { + id: 'typical90_cl', + contest_id: 'typical90', + problem_index: '090', + name: "Tenkei90's Last Problem(★7)", + title: "090. Tenkei90's Last Problem(★7)", + }, + { + id: 'typical90_ck', + contest_id: 'typical90', + problem_index: '089', + name: 'Partitions and Inversions(★7)', + title: '089. Partitions and Inversions(★7)', + }, + { + id: 'typical90_cj', + contest_id: 'typical90', + problem_index: '088', + name: 'Similar but Different Ways(★6)', + title: '088. Similar but Different Ways(★6)', + }, + { + id: 'typical90_ci', + contest_id: 'typical90', + problem_index: '087', + name: "Chokudai's Demand(★5)", + title: "087. Chokudai's Demand(★5)", + }, + { + id: 'typical90_ch', + contest_id: 'typical90', + problem_index: '086', + name: "Snuke's Favorite Arrays(★5)", + title: "086. Snuke's Favorite Arrays(★5)", + }, + { + id: 'typical90_cg', + contest_id: 'typical90', + problem_index: '085', + name: 'Multiplication 085(★4)', + title: '085. Multiplication 085(★4)', + }, + { + id: 'typical90_cf', + contest_id: 'typical90', + problem_index: '084', + name: 'There are two types of characters(★3)', + title: '084. There are two types of characters(★3)', + }, + { + id: 'typical90_ce', + contest_id: 'typical90', + problem_index: '083', + name: 'Colorful Graph(★6)', + title: '083. Colorful Graph(★6)', + }, + { + id: 'typical90_cd', + contest_id: 'typical90', + problem_index: '082', + name: 'Counting Numbers(★3)', + title: '082. Counting Numbers(★3)', + }, + { + id: 'typical90_cc', + contest_id: 'typical90', + problem_index: '081', + name: 'Friendly Group(★5)', + title: '081. Friendly Group(★5)', + }, + { + id: 'typical90_cb', + contest_id: 'typical90', + problem_index: '080', + name: "Let's Share Bit(★6)", + title: "080. Let's Share Bit(★6)", + }, + { + id: 'typical90_ca', + contest_id: 'typical90', + problem_index: '079', + name: 'Two by Two(★3)', + title: '079. Two by Two(★3)', + }, + { + id: 'typical90_bz', + contest_id: 'typical90', + problem_index: '078', + name: 'Easy Graph Problem(★2)', + title: '078. Easy Graph Problem(★2)', + grade: 'Q5', + }, + { + id: 'typical90_n', + contest_id: 'typical90', + problem_index: '014', + name: 'We Used to Sing a Song Together(★3)', + title: '014. We Used to Sing a Song Together(★3)', + grade: 'Q3', + }, + { + id: 'typical90_m', + contest_id: 'typical90', + problem_index: '013', + name: 'Passing(★5)', + title: '013. Passing(★5)', + grade: 'Q2', + }, + { + id: 'typical90_l', + contest_id: 'typical90', + problem_index: '012', + name: 'Red Painting(★4)', + title: '012. Red Painting(★4)', + grade: 'Q3', + }, + { + id: 'typical90_k', + contest_id: 'typical90', + problem_index: '011', + name: 'Gravy Jobs(★6)', + title: '011. Gravy Jobs(★6)', + grade: 'D1', + }, + { + id: 'typical90_j', + contest_id: 'typical90', + problem_index: '010', + name: 'Score Sum Queries(★2)', + title: '010. Score Sum Queries(★2)', + grade: 'Q4', + }, + { + id: 'typical90_i', + contest_id: 'typical90', + problem_index: '009', + name: 'Three Point Angle(★6)', + title: '009. Three Point Angle(★6)', + grade: 'Q1', + }, + { + id: 'typical90_h', + contest_id: 'typical90', + problem_index: '008', + name: 'AtCounter(★4)', + title: '008. AtCounter(★4)', + grade: 'Q2', + }, + { + id: 'typical90_g', + contest_id: 'typical90', + problem_index: '007', + name: 'CP Classes(★3)', + title: '007. CP Classes(★3)', + grade: 'Q4', + }, + { + id: 'typical90_f', + contest_id: 'typical90', + problem_index: '006', + name: 'Smallest Subsequence(★5)', + title: '006. Smallest Subsequence(★5)', + grade: 'Q1', + }, + { + id: 'typical90_e', + contest_id: 'typical90', + problem_index: '005', + name: 'Restricted Digits(★7)', + title: '005. Restricted Digits(★7)', + grade: 'D4', + }, + { + id: 'typical90_d', + contest_id: 'typical90', + problem_index: '004', + name: 'Cross Sum(★2)', + title: '004. Cross Sum(★2)', + grade: 'Q4', + }, + { + id: 'typical90_c', + contest_id: 'typical90', + problem_index: '003', + name: 'Longest Circular Road(★4)', + title: '003. Longest Circular Road(★4)', + grade: 'Q1', + }, + { + id: 'typical90_b', + contest_id: 'typical90', + problem_index: '002', + name: 'Encyclopedia of Parentheses(★3)', + title: '002. Encyclopedia of Parentheses(★3)', + grade: 'Q3', + }, + { + id: 'typical90_a', + contest_id: 'typical90', + problem_index: '001', + name: 'Yokan Party(★4)', + title: '001. Yokan Party(★4)', + grade: 'Q2', + }, { id: 'past18_a', contest_id: 'past18-open', diff --git a/src/lib/utils/contest_table_provider.ts b/src/lib/utils/contest_table_provider.ts index 825179240..34e7c74be 100644 --- a/src/lib/utils/contest_table_provider.ts +++ b/src/lib/utils/contest_table_provider.ts @@ -308,6 +308,38 @@ export class JOIFirstQualRoundProvider extends ContestTableProviderBase { } } +export class Typical90Provider extends ContestTableProviderBase { + protected setFilterCondition(): (taskResult: TaskResult) => boolean { + return (taskResult: TaskResult) => { + if (classifyContest(taskResult.contest_id) !== this.contestType) { + return false; + } + + return taskResult.contest_id === 'typical90'; + }; + } + + getMetadata(): ContestTableMetaData { + return { + title: '競プロ典型 90 問', + abbreviationName: 'typical90', + }; + } + + getDisplayConfig(): ContestTableDisplayConfig { + return { + isShownHeader: false, + isShownRoundLabel: false, + roundLabelWidth: '', // No specific width for task index in Typical90 + isShownTaskIndex: true, + }; + } + + getContestRoundLabel(contestId: string): string { + return ''; + } +} + /** * A class that manages individual provider groups * Manages multiple ContestTableProviders as a single group, @@ -459,6 +491,15 @@ export const prepareContestProviderPresets = () => { buttonLabel: 'JOI 一次予選', ariaLabel: 'Filter JOI First Qualifying Round', }).addProvider(ContestType.JOI, new JOIFirstQualRoundProvider(ContestType.JOI)), + + /** + * Single group for Typical 90 Problems + */ + Typical90: () => + new ContestTableProviderGroup(`競プロ典型 90 問`, { + buttonLabel: '競プロ典型 90 問', + ariaLabel: 'Filter Typical 90 Problems', + }).addProvider(ContestType.TYPICAL90, new Typical90Provider(ContestType.TYPICAL90)), }; }; @@ -468,6 +509,7 @@ export const contestTableProviderGroups = { fromAbc212ToAbc318: prepareContestProviderPresets().ABC212ToABC318(), dps: prepareContestProviderPresets().dps(), // Dynamic Programming (DP) Contests joiFirstQualRound: prepareContestProviderPresets().JOIFirstQualRound(), + typical90: prepareContestProviderPresets().Typical90(), }; export type ContestTableProviderGroups = keyof typeof contestTableProviderGroups; diff --git a/src/test/lib/utils/contest_table_provider.test.ts b/src/test/lib/utils/contest_table_provider.test.ts index 530778ee7..ddb5ab877 100644 --- a/src/test/lib/utils/contest_table_provider.test.ts +++ b/src/test/lib/utils/contest_table_provider.test.ts @@ -10,6 +10,7 @@ import { EDPCProvider, TDPCProvider, JOIFirstQualRoundProvider, + Typical90Provider, ContestTableProviderGroup, prepareContestProviderPresets, } from '$lib/utils/contest_table_provider'; @@ -26,6 +27,8 @@ vi.mock('$lib/utils/contest', () => ({ return ContestType.TDPC; } else if (contestId.startsWith('joi')) { return ContestType.JOI; + } else if (contestId === 'typical90') { + return ContestType.TYPICAL90; } return ContestType.OTHERS; @@ -34,7 +37,7 @@ vi.mock('$lib/utils/contest', () => ({ getContestNameLabel: vi.fn((contestId: string) => { if (contestId.startsWith('abc')) { return `ABC ${contestId.replace('abc', '')}`; - } else if (contestId === 'dp' || contestId === 'tdpc') { + } else if (contestId === 'dp' || contestId === 'tdpc' || contestId === 'typical90') { return ''; } else if (contestId.startsWith('joi')) { // First qual round @@ -395,6 +398,147 @@ describe('ContestTableProviderBase and implementations', () => { }); }); + describe('Typical90 provider', () => { + test('expects to filter tasks to include only typical90 contest', () => { + const provider = new Typical90Provider(ContestType.TYPICAL90); + const mockTypical90Tasks = [ + { contest_id: 'typical90', task_id: 'typical90_a', task_table_index: '001' }, + { contest_id: 'typical90', task_id: 'typical90_b', task_table_index: '002' }, + { contest_id: 'typical90', task_id: 'typical90_j', task_table_index: '010' }, + { contest_id: 'typical90', task_id: 'typical90_ck', task_table_index: '089' }, + { contest_id: 'typical90', task_id: 'typical90_cl', task_table_index: '090' }, + { contest_id: 'abc123', task_id: 'abc123_a', task_table_index: 'A' }, + { contest_id: 'dp', task_id: 'dp_a', task_table_index: 'A' }, + ]; + + const filtered = provider.filter(mockTypical90Tasks as any); + + expect(filtered?.every((task) => task.contest_id === 'typical90')).toBe(true); + expect(filtered?.length).toBe(5); + expect(filtered).not.toContainEqual(expect.objectContaining({ contest_id: 'abc123' })); + expect(filtered).not.toContainEqual(expect.objectContaining({ contest_id: 'dp' })); + }); + + test('expects to get correct metadata', () => { + const provider = new Typical90Provider(ContestType.TYPICAL90); + const metadata = provider.getMetadata(); + + expect(metadata.title).toBe('競プロ典型 90 問'); + expect(metadata.abbreviationName).toBe('typical90'); + }); + + test('expects to get correct display configuration', () => { + const provider = new Typical90Provider(ContestType.TYPICAL90); + const displayConfig = provider.getDisplayConfig(); + + expect(displayConfig.isShownHeader).toBe(false); + expect(displayConfig.isShownRoundLabel).toBe(false); + expect(displayConfig.roundLabelWidth).toBe(''); + expect(displayConfig.isShownTaskIndex).toBe(true); + }); + + test('expects to format contest round label correctly', () => { + const provider = new Typical90Provider(ContestType.TYPICAL90); + const label = provider.getContestRoundLabel('typical90'); + + expect(label).toBe(''); + }); + + test('expects to generate correct table structure', () => { + const provider = new Typical90Provider(ContestType.TYPICAL90); + const mockTypical90Tasks = [ + { contest_id: 'typical90', task_id: 'typical90_a', task_table_index: '001' }, + { contest_id: 'typical90', task_id: 'typical90_b', task_table_index: '002' }, + { contest_id: 'typical90', task_id: 'typical90_c', task_table_index: '003' }, + { contest_id: 'typical90', task_id: 'typical90_j', task_table_index: '010' }, + { contest_id: 'typical90', task_id: 'typical90_ck', task_table_index: '089' }, + { contest_id: 'typical90', task_id: 'typical90_cl', task_table_index: '090' }, + ]; + + const table = provider.generateTable(mockTypical90Tasks as any); + + expect(table).toHaveProperty('typical90'); + expect(table.typical90).toHaveProperty('001'); + expect(table.typical90).toHaveProperty('002'); + expect(table.typical90).toHaveProperty('003'); + expect(table.typical90).toHaveProperty('010'); + expect(table.typical90).toHaveProperty('089'); + expect(table.typical90).toHaveProperty('090'); + expect(table.typical90['001']).toEqual( + expect.objectContaining({ contest_id: 'typical90', task_id: 'typical90_a' }), + ); + expect(table.typical90['002']).toEqual( + expect.objectContaining({ contest_id: 'typical90', task_id: 'typical90_b' }), + ); + expect(table.typical90['003']).toEqual( + expect.objectContaining({ contest_id: 'typical90', task_id: 'typical90_c' }), + ); + expect(table.typical90['010']).toEqual( + expect.objectContaining({ contest_id: 'typical90', task_id: 'typical90_j' }), + ); + expect(table.typical90['089']).toEqual( + expect.objectContaining({ contest_id: 'typical90', task_id: 'typical90_ck' }), + ); + expect(table.typical90['090']).toEqual( + expect.objectContaining({ contest_id: 'typical90', task_id: 'typical90_cl' }), + ); + }); + + test('expects to get contest round IDs correctly', () => { + const provider = new Typical90Provider(ContestType.TYPICAL90); + const mockTypical90Tasks = [ + { contest_id: 'typical90', task_id: 'typical90_a', task_table_index: '001' }, + { contest_id: 'typical90', task_id: 'typical90_b', task_table_index: '002' }, + { contest_id: 'typical90', task_id: 'typical90_c', task_table_index: '003' }, + { contest_id: 'typical90', task_id: 'typical90_j', task_table_index: '010' }, + { contest_id: 'typical90', task_id: 'typical90_ck', task_table_index: '089' }, + { contest_id: 'typical90', task_id: 'typical90_cl', task_table_index: '090' }, + ]; + + const roundIds = provider.getContestRoundIds(mockTypical90Tasks as any); + + expect(roundIds).toEqual(['typical90']); + }); + + test('expects to get header IDs for tasks correctly', () => { + const provider = new Typical90Provider(ContestType.TYPICAL90); + const mockTypical90Tasks = [ + { contest_id: 'typical90', task_id: 'typical90_a', task_table_index: '001' }, + { contest_id: 'typical90', task_id: 'typical90_b', task_table_index: '002' }, + { contest_id: 'typical90', task_id: 'typical90_c', task_table_index: '003' }, + { contest_id: 'typical90', task_id: 'typical90_d', task_table_index: '004' }, + { contest_id: 'typical90', task_id: 'typical90_e', task_table_index: '005' }, + { contest_id: 'typical90', task_id: 'typical90_j', task_table_index: '010' }, + { contest_id: 'typical90', task_id: 'typical90_ck', task_table_index: '089' }, + { contest_id: 'typical90', task_id: 'typical90_cl', task_table_index: '090' }, + ]; + + const headerIds = provider.getHeaderIdsForTask(mockTypical90Tasks as any); + + expect(headerIds).toEqual(['001', '002', '003', '004', '005', '010', '089', '090']); + }); + + test('expects to handle empty task results', () => { + const provider = new Typical90Provider(ContestType.TYPICAL90); + const filtered = provider.filter([]); + + expect(filtered).toEqual([]); + }); + + test('expects to handle task results with different contest types', () => { + const provider = new Typical90Provider(ContestType.TYPICAL90); + const mockMixedTasks = [ + { contest_id: 'abc123', task_id: 'abc123_a', task_table_index: 'A' }, + { contest_id: 'dp', task_id: 'dp_a', task_table_index: 'A' }, + { contest_id: 'tdpc', task_id: 'tdpc_a', task_table_index: 'A' }, + ]; + + const filtered = provider.filter(mockMixedTasks as any); + + expect(filtered).toEqual([]); + }); + }); + describe('Common provider functionality', () => { test('expects to get contest round IDs correctly', () => { const provider = new ABCLatest20RoundsProvider(ContestType.ABC); @@ -570,6 +714,18 @@ describe('prepareContestProviderPresets', () => { expect(group.getProvider(ContestType.TDPC)).toBeInstanceOf(TDPCProvider); }); + test('expects to create Typical90 preset correctly', () => { + const group = prepareContestProviderPresets().Typical90(); + + expect(group.getGroupName()).toBe('競プロ典型 90 問'); + expect(group.getMetadata()).toEqual({ + buttonLabel: '競プロ典型 90 問', + ariaLabel: 'Filter Typical 90 Problems', + }); + expect(group.getSize()).toBe(1); + expect(group.getProvider(ContestType.TYPICAL90)).toBeInstanceOf(Typical90Provider); + }); + test('expects to verify all presets are functions', () => { const presets = prepareContestProviderPresets(); @@ -577,6 +733,7 @@ describe('prepareContestProviderPresets', () => { expect(typeof presets.ABC319Onwards).toBe('function'); expect(typeof presets.ABC212ToABC318).toBe('function'); expect(typeof presets.dps).toBe('function'); + expect(typeof presets.Typical90).toBe('function'); }); test('expects each preset to create independent instances', () => { From 8d232bf24df36926d3421c6b9c380c6684227d8a Mon Sep 17 00:00:00 2001 From: "Kato, H." Date: Wed, 16 Jul 2025 23:08:43 +0900 Subject: [PATCH 2/2] :pencil2: Fix description (#2337) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/lib/utils/contest_table_provider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/utils/contest_table_provider.ts b/src/lib/utils/contest_table_provider.ts index 34e7c74be..5d739f23d 100644 --- a/src/lib/utils/contest_table_provider.ts +++ b/src/lib/utils/contest_table_provider.ts @@ -330,7 +330,7 @@ export class Typical90Provider extends ContestTableProviderBase { return { isShownHeader: false, isShownRoundLabel: false, - roundLabelWidth: '', // No specific width for task index in Typical90 + roundLabelWidth: '', // No specific width for the round label in Typical90 isShownTaskIndex: true, }; }