Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
c4603f2
✨ Add tasks for AOJ JAG prelim and regional (#1514)
KATO-Hiro Nov 20, 2024
55cfc64
:books: Update docs (#1514)
KATO-Hiro Nov 20, 2024
d5f5e36
Merge branch 'staging' of github.com:KATO-Hiro/AtCoderNoviceProblemsS…
KATO-Hiro Nov 20, 2024
2f9d594
🚨 Add tests for contests (#1514)
KATO-Hiro Nov 21, 2024
862d9ba
🚨 Add tests for task url (#1514)
KATO-Hiro Nov 21, 2024
f5aced1
Merge branch 'staging' of github.com:KATO-Hiro/AtCoderNoviceProblemsS…
KATO-Hiro Nov 21, 2024
47c0238
♻️ Refactoring (#1514)
KATO-Hiro Nov 22, 2024
749e84b
:books: Update docs (#1514)
KATO-Hiro Nov 22, 2024
e1f51ae
✨ Add a simple in-memory cache (#1514)
KATO-Hiro Nov 22, 2024
118135c
✨ Add time-based expiration to cache (#1514)
KATO-Hiro Nov 22, 2024
9b26c66
:books: Update docs (#1514)
KATO-Hiro Nov 22, 2024
8f8b842
✨ Move cache to class and add validation to buildEndpoint (#1514)
KATO-Hiro Nov 22, 2024
0401343
Merge branch 'staging' of github.com:KATO-Hiro/AtCoderNoviceProblemsS…
KATO-Hiro Nov 22, 2024
ff36e3d
♻️ Improve cache and buildEndpoint (#1514)
KATO-Hiro Nov 23, 2024
a79a312
:books: Update docs (#1514)
KATO-Hiro Nov 23, 2024
ae1285d
♻️ Make cache parameters configurable (#1514)
KATO-Hiro Nov 23, 2024
af1ccae
♻️ Improve cache and buildEndpoint (#1514)
KATO-Hiro Nov 23, 2024
c72c67d
♻️ Improve cache and error handlings (#1514)
KATO-Hiro Nov 23, 2024
c97d5a1
♻️ Improve cache and buildEndpoint (#1514)
KATO-Hiro Nov 23, 2024
7f7a9ad
♻️ Improve cache and buildEndpoint (#1514)
KATO-Hiro Nov 23, 2024
2e0f95c
♻️ Improve cache and buildEndpoint (#1514)
KATO-Hiro Nov 23, 2024
7cd1e41
♻️ Improve cache and types (#1514)
KATO-Hiro Nov 23, 2024
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
1 change: 1 addition & 0 deletions prisma/ERD.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ UNIVERSITY UNIVERSITY
OTHERS OTHERS
AOJ_COURSES AOJ_COURSES
AOJ_PCK AOJ_PCK
AOJ_JAG AOJ_JAG
}


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "ContestType" ADD VALUE 'AOJ_JAG';
1 change: 1 addition & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ enum ContestType {
OTHERS // AtCoder (その他)
AOJ_COURSES // AIZU ONLINE JUDGE Courses
AOJ_PCK // All-Japan High School Programming Contest (PCK)
AOJ_JAG // ACM-ICPC Japan Alumni Group Contest (JAG)
}

// 11Q(最も簡単)〜6D(最難関)。
Expand Down
145 changes: 114 additions & 31 deletions src/lib/clients/aizu_online_judge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ type AOJChallengeContestAPI = {
readonly contests: ChallengeContests;
};

/**
* Enum representing the types of challenge contests available.
*/
enum ChallengeContestType {
PCK = 'pck',
JAG = 'jag',
}

/**
* Represents a challenge contest in the AOJ
*/
Expand Down Expand Up @@ -80,12 +88,47 @@ enum PckRound {
FINAL = 'final',
}

/**
* Enum representing JAG contest rounds
*/
enum JagRound {
PRELIM = 'prelim',
REGIONAL = 'regional',
}

/**
* A map that associates each type of challenge contest with its corresponding round type.
*
* @typedef {Object} ChallengeRoundMap
* @property {PckRound} ChallengeContestType.PCK - The round type for PCK contests.
* @property {JagRound} ChallengeContestType.JAG - The round type for JAG contests.
*/
type ChallengeRoundMap = {
[ChallengeContestType.PCK]: PckRound;
[ChallengeContestType.JAG]: JagRound;
};

/**
* Constant used as a placeholder for missing timestamp data in AOJ contests
* Value: -1
*/
const PENDING = -1;

/**
* A cache to store contests for import, keyed by contest ID.
* This cache is used to avoid redundant API calls to the Aizu Online Judge.
*
* @type {Map<string, ContestsForImport>}
*/
const contestCache: Map<string, ContestsForImport> = new Map();
/**
* A cache to store tasks for import, keyed by a string identifier.
*
* This cache is implemented as a Map where the key is a string and the value is of type `TasksForImport`.
* It is used to temporarily hold tasks to avoid redundant imports and improve performance.
*/
const taskCache: Map<string, TasksForImport> = new Map();

/**
* AojApiClient is a client for interacting with the Aizu Online Judge (AOJ) API.
* It extends the ContestSiteApiClient and provides methods to fetch contests and tasks
Expand All @@ -98,21 +141,27 @@ export class AojApiClient extends ContestSiteApiClient {
/**
* Fetches and combines contests from different sources.
*
* This method concurrently fetches course contests, preliminary PCK contests,
* and final PCK contests, then combines them into a single array.
* This method concurrently fetches course contests, preliminary and final PCK contests,
* and prelim and regional JAG contests, then combines them into a single array.
*
* @returns {Promise<ContestsForImport>} A promise that resolves to an array of contests.
*/
async getContests(): Promise<ContestsForImport> {
try {
const [courses, pckPrelims, pckFinals] = await Promise.all([
const [courses, pckPrelims, pckFinals, jagPrelims, jagRegionals] = await Promise.all([
this.fetchCourseContests(),
this.fetchPckContests(PckRound.PRELIM),
this.fetchPckContests(PckRound.FINAL),
this.fetchChallengeContests(ChallengeContestType.PCK, PckRound.PRELIM),
this.fetchChallengeContests(ChallengeContestType.PCK, PckRound.FINAL),
this.fetchChallengeContests(ChallengeContestType.JAG, JagRound.PRELIM),
this.fetchChallengeContests(ChallengeContestType.JAG, JagRound.REGIONAL),
]);

const contests = courses.concat(pckPrelims, pckFinals);
console.log(`Found AOJ: ${contests.length} contests.`);
const contests = courses.concat(pckPrelims, pckFinals, jagPrelims, jagRegionals);
console.log(
`Found AOJ contests - Total: ${contests.length} ` +
`(Courses: ${courses.length}, PCK: ${pckPrelims.length + pckFinals.length}, ` +
`JAG: ${jagPrelims.length + jagRegionals.length})`,
);

return contests;
} catch (error) {
Expand Down Expand Up @@ -158,24 +207,38 @@ export class AojApiClient extends ContestSiteApiClient {
}

/**
* Fetches PCK contests from the AOJ API for a given round.
* Fetches challenge contests from the AOJ API for a given round.
*
* @param {PckRound} round - The round identifier for which to fetch contests.
* @param {ChallengeContestType} contestType - The type of challenge contest for which to fetch contests.
* @param {PckRound | JagRound} round - The round identifier for which to fetch contests.
* @returns {Promise<ContestsForImport>} A promise that resolves to an array of contests for import.
*
* @throws Will throw an error if the API request fails or the response validation fails.
*
* @example
* const contestType = ChallengeContestType.PCK;
* const round = 'PRELIM';
* const contests = await fetchPckContests(round);
* const contests = await fetchChallengeContests(contestType, round);
* console.log(contests);
*/
private async fetchPckContests(round: PckRound): Promise<ContestsForImport> {
private async fetchChallengeContests<T extends ChallengeContestType>(
contestType: T,
round: ChallengeRoundMap[T],
): Promise<ContestsForImport> {
const cacheKey = `${contestType}_${round}`;

if (contestCache.has(cacheKey)) {
console.log('Using cached contest data for', cacheKey);
return contestCache.get(cacheKey)!;
}

const contestTypeLabel = contestType.toUpperCase();

try {
const results = await this.fetchApiWithConfig<AOJChallengeContestAPI>({
baseApiUrl: AOJ_API_BASE_URL,
endpoint: `challenges/cl/pck/${round}`,
errorMessage: `Failed to fetch ${round} contests from AOJ API`,
endpoint: `challenges/cl/${contestType}/${round}`,
errorMessage: `Failed to fetch ${contestTypeLabel} ${round} contests from AOJ API`,
validateResponse: (data) =>
'contests' in data && Array.isArray(data.contests) && data.contests.length > 0,
});
Expand All @@ -192,11 +255,13 @@ export class AojApiClient extends ContestSiteApiClient {
[] as ContestsForImport,
);

console.log(`Found AOJ PCK ${round}: ${contests.length} contests.`);
console.log(`Found AOJ ${contestTypeLabel} ${round}: ${contests.length} contests.`);

contestCache.set(cacheKey, contests);

return contests;
} catch (error) {
console.error(`Failed to fetch from AOJ PCK ${round} contests:`, error);
console.error(`Failed to fetch from AOJ ${contestTypeLabel} ${round} contests:`, error);
return [];
}
}
Expand Down Expand Up @@ -226,10 +291,12 @@ export class AojApiClient extends ContestSiteApiClient {
/**
* Fetches tasks from various sources and combines them into a single list.
*
* This method concurrently fetches tasks from three different sources:
* This method concurrently fetches tasks from five different sources:
* - Course tasks
* - PCK Prelim tasks
* - PCK Final tasks
* - JAG Prelim tasks
* - JAG Regional tasks
*
* The fetched tasks are then concatenated into a single array and returned.
*
Expand All @@ -239,13 +306,19 @@ export class AojApiClient extends ContestSiteApiClient {
*/
async getTasks(): Promise<TasksForImport> {
try {
const [courses, pckPrelims, pckFinals] = await Promise.all([
const [courses, pckPrelims, pckFinals, jagPrelims, jagRegionals] = await Promise.all([
this.fetchCourseTasks(),
this.fetchPckTasks(PckRound.PRELIM),
this.fetchPckTasks(PckRound.FINAL),
this.fetchChallengeTasks(ChallengeContestType.PCK, PckRound.PRELIM),
this.fetchChallengeTasks(ChallengeContestType.PCK, PckRound.FINAL),
this.fetchChallengeTasks(ChallengeContestType.JAG, JagRound.PRELIM),
this.fetchChallengeTasks(ChallengeContestType.JAG, JagRound.REGIONAL),
]);
const tasks = courses.concat(pckPrelims, pckFinals);
console.log(`Found AOJ: ${tasks.length} tasks.`);
const tasks = courses.concat(pckPrelims, pckFinals, jagPrelims, jagRegionals);
console.log(
`Found AOJ tasks - Total: ${tasks.length} ` +
`(Courses: ${courses.length}, PCK: ${pckPrelims.length + pckFinals.length}, ` +
`JAG: ${jagPrelims.length + jagRegionals.length})`,
);

return tasks;
} catch (error) {
Expand Down Expand Up @@ -309,9 +382,10 @@ export class AojApiClient extends ContestSiteApiClient {
};

/**
* Fetches tasks for a specified PCK round from the AOJ API.
* Fetches tasks for a specified challenge contest round from the AOJ API.
*
* @param {string} round - The round identifier for which to fetch tasks.
* @param {ChallengeContestType} contestType - The type of challenge contest for which to fetch tasks.
* @param {PckRound | JagRound} round - The round identifier for which to fetch tasks.
* @returns {Promise<TasksForImport>} A promise that resolves to an object containing tasks for import.
* @throws Will throw an error if the API request fails or the response is invalid.
*
Expand All @@ -321,17 +395,24 @@ export class AojApiClient extends ContestSiteApiClient {
* 3. Maps the contest data to a list of tasks, extracting relevant information such as task ID, contest ID, and title.
* 4. Logs the number of tasks found for the specified round.
*/
private async fetchPckTasks(round: string): Promise<TasksForImport> {
if (!Object.values(PckRound).includes(round as PckRound)) {
console.error(`Found invalid PCK round: ${round}`);
return [];
private async fetchChallengeTasks<T extends ChallengeContestType>(
contestType: T,
round: ChallengeRoundMap[T],
): Promise<TasksForImport> {
const cacheKey = `${contestType}_${round}`;

if (taskCache.has(cacheKey)) {
console.log('Using cached tasks for', cacheKey);
return taskCache.get(cacheKey)!;
}

const contestTypeLabel = contestType.toUpperCase();

try {
const allPckContests = await this.fetchApiWithConfig<AOJChallengeContestAPI>({
baseApiUrl: AOJ_API_BASE_URL,
endpoint: `challenges/cl/pck/${round}`,
errorMessage: `Failed to fetch PCK ${round} tasks from AOJ API`,
endpoint: `challenges/cl/${contestType}/${round}`,
errorMessage: `Failed to fetch ${contestTypeLabel} ${round} tasks from AOJ API`,
validateResponse: (data) =>
'contests' in data && Array.isArray(data.contests) && data.contests.length > 0,
});
Expand All @@ -350,11 +431,13 @@ export class AojApiClient extends ContestSiteApiClient {
},
[],
);
console.log(`Found PCK ${round}: ${tasks.length} tasks.`);
console.log(`Found ${contestTypeLabel} ${round}: ${tasks.length} tasks.`);

taskCache.set(cacheKey, tasks);

return tasks;
} catch (error) {
console.error(`Failed to fetch from PCK ${round} tasks:`, error);
console.error(`Failed to fetch from ${contestTypeLabel} ${round} tasks:`, error);
return [];
}
}
Expand Down
1 change: 1 addition & 0 deletions src/lib/clients/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type { TaskForImport, TasksForImport } from '$lib/types/task';
// ・Courses
// ・Challenges
// ・PCK (All-Japan High School Programming Contest)
// ・JAG (ACM-ICPC Japan Alumni Group Contest)

/**
* Creates and returns an object containing instances of various API clients.
Expand Down
16 changes: 16 additions & 0 deletions src/lib/types/contest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ export type ContestsForImport = ContestForImport[];
// Import original enum as type.
import type { ContestType as ContestTypeOrigin } from '@prisma/client';

/**
* An object representing various types of programming contests.
* Each key is a contest type identifier, and the value is the same identifier as a string.
*/
export const ContestType: { [key in ContestTypeOrigin]: key } = {
ABC: 'ABC', // AtCoder Beginner Contest
APG4B: 'APG4B', // C++入門 AtCoder Programming Guide for beginners
Expand All @@ -44,6 +48,7 @@ export const ContestType: { [key in ContestTypeOrigin]: key } = {
OTHERS: 'OTHERS', // AtCoder (その他)
AOJ_COURSES: 'AOJ_COURSES', // AIZU ONLINE JUDGE Courses
AOJ_PCK: 'AOJ_PCK', // All-Japan High School Programming Contest (PCK)
AOJ_JAG: 'AOJ_JAG', // ACM-ICPC Japan Alumni Group Contest (JAG)
} as const;

// Re-exporting the original type with the original name.
Expand All @@ -65,3 +70,14 @@ export type ContestType = ContestTypeOrigin;
export interface ContestPrefix {
[key: string]: string;
}

/**
* Represents a collection of translations for contest labels.
* The keys are language codes or identifiers, and the values are the translated strings.
*
* @property {string} [key] - The abbr contest type in English.
* @property {string} [key: string] - The contest name in Japanese.
*/
export type ContestLabelTranslations = {
[key: string]: string;
};
Loading
Loading