Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save sevenc-nanashi/f2fe6ede04e7b4c538cca715f9edd09f to your computer and use it in GitHub Desktop.
Save sevenc-nanashi/f2fe6ede04e7b4c538cca715f9edd09f to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name atcoder-difficulty-display
// @namespace https://github.com/hotaru-n
// @version 2.0.1
// @description AtCoder Problemsの難易度を表示します。
// @author hotaru-n
// @license MIT
// @supportURL https://github.com/hotaru-n/atcoder-difficulty-display/issues
// @match https://atcoder.jp/contests/*
// @exclude https://atcoder.jp/contests/
// @match https://atcoder.jp/settings
// @inject-into page
// @require https://greasyfork.org/scripts/437862-atcoder-problems-api/code/atcoder-problems-api.js?version=1004589
// @downloadURL https://update.greasyfork.org/scripts/397185/atcoder-difficulty-display.user.js
// @updateURL https://update.greasyfork.org/scripts/397185/atcoder-difficulty-display.meta.js
// ==/UserScript==
const nonPenaltyJudge = ["AC", "CE", "IE", "WJ", "WR"];
/** 設定 ネタバレ防止のID, Key */
const hideDifficultyID = "hide-difficulty-atcoder-difficulty-display";
/**
* 後方互換処理
*/
const backwardCompatibleProcessing = () => {
const oldLocalStorageKeys = [
"atcoderDifficultyDisplayUserSubmissions",
"atcoderDifficultyDisplayUserSubmissionslastFetchedAt",
"atcoderDifficultyDisplayEstimatedDifficulties",
"atcoderDifficultyDisplayEstimatedDifficultieslastFetchedAt",
];
/** 過去バージョンのlocalStorageデータを削除する */
oldLocalStorageKeys.forEach((key) => {
localStorage.removeItem(key);
});
};
const getTypical90Difficulty = (title) => {
if (title.includes("★1"))
return 149;
if (title.includes("★2"))
return 399;
if (title.includes("★3"))
return 799;
if (title.includes("★4"))
return 1199;
if (title.includes("★5"))
return 1599;
if (title.includes("★6"))
return 1999;
if (title.includes("★7"))
return 2399;
return NaN;
};
const getTypical90Description = (title) => {
if (title.includes("★1"))
return "200 点問題レベル";
if (title.includes("★2"))
return "300 点問題レベル";
if (title.includes("★3"))
return "";
if (title.includes("★4"))
return "400 点問題レベル";
if (title.includes("★5"))
return "500 点問題レベル";
if (title.includes("★6"))
return "これが安定して解ければ上級者です";
if (title.includes("★7"))
return "チャレンジ問題枠です";
return "エラー: 競プロ典型 90 問の難易度読み取りに失敗しました";
};
const addTypical90Difficulty = (problemModels, problems) => {
const models = problemModels;
const problemsT90 = problems.filter((element) => element.contest_id === "typical90");
problemsT90.forEach((element) => {
const difficulty = getTypical90Difficulty(element.title);
const model = {
slope: NaN,
intercept: NaN,
variance: NaN,
difficulty,
discrimination: NaN,
irt_loglikelihood: NaN,
irt_users: NaN,
is_experimental: false,
extra_difficulty: `${getTypical90Description(element.title)}`,
};
models[element.id] = model;
});
return models;
};
// 次のコードを引用
// [AtCoderProblems/theme\.ts at master · kenkoooo/AtCoderProblems](https://github.com/kenkoooo/AtCoderProblems/blob/master/atcoder-problems-frontend/src/style/theme.ts)
// 8b1b86c740e627e59abf056a11c00582e12b30ff
const ThemeLight = {
difficultyBlackColor: "#404040",
difficultyGreyColor: "#808080",
difficultyBrownColor: "#804000",
difficultyGreenColor: "#008000",
difficultyCyanColor: "#00C0C0",
difficultyBlueColor: "#0000FF",
difficultyYellowColor: "#C0C000",
difficultyOrangeColor: "#FF8000",
difficultyRedColor: "#FF0000",
};
({
...ThemeLight,
difficultyBlackColor: "#FFFFFF",
difficultyGreyColor: "#C0C0C0",
difficultyBrownColor: "#B08C56",
difficultyGreenColor: "#3FAF3F",
difficultyCyanColor: "#42E0E0",
difficultyBlueColor: "#8888FF",
difficultyYellowColor: "#FFFF56",
difficultyOrangeColor: "#FFB836",
difficultyRedColor: "#FF6767",
});
// 次のコードを引用・編集
// [AtCoderProblems/index\.ts at master · kenkoooo/AtCoderProblems](https://github.com/kenkoooo/AtCoderProblems/blob/master/atcoder-problems-frontend/src/utils/index.ts)
// 5835f5dcacfa0cbdcc8ab1116939833d5ab71ed4
const clipDifficulty = (difficulty) => Math.round(difficulty >= 400 ? difficulty : 400 / Math.exp(1.0 - difficulty / 400));
const RatingColors = [
"Black",
"Grey",
"Brown",
"Green",
"Cyan",
"Blue",
"Yellow",
"Orange",
"Red",
];
const getRatingColor = (rating) => {
const index = Math.min(Math.floor(rating / 400), RatingColors.length - 2);
return RatingColors[index + 1] ?? "Black";
};
const getRatingColorClass = (rating) => {
const ratingColor = getRatingColor(rating);
switch (ratingColor) {
case "Black":
return "difficulty-black";
case "Grey":
return "difficulty-grey";
case "Brown":
return "difficulty-brown";
case "Green":
return "difficulty-green";
case "Cyan":
return "difficulty-cyan";
case "Blue":
return "difficulty-blue";
case "Yellow":
return "difficulty-yellow";
case "Orange":
return "difficulty-orange";
case "Red":
return "difficulty-red";
default:
return "difficulty-black";
}
};
const getRatingColorCode = (ratingColor, theme = ThemeLight) => {
switch (ratingColor) {
case "Black":
return theme.difficultyBlackColor;
case "Grey":
return theme.difficultyGreyColor;
case "Brown":
return theme.difficultyBrownColor;
case "Green":
return theme.difficultyGreenColor;
case "Cyan":
return theme.difficultyCyanColor;
case "Blue":
return theme.difficultyBlueColor;
case "Yellow":
return theme.difficultyYellowColor;
case "Orange":
return theme.difficultyOrangeColor;
case "Red":
return theme.difficultyRedColor;
default:
return theme.difficultyBlackColor;
}
};
// 次のコードを引用・編集
// [AtCoderProblems/TopcoderLikeCircle\.tsx at master · kenkoooo/AtCoderProblems](https://github.com/kenkoooo/AtCoderProblems/blob/master/atcoder-problems-frontend/src/components/TopcoderLikeCircle.tsx)
// 02d7ed77d8d8a9fa8d32cb9981f18dfe53f2c5f0
// FIXME: ダークテーマ対応
const useTheme = () => ThemeLight;
const getRatingMetalColorCode = (metalColor) => {
switch (metalColor) {
case "Bronze":
return { base: "#965C2C", highlight: "#FFDABD" };
case "Silver":
return { base: "#808080", highlight: "white" };
case "Gold":
return { base: "#FFD700", highlight: "white" };
default:
return { base: "#FFD700", highlight: "white" };
}
};
const getStyleOptions = (color, fillRatio, theme) => {
if (color === "Bronze" || color === "Silver" || color === "Gold") {
const metalColor = getRatingMetalColorCode(color);
return {
borderColor: metalColor.base,
background: `linear-gradient(to right, \
${metalColor.base}, ${metalColor.highlight}, ${metalColor.base})`,
};
}
const colorCode = getRatingColorCode(color, theme);
return {
borderColor: colorCode,
background: `border-box linear-gradient(to top, \
${colorCode} ${fillRatio * 100}%, \
rgba(0,0,0,0) ${fillRatio * 100}%)`,
};
};
const topcoderLikeCircle = (color, rating, big = true, extraDescription = "") => {
const fillRatio = rating >= 3200 ? 1.0 : (rating % 400) / 400;
const className = `topcoder-like-circle
${big ? "topcoder-like-circle-big" : ""} rating-circle`;
const theme = useTheme();
const styleOptions = getStyleOptions(color, fillRatio, theme);
const styleOptionsString = `border-color: ${styleOptions.borderColor}; background: ${styleOptions.background};`;
const content = extraDescription
? `Difficulty: ${extraDescription}`
: `Difficulty: ${rating}`;
// FIXME: TooltipにSolve Prob, Solve Timeを追加
return `<span
class="${className}" style="${styleOptionsString}"
data-toggle="tooltip" title="${content}" data-placement="bottom"
/>`;
};
// 次のコードを引用・編集
// [AtCoderProblems/DifficultyCircle\.tsx at master · kenkoooo/AtCoderProblems](https://github.com/kenkoooo/AtCoderProblems/blob/master/atcoder-problems-frontend/src/components/DifficultyCircle.tsx)
// 0469e07274fda2282c9351c2308ed73880728e95
const getColor = (difficulty) => {
if (difficulty < 3200)
return getRatingColor(difficulty);
if (difficulty < 3600)
return "Bronze";
if (difficulty < 4000)
return "Silver";
return "Gold";
};
const difficultyCircle = (difficulty, big = true, extraDescription = "") => {
if (Number.isNaN(difficulty)) {
// Unavailableの難易度円はProblemsとは異なりGlyphiconの「?」を使用
const className = `glyphicon glyphicon-question-sign aria-hidden='true'
difficulty-unavailable
${big ? "difficulty-unavailable-icon-big" : "difficulty-unavailable-icon"}`;
const content = "Difficulty is unavailable.";
return `<span
class="${className}"
data-toggle="tooltip" title="${content}" data-placement="bottom"
/>`;
}
const color = getColor(difficulty);
return topcoderLikeCircle(color, difficulty, big, extraDescription);
};
var html = "<h2>atcoder-difficulty-display</h2>\n<hr>\n<a href=\"https://github.com/hotaru-n/atcoder-difficulty-display\">GitHub</a>\n<div class=\"form-horizontal\">\n <div class=\"form-group\">\n <label class=\"control-label col-sm-3\">ネタバレ防止</label>\n <div class=\"col-sm-5\">\n <div class=\"checkbox\">\n <label>\n <input type=\"checkbox\" id=\"hide-difficulty-atcoder-difficulty-display\">\n 画面上のボタンを押した後に難易度が表示されるようにする\n </label>\n </div>\n </div>\n </div>\n</div>\n";
var css = ".difficulty-red {\n color: #ff0000;\n}\n\n.difficulty-orange {\n color: #ff8000;\n}\n\n.difficulty-yellow {\n color: #c0c000;\n}\n\n.difficulty-blue {\n color: #0000ff;\n}\n\n.difficulty-cyan {\n color: #00c0c0;\n}\n\n.difficulty-green {\n color: #008000;\n}\n\n.difficulty-brown {\n color: #804000;\n}\n\n.difficulty-grey {\n color: #808080;\n}\n\n.topcoder-like-circle {\n display: block;\n border-radius: 50%;\n border-style: solid;\n border-width: 1px;\n width: 12px;\n height: 12px;\n}\n\n.topcoder-like-circle-big {\n border-width: 3px;\n width: 36px;\n height: 36px;\n}\n\n.rating-circle {\n margin-right: 5px;\n display: inline-block;\n}\n\n.difficulty-unavailable {\n color: #17a2b8;\n}\n\n.difficulty-unavailable-icon {\n margin-right: 0.3px;\n}\n\n.difficulty-unavailable-icon-big {\n font-size: 36px;\n margin-right: 5px;\n}\n\n.label-status-a {\n color: white;\n}\n\n.label-success-after-contest {\n background-color: #9ad59e;\n}\n\n.label-warning-after-contest {\n background-color: #ffdd99;\n}";
// AtCoderの問題ページをパースする
/**
* URLをパースする パラメータを消す \
* 例: in: https://atcoder.jp/contests/abc210?lang=en \
* 例: out: (5)['https:', '', 'atcoder.jp', 'contests', 'abc210']
*/
const parseURL = (url) => {
// 区切り文字`/`で分割する
// ?以降の文字列を削除してパラメータを削除する
return url.split("/").map((x) => x.replace(/\?.*/i, ""));
};
const URL = parseURL(window.location.href);
/**
* 表セル要素から、前の要素のテキストが引数と一致する要素を探す
* 個別の提出ページで使うことを想定
* 例: searchSubmissionInfo(["問題", "Task"])
*/
const searchSubmissionInfo = (key) => {
const tdTags = document.getElementsByTagName("td");
const tdTagsArray = Array.prototype.slice.call(tdTags);
return tdTagsArray.filter((elem) => {
const prevElem = elem.previousElementSibling;
const text = prevElem?.textContent;
if (typeof text === "string")
return key.includes(text);
return false;
})[0];
};
/** コンテストタイトル 例: AtCoder Beginner Contest 210 */
document.getElementsByClassName("contest-title")[0]?.textContent ?? "";
/** コンテストID 例: abc210 */
const contestID = URL[4] ?? "";
/**
* ページ種類 \
* 基本的にコンテストIDの次のパス
* ### 例外
* 個別の問題: task
* 個別の提出: submission
* 個別の問題ページで解説ボタンを押すと遷移する個別の問題の解説一覧ページ: task_editorial
*/
const pageType = (() => {
if (URL.length < 6)
return "";
if (URL.length >= 7 && URL[5] === "submissions" && URL[6] !== "me")
return "submission";
if (URL.length >= 8 && URL[5] === "tasks" && URL[7] === "editorial")
return "task_editorial";
if (URL.length >= 7 && URL[5] === "tasks")
return "task";
return URL[5] ?? "";
})();
/** 問題ID 例: abc210_a */
const taskID = (() => {
if (pageType === "task") {
// 問題ページでは、URLから問題IDを取り出す
return URL[6] ?? "";
}
if (pageType === "submission") {
// 個別の提出ページでは、問題リンクのURLから問題IDを取り出す
// 提出情報の問題のURLを取得する
const taskCell = searchSubmissionInfo(["問題", "Task"]);
if (!taskCell)
return "";
const taskLink = taskCell.getElementsByTagName("a")[0];
if (!taskLink)
return "";
const taskUrl = parseURL(taskLink.href);
const taskIDParsed = taskUrl[6] ?? "";
return taskIDParsed;
}
return "";
})();
/** 問題名 例: A - Cabbages */
(() => {
if (pageType === "task") {
// 問題ページでは、h2から問題名を取り出す
return (document
.getElementsByClassName("h2")[0]
?.textContent?.trim()
.replace(/\n.*/i, "") ?? "");
}
if (pageType === "submission") {
// 個別の提出ページでは、問題リンクのテキストから問題名を取り出す
// 提出情報の問題のテキストを取得する
const taskCell = searchSubmissionInfo(["問題", "Task"]);
if (!taskCell)
return "";
const taskLink = taskCell.getElementsByTagName("a")[0];
if (!taskLink)
return "";
return taskLink.textContent ?? "";
}
return "";
})();
/** 提出ユーザー 例: machikane */
(() => {
if (pageType !== "submission")
return "";
// 個別の提出ページのとき
const userCell = searchSubmissionInfo(["ユーザ", "User"]);
if (!userCell)
return "";
return userCell?.textContent?.trim() ?? "";
})();
/** 提出結果 例: AC */
(() => {
if (pageType !== "submission")
return "";
// 個別の提出ページのとき
const statusCell = searchSubmissionInfo(["結果", "Status"]);
if (!statusCell)
return "";
return statusCell?.textContent?.trim() ?? "";
})();
/** 得点 例: 100 */
(() => {
if (pageType !== "submission")
return 0;
// 個別の提出ページのとき
const scoreCell = searchSubmissionInfo(["得点", "Score"]);
if (!scoreCell)
return 0;
return parseInt(scoreCell?.textContent?.trim() ?? "0", 10);
})();
/**
* 得点が最大の提出を返す
*/
const parseMaxScore = (submissionsArg) => {
if (submissionsArg.length === 0) {
return undefined;
}
const maxScore = submissionsArg.reduce((left, right) => left.point > right.point ? left : right);
return maxScore;
};
/**
* ペナルティ数を数える
*/
const parsePenalties = (submissionsArg) => {
let penalties = 0;
let hasAccepted = false;
submissionsArg.forEach((element) => {
hasAccepted = element.result === "AC" || hasAccepted;
if (!hasAccepted && !nonPenaltyJudge.includes(element.result)) {
penalties += 1;
}
});
return penalties;
};
/**
* 最初にACした提出を返す
*/
const parseFirstAcceptedTime = (submissionsArg) => {
const ac = submissionsArg.filter((element) => element.result === "AC");
return ac[0];
};
/**
* 代表的な提出を返す
* 1. 最後にACした提出
* 2. 最後の提出
* 3. undefined
*/
const parseRepresentativeSubmission = (submissionsArg) => {
const ac = submissionsArg.filter((element) => element.result === "AC");
const nonAC = submissionsArg.filter((element) => element.result !== "AC");
if (ac.length > 0)
return ac.slice(-1)[0];
if (nonAC.length > 0)
return nonAC.slice(-1)[0];
return undefined;
};
/**
* 提出をパースして情報を返す
* 対象: コンテスト前,中,後の提出 別コンテストの同じ問題への提出
* 返す情報: 得点が最大の提出 最初のACの提出 代表的な提出 ペナルティ数
*/
const analyzeSubmissions = (submissionsArg) => {
const submissions = submissionsArg.filter((element) => element.problem_id === taskID);
const beforeContest = submissions.filter((element) => element.contest_id === contestID &&
element.epoch_second < startTime.unix());
const duringContest = submissions.filter((element) => element.contest_id === contestID &&
element.epoch_second >= startTime.unix() &&
element.epoch_second < endTime.unix());
const afterContest = submissions.filter((element) => element.contest_id === contestID && element.epoch_second >= endTime.unix());
const anotherContest = submissions.filter((element) => element.contest_id !== contestID);
return {
before: {
maxScore: parseMaxScore(beforeContest),
firstAc: parseFirstAcceptedTime(beforeContest),
representative: parseRepresentativeSubmission(beforeContest),
},
during: {
maxScore: parseMaxScore(duringContest),
firstAc: parseFirstAcceptedTime(duringContest),
representative: parseRepresentativeSubmission(duringContest),
penalties: parsePenalties(duringContest),
},
after: {
maxScore: parseMaxScore(afterContest),
firstAc: parseFirstAcceptedTime(afterContest),
representative: parseRepresentativeSubmission(afterContest),
},
another: {
maxScore: parseMaxScore(anotherContest),
firstAc: parseFirstAcceptedTime(anotherContest),
representative: parseRepresentativeSubmission(anotherContest),
},
};
};
/**
* 提出状況を表すラベルを生成
*/
const generateStatusLabel = (submission, type) => {
if (submission === undefined) {
return "";
}
const isAC = submission.result === "AC";
let className = "";
switch (type) {
case "before":
className = "label-primary";
break;
case "during":
className = isAC ? "label-success" : "label-warning";
break;
case "after":
className = isAC
? "label-success-after-contest"
: "label-warning-after-contest";
break;
case "another":
className = "label-default";
break;
}
let content = "";
switch (type) {
case "before":
content = "コンテスト前の提出";
break;
case "during":
content = "コンテスト中の提出";
break;
case "after":
content = "コンテスト後の提出";
break;
case "another":
content = "別コンテストの同じ問題への提出";
break;
}
const href = `https://atcoder.jp/contests/${submission.contest_id}/submissions/${submission.id}`;
return `<span class="label ${className}"
data-toggle="tooltip" data-placement="bottom" title="${content}">
<a class="label-status-a" href=${href}>${submission.result}</a>
</span> `;
};
/**
* ペナルティ数を表示
*/
const generatePenaltiesCount = (penalties) => {
if (penalties <= 0) {
return "";
}
const content = "コンテスト中のペナルティ数";
return `<span data-toggle="tooltip" data-placement="bottom" title="${content}"class="difficulty-red" style='font-weight: bold; font-size: x-small;'>
(${penalties.toString()})
</span>`;
};
/**
* 最初のACの時間を表示
*/
const generateFirstAcTime = (submission) => {
if (submission === undefined) {
return "";
}
const content = "提出時間";
const href = `https://atcoder.jp/contests/${submission.contest_id}/submissions/${submission.id}`;
const elapsed = submission.epoch_second - startTime.unix();
const elapsedSeconds = elapsed % 60;
const elapsedMinutes = Math.trunc(elapsed / 60);
return `<span data-toggle="tooltip" data-placement="bottom" title="${content}">
<a class="difficulty-orange" style='font-weight: bold; font-size: x-small;' href=${href}>
${elapsedMinutes}:${elapsedSeconds}
</a>
</span>`;
};
/**
* マラソン用に得点を表示するスパンを生成
*/
const generateScoreSpan = (submission, type) => {
if (submission === undefined) {
return "";
}
// マラソン用を考えているのでとりあえず1万点未満は表示しない
if (submission.point < 10000) {
return "";
}
let className = "";
switch (type) {
case "before":
className = "difficulty-blue";
break;
case "during":
className = "difficulty-green";
break;
case "after":
className = "difficulty-yellow";
break;
case "another":
className = "difficulty-grey";
break;
}
let content = "";
switch (type) {
case "before":
content = "コンテスト前の提出";
break;
case "during":
content = "コンテスト中の提出";
break;
case "after":
content = "コンテスト後の提出";
break;
case "another":
content = "別コンテストの同じ問題への提出";
break;
}
const href = `https://atcoder.jp/contests/${submission.contest_id}/submissions/${submission.id}`;
return `<span
data-toggle="tooltip" data-placement="bottom" title="${content}">
<a class="${className}" style='font-weight: bold;' href=${href}>
${submission.point}
</a>
</span> `;
};
/**
* 色付け対象の要素の配列を取得する
* * 個別の問題ページのタイトル
* * 問題へのリンク
* * 解説ページのH3の問題名
*/
const getElementsColorizable = () => {
const elementsColorizable = [];
// 問題ページのタイトル
if (pageType === "task") {
const element = document.getElementsByClassName("h2")[0];
if (element) {
elementsColorizable.push({ element, taskID, big: true });
}
}
// aタグ要素 問題ページ、提出ページ等のリンクを想定
const aTagsRaw = document.getElementsByTagName("a");
let aTagsArray = Array.prototype.slice.call(aTagsRaw);
// 問題ページの一番左の要素は除く 見た目の問題です
aTagsArray = aTagsArray.filter((element) => !((pageType === "tasks" || pageType === "score") &&
!element.parentElement?.previousElementSibling));
// 左上の日本語/英語切り替えリンクは除く
aTagsArray = aTagsArray.filter((element) => !element.href.includes("?lang="));
// 解説ページの問題名の右のリンクは除く
aTagsArray = aTagsArray.filter((element) => !(pageType === "editorial" &&
element.children[0]?.classList.contains("glyphicon-new-window")));
const aTagsConverted = aTagsArray.map((element) => {
const url = parseURL(element.href);
const taskIDFromURL = (url[url.length - 2] ?? "") === "tasks" ? url[url.length - 1] ?? "" : "";
// 個別の解説ページではbig
const big = element.parentElement?.tagName.includes("H2") ?? false;
// Comfortable AtCoderのドロップダウンではafterbegin
const afterbegin = element.parentElement?.parentElement?.classList.contains("dropdown-menu") ?? false;
return { element, taskID: taskIDFromURL, big, afterbegin };
});
elementsColorizable.push(...aTagsConverted);
// h3タグ要素 解説ページの問題名を想定
const h3TagsRaw = document.getElementsByTagName("h3");
const h3TagsArray = Array.prototype.slice.call(h3TagsRaw);
const h3TagsConverted = h3TagsArray.map((element) => {
const url = parseURL(element.getElementsByTagName("a")[0]?.href ?? "");
const taskIDFromURL = (url[url.length - 2] ?? "") === "tasks" ? url[url.length - 1] ?? "" : "";
return { element, taskID: taskIDFromURL, big: true, afterbegin: true };
});
// FIXME: 別ユーザースクリプトが指定した要素を色付けする機能
// 指定したクラスがあれば対象とすることを考えている
// ユーザースクリプトの実行順はユーザースクリプトマネージャーの設定で変更可能
elementsColorizable.push(...h3TagsConverted);
return elementsColorizable;
};
/**
* 問題ステータス(実行時間制限とメモリ制限が書かれた部分)のHTMLオブジェクトを取得
*/
const getElementOfProblemStatus = () => {
if (pageType !== "task")
return undefined;
const psRaw = document
?.getElementById("main-container")
?.getElementsByTagName("p");
const ps = Array.prototype.slice.call(psRaw);
if (!psRaw)
return undefined;
const problemStatuses = ps.filter((p) => {
return (p.textContent?.includes("メモリ制限") ||
p.textContent?.includes("Memory Limit"));
});
return problemStatuses[0];
};
/** 常設コンテストID一覧 */
const permanentContestIDs = [
"practice",
"APG4b",
"abs",
"practice2",
"typical90",
"math-and-algorithm",
"tessoku-book",
];
// FIXME: FIXME: Problemsでデータ取れなかったらコンテストが終了していない判定で良さそう
/**
* 開いているページのコンテストが終了していればtrue \
* 例外処理として以下の場合もtrueを返す
* * コンテストが常設コンテスト
* * コンテストのページ以外にいる <https://atcoder.jp/contests/*>
*/
var isContestOver = () => {
if (!(URL[3] === "contests" && URL.length >= 5))
return true;
if (permanentContestIDs.includes(contestID))
return true;
return Date.now() > window.endTime.valueOf();
};
/**
* コンテストページ <https://atcoder.jp/contests/*> の処理 \
* メインの処理
*/
const contestPageProcess = async () => {
// コンテスト終了前は不要なので無効化する
if (!isContestOver())
return;
// FIXME: ダークテーマ対応
const style = document.createElement("style")
style.innerHTML = css;
document.head.appendChild(style)
/** 問題一覧取得 */
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const problems = await getProblems();
/** 難易度取得 */
const problemModels = addTypical90Difficulty(
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
await getEstimatedDifficulties(), problems);
// FIXME: PAST対応
// FIXME: JOI非公式難易度表対応
/** 提出状況取得 */
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const submissions = await getSubmissions(userScreenName);
// 色付け対象の要素の配列を取得する
// 難易度が無いものを除く
const elementsColorizable = getElementsColorizable().filter((element) => element.taskID in problemModels);
// 問題ステータス(個別の問題ページの実行時間制限とメモリ制限が書かれた部分)を取得する
const elementProblemStatus = getElementOfProblemStatus();
/**
* 色付け処理を実行する
*/
const colorizeElement = () => {
// 問題見出し、問題リンクを色付け
elementsColorizable.forEach((element) => {
const model = problemModels[element.taskID];
// 難易度がUnavailableならばdifficultyプロパティが無い
// difficultyの値をNaNとする
const difficulty = clipDifficulty(model?.difficulty ?? NaN);
// 色付け
if (!Number.isNaN(difficulty)) {
const color = getRatingColorClass(difficulty);
// eslint-disable-next-line no-param-reassign
element.element.classList.add(color);
}
else {
element.element.classList.add("difficulty-unavailable");
}
// 🧪追加
if (model?.is_experimental) {
element.element.insertAdjacentText("afterbegin", "🧪");
}
// ◒難易度円追加
element.element.insertAdjacentHTML(element.afterbegin ? "afterbegin" : "beforebegin", difficultyCircle(difficulty, element.big, model?.extra_difficulty));
});
// 個別の問題ページのところに難易度等情報を追加
if (elementProblemStatus) {
// 難易度の値を表示する
// 難易度推定の対象外なら、この値はundefined
const model = problemModels[taskID];
// 難易度がUnavailableのときはdifficultyの値をNaNとする
// 難易度がUnavailableならばdifficultyプロパティが無い
const difficulty = clipDifficulty(model?.difficulty ?? NaN);
// 色付け
let className = "";
if (difficulty) {
className = getRatingColorClass(difficulty);
}
else if (model) {
className = "difficulty-unavailable";
}
else {
className = "";
}
// Difficultyの値設定
let value = "";
if (difficulty) {
value = difficulty.toString();
}
else if (model) {
value = "Unavailable";
}
else {
value = "None";
}
// 🧪追加
const experimentalText = model?.is_experimental ? "🧪" : "";
const content = `${experimentalText}${value}`;
elementProblemStatus.insertAdjacentHTML("beforeend", ` / Difficulty:
<span style='font-weight: bold;' class="${className}">${content}</span>`);
/** この問題への提出 提出時間ソート済みと想定 */
const thisTaskSubmissions = submissions.filter((element) => element.problem_id === taskID);
const analyze = analyzeSubmissions(thisTaskSubmissions);
// コンテスト前中後外の提出状況 コンテスト中の解答時間とペナルティ数を表示する
let statuesHTML = "";
statuesHTML += generateStatusLabel(analyze.before.representative, "before");
statuesHTML += generateStatusLabel(analyze.during.representative, "during");
statuesHTML += generateStatusLabel(analyze.after.representative, "after");
statuesHTML += generateStatusLabel(analyze.another.representative, "another");
statuesHTML += generatePenaltiesCount(analyze.during.penalties);
statuesHTML += generateFirstAcTime(analyze.during.firstAc);
if (statuesHTML.length > 0) {
elementProblemStatus.insertAdjacentHTML("beforeend", ` / Status: ${statuesHTML}`);
}
// コンテスト前中後外の1万点以上の最大得点を表示する
// NOTE: マラソン用のため、1万点以上とした
let scoresHTML = "";
scoresHTML += generateScoreSpan(analyze.before.maxScore, "before");
scoresHTML += generateScoreSpan(analyze.during.maxScore, "during");
scoresHTML += generateScoreSpan(analyze.after.maxScore, "after");
scoresHTML += generateScoreSpan(analyze.another.maxScore, "another");
if (scoresHTML.length > 0) {
elementProblemStatus.insertAdjacentHTML("beforeend", ` / Scores: ${scoresHTML}`);
}
}
// bootstrap3のtooltipを有効化 難易度円の値を表示するtooltip
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call, no-undef
$('[data-toggle="tooltip"]').tooltip();
};
// 色付け処理実行
if (localStorage.getItem(hideDifficultyID) !== "true") {
// 設定 ネタバレ防止がOFFなら何もせず実行
colorizeElement();
}
else {
// 設定 ネタバレ防止がONなら
// ページ上部にボタンを追加 押すと色付け処理が実行される
const place = document.getElementsByTagName("h2")[0] ??
document.getElementsByClassName("h2")[0] ??
undefined;
if (place) {
place.insertAdjacentHTML("beforebegin", `<input type="button" id="${hideDifficultyID}" class="btn btn-info"
value="Show Difficulty" />`);
const button = document.getElementById(hideDifficultyID);
if (button) {
button.addEventListener("click", () => {
button.style.display = "none";
colorizeElement();
});
}
}
}
};
/**
* 設定ページ <https://atcoder.jp/settings> の処理 \
* 設定ボタンを追加する
*/
const settingPageProcess = async () => {
const insertion = document.getElementsByClassName("form-horizontal")[0];
if (insertion === undefined)
return;
insertion.insertAdjacentHTML("afterend", html);
// 設定 ネタバレ防止のチェックボックスの読み込み 切り替え 保存処理を追加
const hideDifficultyChechbox = document.getElementById(hideDifficultyID);
if (hideDifficultyChechbox &&
hideDifficultyChechbox instanceof HTMLInputElement) {
hideDifficultyChechbox.checked = localStorage.getItem(hideDifficultyID) === "true";
hideDifficultyChechbox.addEventListener("change", () => {
localStorage.setItem(hideDifficultyID, JSON.stringify(hideDifficultyChechbox.checked));
});
}
};
/**
* 最初に実行される部分 \
* 共通の処理を実行した後ページごとの処理を実行する
*/
const init = async () => {
// 共通の処理
backwardCompatibleProcessing();
// ページ別の処理
if (URL[3] === "contests" && URL.length >= 5) {
await contestPageProcess();
}
if (URL[3] === "settings" && URL.length === 4) {
settingPageProcess();
}
};
const initWrapped = () => init().catch((error) => {
// eslint-disable-next-line no-console
console.error("[atcoder-difficulty-display]", error.toString());
setTimeout(initWrapped, 100)
});
initWrapped();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment