Greasy Fork is available in English.

AtCoder Difficulty Display

AtCoder Problemsの難易度を表示します。

נכון ליום 20-11-2022. ראה הגרסה האחרונה.

  1. // ==UserScript==
  2. // @name AtCoder Difficulty Display
  3. // @namespace https://github.com/hotarunx
  4. // @homepage https://github.com/hotarunx/AtCoderDifficultyDisplay
  5. // @supportURL https://github.com/hotarunx/AtCoderDifficultyDisplay/issues
  6. // @version 1.0.9
  7. // @description AtCoder Problemsの難易度を表示します。
  8. // @description:en display a difficulty of AtCoder Problems.
  9. // @author hotarunx
  10. // @match https://atcoder.jp/contests/*
  11. // @exclude https://atcoder.jp/contests/
  12. // @grant none
  13. // @connect https://kenkoooo.com/atcoder/resources/problem-models.json
  14. // @connect https://kenkoooo.com/atcoder/atcoder-api/results?user=*
  15. // @license MIT
  16. //
  17. // Copyright(c) 2020 hotarunx
  18. // This software is released under the MIT License, see LICENSE or https://github.com/hotarunx/AtCoderMyExtensions/blob/master/LICENSE.
  19. //
  20. // ==/UserScript==
  21.  
  22. const hideDifficulty = false;
  23.  
  24. // 現在時間、コンテスト開始時間、コンテスト終了時間(UNIX時間 + 時差)
  25. const nowTime = Math.floor(Date.now() / 1000);
  26. const contestStartTime = Math.floor(Date.parse(startTime._i) / 1000);
  27. const contestEndTime = Math.floor(Date.parse(endTime._i) / 1000);
  28.  
  29. (async function () {
  30. // URLから問題ID(ex: abc170_a)を取得
  31. const path = location.pathname.split("/");
  32. const problemId = path[path.length - 1];
  33. const isABS = path[2] == "abs";
  34.  
  35. // 問題のコンテストが開催中ならば全ての処理をスキップする。
  36. if (!isABS && !isContestOver(nowTime)) return;
  37.  
  38. const diffURL = "https://kenkoooo.com/atcoder/resources/problem-models.json";
  39. const diffKey = "atcoderDifficultyDisplayEstimatedDifficulties";
  40.  
  41. const submissionsURL = "https://kenkoooo.com/atcoder/atcoder-api/results?user=" + userScreenName;
  42. const submissionsKey = "atcoderDifficultyDisplayUserSubmissions";
  43.  
  44. const estimatedDifficulties = await fetchAPIData(diffURL, diffKey, 1 * 60 * 60);
  45. // const userSubmissions = await fetchAPIData(submissionsURL, submissionsKey, 1 * 60 * 60);
  46.  
  47. const problemFunctions = [];
  48. const problemListFunctions = [];
  49.  
  50. if (path[path.length - 2] == "tasks") {
  51. const problemStatus = getElementOfProblemStatus();
  52. const problemTitle = document.getElementsByClassName("h2")[0];
  53. if (hideDifficulty) {
  54. problemFunctions.push(() => changeProblemTitle(problemId, estimatedDifficulties, problemTitle, false));
  55. problemFunctions.push(() => addDifficultyText(problemId, estimatedDifficulties, problemStatus));
  56. addDifficultyShowButton(problemTitle, problemFunctions);
  57. }
  58. else {
  59. changeProblemTitle(problemId, estimatedDifficulties, problemTitle, false);
  60. addDifficultyText(problemId, estimatedDifficulties, problemStatus);
  61. }
  62.  
  63. // if (!isABS)
  64. // addIsSolvedText(problemId, userSubmissions, problemStatus);
  65. }
  66.  
  67. const as = document.getElementsByTagName("a");
  68. for (const item of as) {
  69. const h = item.getAttribute("href");
  70. if (typeof (h) != "string") continue;
  71. const hpath = h.split("/");
  72.  
  73. if (hpath[hpath.length - 2] == "tasks") {
  74. // itemが*/tasksページの表の一番左なら色付けしない
  75. if (item.parentElement.className === "text-center no-break") continue;
  76.  
  77. const hProblemId = hpath[hpath.length - 1];
  78. if (hideDifficulty) {
  79. problemListFunctions.push(() => changeProblemTitle(hProblemId, estimatedDifficulties, item, true));
  80. }
  81. else {
  82. changeProblemTitle(hProblemId, estimatedDifficulties, item, true);
  83. }
  84. }
  85. }
  86. if (
  87. hideDifficulty &&
  88. problemListFunctions.length != 0 &&
  89. path[path.length - 2] !== "tasks"
  90. ) {
  91. const problemListTitle = document.getElementsByTagName("h2")[0];
  92. problemListTitle.style.display = "inline";
  93. addDifficultyShowButton(problemListTitle, problemListFunctions);
  94. }
  95. })();
  96.  
  97. // コンテストが終了した?
  98. function isContestOver(time) {
  99. // 緩衝時間(20分)
  100. const bufferTime = 20 * 60;
  101.  
  102. // 現在時間 > コンテスト終了時間 + 緩衝時間?
  103. if (time > contestEndTime + bufferTime) return true;
  104. return false;
  105. }
  106.  
  107. // APIサーバからデータを取得してlocalStorageに保存して返す
  108. // 直前の取得から時間が経過していないならば保存したデータを返す
  109. async function fetchAPIData(url, keyData, timeInterval) {
  110. const keyLastFetch = keyData + "lastFetchedAt";
  111. let jsondata = JSON.parse(localStorage.getItem(keyData));
  112. const fetchTime = parseInt(localStorage.getItem(keyLastFetch));
  113.  
  114. // コンテストが終了していないならばデータ取得はしない
  115. if (!isContestOver(nowTime)) return jsondata;
  116.  
  117. // 次のいずれかを満たすならば取得する
  118. // * データが保存されていない
  119. // * 直前の取得からtimeInterval経過した
  120. // * 直前の取得時にコンテストが終了していなかった
  121.  
  122. let need2Fetch = false;
  123. if (isNaN(fetchTime)) need2Fetch = true;
  124. else if (nowTime >= timeInterval + fetchTime) need2Fetch = true;
  125. else if (!isContestOver(fetchTime)) need2Fetch = true;
  126.  
  127. // データを取得する
  128. if (need2Fetch) {
  129. // alert(keyData + "is fetched.");
  130. jsondata = await (await (fetch(url))).json();
  131. removeUnusedValues(jsondata);
  132. localStorage.setItem(keyData, JSON.stringify(jsondata));
  133. localStorage.setItem(keyLastFetch, nowTime);
  134. }
  135.  
  136. return jsondata;
  137. }
  138.  
  139. function removeUnusedValues(jsondata) {
  140. const necessaryKeys = ["difficulty", "is_experimental", "epoch_second", "point", "result", "problem_id"];
  141.  
  142. for (const item in jsondata) {
  143. for (const key in jsondata[item]) {
  144. if (!necessaryKeys.includes(key)) {
  145. delete jsondata[item][key];
  146. }
  147. }
  148. }
  149. }
  150.  
  151. // Webページの問題ステータス(実行時間制限とメモリ制限が書かれた部分)のHTMLオブジェクトを取得
  152. function getElementOfProblemStatus() {
  153. let element_status;
  154.  
  155. const main_container = document.getElementById('main-container');
  156. const elements_p = main_container.getElementsByTagName("p");
  157.  
  158. for (let i = 0; i < elements_p.length; i++) {
  159. const element = elements_p[i];
  160. if (element.textContent.match("メモリ制限:") || element.textContent.match("Memory Limit:")) {
  161. element_status = element;
  162. break;
  163. }
  164. }
  165.  
  166. return element_status;
  167. }
  168.  
  169. // レーティングに対応する色のカラーコード
  170. function colorRating(rating) {
  171. if (rating < 400) return '#808080'; // gray
  172. else if (rating < 800) return '#804000'; // brown
  173. else if (rating < 1200) return '#008000'; // green
  174. else if (rating < 1600) return '#00C0C0'; // cyan
  175. else if (rating < 2000) return '#0000FF'; // blue
  176. else if (rating < 2400) return '#C0C000'; // yellow
  177. else if (rating < 2800) return '#FF8000'; // orange
  178. return '#FF0000'; // red
  179. }
  180.  
  181. // レートを表す難易度円(◒)を生成
  182. function generateDifficultyCircle(rating, isSmall = true) {
  183. const size = (isSmall ? 12 : 36);
  184. const borderWidth = (isSmall ? 1 : 3);
  185.  
  186. if (rating < 3200) {
  187. // 色と円がどのぐらい満ちているかを計算
  188. const color = colorRating(rating);
  189. const percentFull = (rating % 400) / 400 * 100;
  190.  
  191. // ◒を生成
  192. return "<span style = 'display: inline-block; border-radius: 50%; border-style: solid;border-width: " + borderWidth + "px; margin-right: 5px; vertical-align: initial; height: " + size + "px; width: " + size + "px;border-color: " + color + "; background: linear-gradient(to top, " + color + " 0%, " + color + " " + percentFull + "%, rgba(0, 0, 0, 0) " + percentFull + "%, rgba(0, 0, 0, 0) 100%); '></span>";
  193.  
  194. }
  195. // 金銀銅は例外処理
  196. else if (rating < 3600) {
  197. return '<span style="display: inline-block; border-radius: 50%; border-style: solid;border-width: ' + borderWidth + 'px; margin-right: 5px; vertical-align: initial; height: ' + size + 'px; width: ' + size + 'px; border-color: rgb(150, 92, 44); background: linear-gradient(to right, rgb(150, 92, 44), rgb(255, 218, 189), rgb(150, 92, 44));"></span>';
  198.  
  199. } else if (rating < 4000) {
  200. return '<span style="display: inline-block; border-radius: 50%; border-style: solid;border-width: ' + borderWidth + 'px; margin-right: 5px; vertical-align: initial; height: ' + size + 'px; width: ' + size + 'px; border-color: rgb(128, 128, 128); background: linear-gradient(to right, rgb(128, 128, 128), white, rgb(128, 128, 128));"></span>';
  201.  
  202. } else {
  203. return '<span style="display: inline-block; border-radius: 50%; border-style: solid;border-width: ' + borderWidth + 'px; margin-right: 5px; vertical-align: initial; height: ' + size + 'px; width: ' + size + 'px; border-color: rgb(255, 215, 0); background: linear-gradient(to right, rgb(255, 215, 0), white, rgb(255, 215, 0));"></span>';
  204.  
  205. }
  206. }
  207.  
  208. // 400未満のレーティングを補正
  209. // 参考 https://qiita.com/anqooqie/items/92005e337a0d2569bdbd#性質4-初心者への慈悲
  210. function correctLowerRating(rating) {
  211. if (rating >= 400) return rating;
  212. return 400 / Math.exp((400 - rating) / 400);
  213. }
  214.  
  215. function changeProblemTitle(problemId, estimatedDifficulties, problemTitle, isSmall = true) {
  216. const problem = estimatedDifficulties[problemId];
  217.  
  218. // 問題が存在しなければ終了
  219. if (problem == null) return;
  220. if (problem.difficulty != null) {
  221. const difficulty = correctLowerRating(problem.difficulty).toFixed();
  222. problemTitle.style.color = colorRating(difficulty);
  223. if (problem.is_experimental) problemTitle.insertAdjacentHTML("afterbegin", "🧪");
  224. problemTitle.insertAdjacentHTML("beforebegin", generateDifficultyCircle(difficulty, isSmall));
  225. }
  226. else {
  227. problemTitle.style.color = "#17a2b8";
  228. const unavailableCircle = "(?)";
  229. problemTitle.insertAdjacentHTML("afterbegin", unavailableCircle);
  230. }
  231. }
  232.  
  233. // 推定難易度文字列を生成
  234. function generateDifficultyText(difficulty, is_experimental) {
  235. // 推定難易度を補正して四捨五入
  236. difficulty = correctLowerRating(difficulty);
  237. difficulty = difficulty.toFixed();
  238.  
  239. const textValue = "<span style='font-weight: bold; color: " + colorRating(difficulty) + ";'>" + difficulty + "</span>";
  240. // textDiff = "<a href='https://kenkoooo.com/atcoder/#/table/" + userScreenName + "'>Difficulty</a>";
  241. const textDiff = "Difficulty";
  242.  
  243. return textDiff + ": " + textValue + (is_experimental ? " (🧪)" : "");
  244. }
  245.  
  246. // 推定難易度表示を追加する
  247. function addDifficultyText(problemId, estimatedDifficulties, problemStatus) {
  248. const problem = estimatedDifficulties[problemId];
  249.  
  250. // 問題が存在しなければ終了
  251. if (problem == null) return;
  252.  
  253. if (problem.difficulty != null) {
  254. // 難易度を表示する文字列を生成
  255. const text = generateDifficultyText(problem.difficulty, problem.is_experimental);
  256. problemStatus.insertAdjacentHTML('beforeend', " / " + text);
  257. } else
  258. problemStatus.insertAdjacentHTML('beforeend', " / Difficulty: <span style='font-weight: bold; color: #17a2b8;'>Unavailable</span>");
  259. }
  260.  
  261. function displayDifficulty(functions) {
  262. functions.forEach(func => func());
  263. document.getElementById('displayButton').style.display = 'none';
  264. }
  265.  
  266. function addDifficultyShowButton(placeToAdd, functions) {
  267. placeToAdd.insertAdjacentHTML("beforebegin", `<input id="displayButton" type="button" value="diff"/>`);
  268. document.getElementById('displayButton').onclick = () => displayDifficulty(functions);
  269. }
  270.  
  271. // AC、コンテスト中AC、ペナルティ数、AC時間、最大得点、コンテスト中最大得点を計算
  272. function searchSubmissionsResult(submissions) {
  273. const nonPenaltyJudge = ["AC", "CE", "IE", "WJ", "WR"];
  274. submissions.sort((a, b) => a.epoch_second - b.epoch_second);
  275.  
  276. let accepted = false;
  277. let acceptedDuringContest = false;
  278. let penalties = 0;
  279. let acceptedTime = contestEndTime;
  280. let maxPoint = 0;
  281. let maxPointDuringContest = 0;
  282.  
  283. for (const item of submissions) {
  284. const duringContest = contestStartTime <= item.epoch_second && item.epoch_second < contestEndTime;
  285.  
  286. if (item.result == "AC") {
  287. accepted = true;
  288. if (duringContest) {
  289. acceptedDuringContest = true;
  290. acceptedTime = Math.min(item.epoch_second, acceptedTime);
  291. }
  292. }
  293.  
  294. if (!accepted && duringContest && !nonPenaltyJudge.includes(item.result)) {
  295. penalties++;
  296. }
  297.  
  298. maxPoint = Math.max(item.point, maxPoint);
  299. if (duringContest)
  300. maxPointDuringContest = Math.max(item.point, maxPointDuringContest);
  301. }
  302.  
  303. return { accepted, acceptedDuringContest, penalties, acceptedTime, maxPoint, maxPointDuringContest };
  304. }
  305.  
  306. function epochTime2HHMM(time) {
  307. return Math.floor(time / 60).toFixed() + ":" + (time % 60).toFixed().padStart(2, '0');
  308. }
  309.  
  310. // ACしたか、AC時間、ペナルティ数を表示
  311. function addIsSolvedText(problemId, userSubmissions, problemStatus) {
  312. const submissions = userSubmissions.filter(function (item, index) { if (item.problem_id == problemId) return true; });
  313. const submitted = submissions.length > 0;
  314. const { accepted, acceptedDuringContest, penalties, acceptedTime, maxPoint, maxPointDuringContest } = searchSubmissionsResult(submissions);
  315.  
  316. let text = "Is Solved: ";
  317. if (acceptedDuringContest) text += "<span style='font-weight: bold; color: white; background: green; border-radius: 20%;'>✓</span>";
  318. else if (accepted) text += "<span style='font-weight: bold; color: green;'>✓</span>";
  319. else if (submitted) text += "<span style='font-weight: bold; color: orange;'>✘</span>";
  320. else text += "<span style='font-weight: bold; color: gray;'>✘</span>";
  321.  
  322. if (acceptedDuringContest)
  323. text += " <span style='font-size: x-small; color: grey;'>" + epochTime2HHMM(acceptedTime - contestStartTime) + "</span> ";
  324.  
  325. if (penalties > 0)
  326. text += " <span style='font-size: x-small; color: red;'>(" + penalties + ")</span> ";
  327.  
  328. if (maxPoint >= 10000) {
  329. text += " <span style='font-size: x-small; color: orange;'>" + maxPoint + "</span> ";
  330. if (maxPointDuringContest != maxPoint)
  331. text += " <span style='font-size: x-small; color: orange;'>(" + maxPointDuringContest + ")</span> ";
  332. }
  333.  
  334. problemStatus.insertAdjacentHTML('beforeend', " / " + text);
  335. }