AtCoder Difficulty Display

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

2020-08-02 या दिनांकाला. सर्वात नवीन आवृत्ती पाहा.

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