Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import Problem from "../../../interfaces/Problem";
import { restoreOriginalContest } from "./OriginalContest";

const adtProblem: Problem = {
id: "abc212_e",
contest_id: "adt_all_20231220_1",
problem_index: "H",
name: "Safety Journey",
};
const contestToProblems = new Map([
["abc212", [{ ...adtProblem, problem_index: "E" }]],
["adt_all_20231220_1", [adtProblem]],
]);

describe("Restore the original contest for recommendations", () => {
it("restores the contest and index of an ABC problem reused in ADT", () => {
expect(restoreOriginalContest(adtProblem, contestToProblems)).toEqual({
...adtProblem,
contest_id: "abc212",
problem_index: "E",
});
expect(adtProblem.contest_id).toBe("adt_all_20231220_1");
expect(adtProblem.problem_index).toBe("H");
});

it("uses the contest mapping for legacy numeric task IDs", () => {
const problem = { ...adtProblem, id: "abc001_1" };
const contests = new Map([
["abc001", [{ ...problem, problem_index: "A" }]],
]);
expect(restoreOriginalContest(problem, contests)).toMatchObject({
contest_id: "abc001",
problem_index: "A",
});
});

it("keeps non-ADT contest associations unchanged", () => {
const problem = { ...adtProblem, contest_id: "another_contest" };
expect(restoreOriginalContest(problem, contestToProblems)).toBe(problem);
});

it("keeps the available metadata while contest mappings are loading", () => {
expect(restoreOriginalContest(adtProblem, undefined)).toBe(adtProblem);
});

it("does not infer an original contest without a matching problem", () => {
expect(restoreOriginalContest(adtProblem, new Map())).toBe(adtProblem);
const contests = new Map([
["abc212", [{ ...adtProblem, id: "abc212_a", problem_index: "A" }]],
]);
expect(restoreOriginalContest(adtProblem, contests)).toBe(adtProblem);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import Problem from "../../../interfaces/Problem";
import { ContestId } from "../../../interfaces/Status";

export const restoreOriginalContest = (
problem: Problem,
contestToProblems: Map<ContestId, Problem[]> | undefined
): Problem => {
if (!problem.contest_id.startsWith("adt_")) {
return problem;
}

const contestId = problem.id.slice(0, problem.id.lastIndexOf("_"));
const originalProblem = contestToProblems
?.get(contestId)
?.find((candidate) => candidate.id === problem.id);
if (!originalProblem) {
return problem;
}

return {
...problem,
contest_id: contestId,
problem_index: originalProblem.problem_index,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import React from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { MemoryRouter } from "react-router-dom";
import * as APIClient from "../../../api/APIClient";
import { Recommendations } from ".";

jest.mock("../../../api/APIClient");
jest.mock("../../../api/InternalAPIClient", () => ({
useLoginState: () => ({ data: undefined }),
}));

beforeEach(() => {
localStorage.clear();
const problem = {
id: "abc212_e",
contest_id: "adt_all_20231220_1",
problem_index: "H",
name: "Safety Journey",
title: "H. Safety Journey",
first_user_id: null,
first_contest_id: null,
first_submission_id: null,
fastest_user_id: null,
fastest_contest_id: null,
fastest_submission_id: null,
execution_time: null,
shortest_user_id: null,
shortest_contest_id: null,
shortest_submission_id: null,
source_code_length: null,
solver_count: null,
};
jest.spyOn(APIClient, "useMergedProblemMap").mockReturnValue({
data: new Map([[problem.id, problem]]),
});
jest
.spyOn(APIClient, "useContestToProblems")
.mockReturnValue(
new Map([["abc212", [{ ...problem, problem_index: "E" }]]])
);
jest.spyOn(APIClient, "useContestMap").mockReturnValue(
new Map([
[
"abc212",
{
id: "abc212",
title: "AtCoder Beginner Contest 212",
start_epoch_second: 1627128000,
duration_second: 6000,
rate_change: " ~ 1999",
},
],
])
);
jest.spyOn(APIClient, "useProblemModelMap").mockReturnValue(
new Map([
[
problem.id,
{
difficulty: 1500,
rawDifficulty: 1500,
discrimination: 0.01,
is_experimental: false,
slope: undefined,
intercept: undefined,
variance: undefined,
},
],
])
);
jest.spyOn(APIClient, "useRatingInfo").mockReturnValue({
rating: 1500,
internalRating: 1500,
participationCount: 10,
});
jest.spyOn(APIClient, "useUserSubmission").mockReturnValue([
{
id: 1,
epoch_second: 1,
problem_id: "abc212_a",
contest_id: "abc212",
user_id: "test_user",
language: "C++",
point: 100,
length: 100,
result: "AC",
execution_time: 1,
},
]);
});

it.each(["All", "ABC"])(
"shows the original contest, problem index and links with the %s filter",
(category) => {
localStorage.setItem("recommendCategoryOption", JSON.stringify(category));
const html = renderToStaticMarkup(
<MemoryRouter>
<Recommendations userId="test_user" />
</MemoryRouter>
);
expect(html).toContain("E. Safety Journey");
expect(html).toContain("https://atcoder.jp/contests/abc212/tasks/abc212_e");
expect(html).toContain('href="https://atcoder.jp/contests/abc212"');
expect(html).not.toContain("H. Safety Journey");
expect(html).not.toContain("adt_all_20231220_1");
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useHistory } from "react-router-dom";
import { Button, ButtonGroup, Row } from "reactstrap";
import {
useContestMap,
useContestToProblems,
useMergedProblemMap,
useProblemModelMap,
useRatingInfo,
Expand Down Expand Up @@ -37,6 +38,7 @@ import {
import { classifyContest } from "../../../utils/ContestClassifier";
import { getLikeContestCategory } from "../../../utils/LikeContestUtils";
import { recommendProblems } from "./RecommendProblems";
import { restoreOriginalContest } from "./OriginalContest";
import {
CategoryOption,
RecommendController,
Expand Down Expand Up @@ -80,8 +82,11 @@ export const Recommendations = (props: Props) => {

const userSubmissions = useUserSubmission(props.userId) ?? [];
const { data: mergedProblemsMap } = useMergedProblemMap();
const contestToProblems = useContestToProblems();
const problems = mergedProblemsMap
? Array.from(mergedProblemsMap.values())
? Array.from(mergedProblemsMap.values()).map((problem) =>
restoreOriginalContest(problem, contestToProblems)
)
: [];
const contestMap = useContestMap();
const problemModels = useProblemModelMap();
Expand Down Expand Up @@ -183,20 +188,22 @@ export const Recommendations = (props: Props) => {
selectRow={isLoggedIn ? selectRowProps : undefined}
>
<TableHeaderColumn
dataField="title"
dataField="name"
dataFormat={(
name: string,
{
id,
contest_id,
problem_index,
is_experimental,
}: { id: string; contest_id: string; is_experimental: boolean }
}: Problem & { is_experimental: boolean }
): React.ReactElement => (
<ProblemLink
isExperimentalDifficulty={is_experimental}
showDifficulty={true}
problemId={id}
problemName={name}
problemIndex={problem_index}
contestId={contest_id}
problemModel={problemModels?.get(id) ?? null}
userRatingInfo={userRatingInfo}
Expand Down