atcoder-difficulty-display

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

As of 2023-05-31. See the latest version.

  1. // ==UserScript==
  2. // @name atcoder-difficulty-display
  3. // @namespace https://github.com/hotarunx
  4. // @version 2.0.0
  5. // @description AtCoder Problemsの難易度を表示します。
  6. // @author hotarunx
  7. // @license MIT
  8. // @supportURL https://github.com/hotarunx/AtCoderDifficultyDisplay/issues
  9. // @match https://atcoder.jp/contests/*
  10. // @exclude https://atcoder.jp/contests/
  11. // @match https://atcoder.jp/settings
  12. // @grant GM_addStyle
  13. // @grant GM_setValue
  14. // @grant GM_getValue
  15. // @require https://greatest.deepsurf.us/scripts/437862-atcoder-problems-api/code/atcoder-problems-api.js?version=1004589
  16. // ==/UserScript==
  17. const nonPenaltyJudge = ["AC", "CE", "IE", "WJ", "WR"];
  18. /** 設定 ネタバレ防止のID, Key */
  19. const hideDifficultyID = "hide-difficulty-atcoder-difficulty-display";
  20. /**
  21. * 後方互換処理
  22. */
  23. const backwardCompatibleProcessing = () => {
  24. const oldLocalStorageKeys = [
  25. "atcoderDifficultyDisplayUserSubmissions",
  26. "atcoderDifficultyDisplayUserSubmissionslastFetchedAt",
  27. "atcoderDifficultyDisplayEstimatedDifficulties",
  28. "atcoderDifficultyDisplayEstimatedDifficultieslastFetchedAt",
  29. ];
  30. /** 過去バージョンのlocalStorageデータを削除する */
  31. oldLocalStorageKeys.forEach((key) => {
  32. localStorage.removeItem(key);
  33. });
  34. };
  35. const getTypical90Difficulty = (title) => {
  36. if (title.includes("★1"))
  37. return 149;
  38. if (title.includes("★2"))
  39. return 399;
  40. if (title.includes("★3"))
  41. return 799;
  42. if (title.includes("★4"))
  43. return 1199;
  44. if (title.includes("★5"))
  45. return 1599;
  46. if (title.includes("★6"))
  47. return 1999;
  48. if (title.includes("★7"))
  49. return 2399;
  50. return NaN;
  51. };
  52. const getTypical90Description = (title) => {
  53. if (title.includes("★1"))
  54. return "200 点問題レベル";
  55. if (title.includes("★2"))
  56. return "300 点問題レベル";
  57. if (title.includes("★3"))
  58. return "";
  59. if (title.includes("★4"))
  60. return "400 点問題レベル";
  61. if (title.includes("★5"))
  62. return "500 点問題レベル";
  63. if (title.includes("★6"))
  64. return "これが安定して解ければ上級者です";
  65. if (title.includes("★7"))
  66. return "チャレンジ問題枠です";
  67. return "エラー: 競プロ典型 90 問の難易度読み取りに失敗しました";
  68. };
  69. const addTypical90Difficulty = (problemModels, problems) => {
  70. const models = problemModels;
  71. const problemsT90 = problems.filter((element) => element.contest_id === "typical90");
  72. problemsT90.forEach((element) => {
  73. const difficulty = getTypical90Difficulty(element.title);
  74. const model = {
  75. slope: NaN,
  76. intercept: NaN,
  77. variance: NaN,
  78. difficulty,
  79. discrimination: NaN,
  80. irt_loglikelihood: NaN,
  81. irt_users: NaN,
  82. is_experimental: false,
  83. extra_difficulty: `${getTypical90Description(element.title)}`,
  84. };
  85. models[element.id] = model;
  86. });
  87. return models;
  88. };
  89.  
  90. // 次のコードを引用
  91. // [AtCoderProblems/theme\.ts at master · kenkoooo/AtCoderProblems](https://github.com/kenkoooo/AtCoderProblems/blob/master/atcoder-problems-frontend/src/style/theme.ts)
  92. // 8b1b86c740e627e59abf056a11c00582e12b30ff
  93. const ThemeLight = {
  94. difficultyBlackColor: "#404040",
  95. difficultyGreyColor: "#808080",
  96. difficultyBrownColor: "#804000",
  97. difficultyGreenColor: "#008000",
  98. difficultyCyanColor: "#00C0C0",
  99. difficultyBlueColor: "#0000FF",
  100. difficultyYellowColor: "#C0C000",
  101. difficultyOrangeColor: "#FF8000",
  102. difficultyRedColor: "#FF0000",
  103. };
  104. ({
  105. ...ThemeLight,
  106. difficultyBlackColor: "#FFFFFF",
  107. difficultyGreyColor: "#C0C0C0",
  108. difficultyBrownColor: "#B08C56",
  109. difficultyGreenColor: "#3FAF3F",
  110. difficultyCyanColor: "#42E0E0",
  111. difficultyBlueColor: "#8888FF",
  112. difficultyYellowColor: "#FFFF56",
  113. difficultyOrangeColor: "#FFB836",
  114. difficultyRedColor: "#FF6767",
  115. });
  116.  
  117. // 次のコードを引用・編集
  118. const clipDifficulty = (difficulty) => Math.round(difficulty >= 400 ? difficulty : 400 / Math.exp(1.0 - difficulty / 400));
  119. const RatingColors = [
  120. "Black",
  121. "Grey",
  122. "Brown",
  123. "Green",
  124. "Cyan",
  125. "Blue",
  126. "Yellow",
  127. "Orange",
  128. "Red",
  129. ];
  130. const getRatingColor = (rating) => {
  131. const index = Math.min(Math.floor(rating / 400), RatingColors.length - 2);
  132. return RatingColors[index + 1] ?? "Black";
  133. };
  134. const getRatingColorClass = (rating) => {
  135. const ratingColor = getRatingColor(rating);
  136. switch (ratingColor) {
  137. case "Black":
  138. return "difficulty-black";
  139. case "Grey":
  140. return "difficulty-grey";
  141. case "Brown":
  142. return "difficulty-brown";
  143. case "Green":
  144. return "difficulty-green";
  145. case "Cyan":
  146. return "difficulty-cyan";
  147. case "Blue":
  148. return "difficulty-blue";
  149. case "Yellow":
  150. return "difficulty-yellow";
  151. case "Orange":
  152. return "difficulty-orange";
  153. case "Red":
  154. return "difficulty-red";
  155. default:
  156. return "difficulty-black";
  157. }
  158. };
  159. const getRatingColorCode = (ratingColor, theme = ThemeLight) => {
  160. switch (ratingColor) {
  161. case "Black":
  162. return theme.difficultyBlackColor;
  163. case "Grey":
  164. return theme.difficultyGreyColor;
  165. case "Brown":
  166. return theme.difficultyBrownColor;
  167. case "Green":
  168. return theme.difficultyGreenColor;
  169. case "Cyan":
  170. return theme.difficultyCyanColor;
  171. case "Blue":
  172. return theme.difficultyBlueColor;
  173. case "Yellow":
  174. return theme.difficultyYellowColor;
  175. case "Orange":
  176. return theme.difficultyOrangeColor;
  177. case "Red":
  178. return theme.difficultyRedColor;
  179. default:
  180. return theme.difficultyBlackColor;
  181. }
  182. };
  183.  
  184. // 次のコードを引用・編集
  185. // FIXME: ダークテーマ対応
  186. const useTheme = () => ThemeLight;
  187. const getRatingMetalColorCode = (metalColor) => {
  188. switch (metalColor) {
  189. case "Bronze":
  190. return { base: "#965C2C", highlight: "#FFDABD" };
  191. case "Silver":
  192. return { base: "#808080", highlight: "white" };
  193. case "Gold":
  194. return { base: "#FFD700", highlight: "white" };
  195. default:
  196. return { base: "#FFD700", highlight: "white" };
  197. }
  198. };
  199. const getStyleOptions = (color, fillRatio, theme) => {
  200. if (color === "Bronze" || color === "Silver" || color === "Gold") {
  201. const metalColor = getRatingMetalColorCode(color);
  202. return {
  203. borderColor: metalColor.base,
  204. background: `linear-gradient(to right, \
  205. ${metalColor.base}, ${metalColor.highlight}, ${metalColor.base})`,
  206. };
  207. }
  208. const colorCode = getRatingColorCode(color, theme);
  209. return {
  210. borderColor: colorCode,
  211. background: `border-box linear-gradient(to top, \
  212. ${colorCode} ${fillRatio * 100}%, \
  213. rgba(0,0,0,0) ${fillRatio * 100}%)`,
  214. };
  215. };
  216. const topcoderLikeCircle = (color, rating, big = true, extraDescription = "") => {
  217. const fillRatio = rating >= 3200 ? 1.0 : (rating % 400) / 400;
  218. const className = `topcoder-like-circle
  219. ${big ? "topcoder-like-circle-big" : ""} rating-circle`;
  220. const theme = useTheme();
  221. const styleOptions = getStyleOptions(color, fillRatio, theme);
  222. const styleOptionsString = `border-color: ${styleOptions.borderColor}; background: ${styleOptions.background};`;
  223. const content = extraDescription
  224. ? `Difficulty: ${extraDescription}`
  225. : `Difficulty: ${rating}`;
  226. // FIXME: TooltipにSolve Prob, Solve Timeを追加
  227. return `<span
  228. class="${className}" style="${styleOptionsString}"
  229. data-toggle="tooltip" title="${content}" data-placement="bottom"
  230. />`;
  231. };
  232.  
  233. // 次のコードを引用・編集
  234. const getColor = (difficulty) => {
  235. if (difficulty < 3200)
  236. return getRatingColor(difficulty);
  237. if (difficulty < 3600)
  238. return "Bronze";
  239. if (difficulty < 4000)
  240. return "Silver";
  241. return "Gold";
  242. };
  243. const difficultyCircle = (difficulty, big = true, extraDescription = "") => {
  244. if (Number.isNaN(difficulty)) {
  245. // Unavailableの難易度円はProblemsとは異なりGlyphiconの「?」を使用
  246. const className = `glyphicon glyphicon-question-sign aria-hidden='true'
  247. difficulty-unavailable
  248. ${big ? "difficulty-unavailable-icon-big" : "difficulty-unavailable-icon"}`;
  249. const content = "Difficulty is unavailable.";
  250. return `<span
  251. class="${className}"
  252. data-toggle="tooltip" title="${content}" data-placement="bottom"
  253. />`;
  254. }
  255. const color = getColor(difficulty);
  256. return topcoderLikeCircle(color, difficulty, big, extraDescription);
  257. };
  258.  
  259. var html = "<h2>AtCoder Difficulty Display</h2>\n<hr>\n<a href=\"https://github.com/hotarunx/AtCoderDifficultyDisplay\">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";
  260.  
  261. 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}";
  262.  
  263. // AtCoderの問題ページをパースする
  264. /**
  265. * URLをパースする パラメータを消す \
  266. * 例: in: https://atcoder.jp/contests/abc210?lang=en \
  267. * 例: out: (5)['https:', '', 'atcoder.jp', 'contests', 'abc210']
  268. */
  269. const parseURL = (url) => {
  270. // 区切り文字`/`で分割する
  271. // ?以降の文字列を削除してパラメータを削除する
  272. return url.split("/").map((x) => x.replace(/\?.*/i, ""));
  273. };
  274. const URL = parseURL(window.location.href);
  275. /**
  276. * 表セル要素から、前の要素のテキストが引数と一致する要素を探す
  277. * 個別の提出ページで使うことを想定
  278. * 例: searchSubmissionInfo(["問題", "Task"])
  279. */
  280. const searchSubmissionInfo = (key) => {
  281. const tdTags = document.getElementsByTagName("td");
  282. const tdTagsArray = Array.prototype.slice.call(tdTags);
  283. return tdTagsArray.filter((elem) => {
  284. const prevElem = elem.previousElementSibling;
  285. const text = prevElem?.textContent;
  286. if (typeof text === "string")
  287. return key.includes(text);
  288. return false;
  289. })[0];
  290. };
  291. /** コンテストタイトル 例: AtCoder Beginner Contest 210 */
  292. document.getElementsByClassName("contest-title")[0]?.textContent ?? "";
  293. /** コンテストID 例: abc210 */
  294. const contestID = URL[4] ?? "";
  295. /**
  296. * ページ種類 \
  297. * 基本的にコンテストIDの次のパス
  298. * ### 例外
  299. * 個別の問題: task
  300. * 個別の提出: submission
  301. * 個別の問題ページで解説ボタンを押すと遷移する個別の問題の解説一覧ページ: task_editorial
  302. */
  303. const pageType = (() => {
  304. if (URL.length < 6)
  305. return "";
  306. if (URL.length >= 7 && URL[5] === "submissions" && URL[6] !== "me")
  307. return "submission";
  308. if (URL.length >= 8 && URL[5] === "tasks" && URL[7] === "editorial")
  309. return "task_editorial";
  310. if (URL.length >= 7 && URL[5] === "tasks")
  311. return "task";
  312. return URL[5] ?? "";
  313. })();
  314. /** 問題ID 例: abc210_a */
  315. const taskID = (() => {
  316. if (pageType === "task") {
  317. // 問題ページでは、URLから問題IDを取り出す
  318. return URL[6] ?? "";
  319. }
  320. if (pageType === "submission") {
  321. // 個別の提出ページでは、問題リンクのURLから問題IDを取り出す
  322. // 提出情報の問題のURLを取得する
  323. const taskCell = searchSubmissionInfo(["問題", "Task"]);
  324. if (!taskCell)
  325. return "";
  326. const taskLink = taskCell.getElementsByTagName("a")[0];
  327. if (!taskLink)
  328. return "";
  329. const taskUrl = parseURL(taskLink.href);
  330. const taskIDParsed = taskUrl[6] ?? "";
  331. return taskIDParsed;
  332. }
  333. return "";
  334. })();
  335. /** 問題名 例: A - Cabbages */
  336. (() => {
  337. if (pageType === "task") {
  338. // 問題ページでは、h2から問題名を取り出す
  339. return (document
  340. .getElementsByClassName("h2")[0]
  341. ?.textContent?.trim()
  342. .replace(/\n.*/i, "") ?? "");
  343. }
  344. if (pageType === "submission") {
  345. // 個別の提出ページでは、問題リンクのテキストから問題名を取り出す
  346. // 提出情報の問題のテキストを取得する
  347. const taskCell = searchSubmissionInfo(["問題", "Task"]);
  348. if (!taskCell)
  349. return "";
  350. const taskLink = taskCell.getElementsByTagName("a")[0];
  351. if (!taskLink)
  352. return "";
  353. return taskLink.textContent ?? "";
  354. }
  355. return "";
  356. })();
  357. /** 提出ユーザー 例: machikane */
  358. (() => {
  359. if (pageType !== "submission")
  360. return "";
  361. // 個別の提出ページのとき
  362. const userCell = searchSubmissionInfo(["ユーザ", "User"]);
  363. if (!userCell)
  364. return "";
  365. return userCell?.textContent?.trim() ?? "";
  366. })();
  367. /** 提出結果 例: AC */
  368. (() => {
  369. if (pageType !== "submission")
  370. return "";
  371. // 個別の提出ページのとき
  372. const statusCell = searchSubmissionInfo(["結果", "Status"]);
  373. if (!statusCell)
  374. return "";
  375. return statusCell?.textContent?.trim() ?? "";
  376. })();
  377. /** 得点 例: 100 */
  378. (() => {
  379. if (pageType !== "submission")
  380. return 0;
  381. // 個別の提出ページのとき
  382. const scoreCell = searchSubmissionInfo(["得点", "Score"]);
  383. if (!scoreCell)
  384. return 0;
  385. return parseInt(scoreCell?.textContent?.trim() ?? "0", 10);
  386. })();
  387.  
  388. /**
  389. * 得点が最大の提出を返す
  390. */
  391. const parseMaxScore = (submissionsArg) => {
  392. if (submissionsArg.length === 0) {
  393. return undefined;
  394. }
  395. const maxScore = submissionsArg.reduce((left, right) => left.point > right.point ? left : right);
  396. return maxScore;
  397. };
  398. /**
  399. * ペナルティ数を数える
  400. */
  401. const parsePenalties = (submissionsArg) => {
  402. let penalties = 0;
  403. let hasAccepted = false;
  404. submissionsArg.forEach((element) => {
  405. hasAccepted = element.result === "AC" || hasAccepted;
  406. if (!hasAccepted && !nonPenaltyJudge.includes(element.result)) {
  407. penalties += 1;
  408. }
  409. });
  410. return penalties;
  411. };
  412. /**
  413. * 最初にACした提出を返す
  414. */
  415. const parseFirstAcceptedTime = (submissionsArg) => {
  416. const ac = submissionsArg.filter((element) => element.result === "AC");
  417. return ac[0];
  418. };
  419. /**
  420. * 代表的な提出を返す
  421. * 1. 最後にACした提出
  422. * 2. 最後の提出
  423. * 3. undefined
  424. */
  425. const parseRepresentativeSubmission = (submissionsArg) => {
  426. const ac = submissionsArg.filter((element) => element.result === "AC");
  427. const nonAC = submissionsArg.filter((element) => element.result !== "AC");
  428. if (ac.length > 0)
  429. return ac.slice(-1)[0];
  430. if (nonAC.length > 0)
  431. return nonAC.slice(-1)[0];
  432. return undefined;
  433. };
  434. /**
  435. * 提出をパースして情報を返す
  436. * 対象: コンテスト前,中,後の提出 別コンテストの同じ問題への提出
  437. * 返す情報: 得点が最大の提出 最初のACの提出 代表的な提出 ペナルティ数
  438. */
  439. const analyzeSubmissions = (submissionsArg) => {
  440. const submissions = submissionsArg.filter((element) => element.problem_id === taskID);
  441. const beforeContest = submissions.filter((element) => element.contest_id === contestID &&
  442. element.epoch_second < startTime.unix());
  443. const duringContest = submissions.filter((element) => element.contest_id === contestID &&
  444. element.epoch_second >= startTime.unix() &&
  445. element.epoch_second < endTime.unix());
  446. const afterContest = submissions.filter((element) => element.contest_id === contestID && element.epoch_second >= endTime.unix());
  447. const anotherContest = submissions.filter((element) => element.contest_id !== contestID);
  448. return {
  449. before: {
  450. maxScore: parseMaxScore(beforeContest),
  451. firstAc: parseFirstAcceptedTime(beforeContest),
  452. representative: parseRepresentativeSubmission(beforeContest),
  453. },
  454. during: {
  455. maxScore: parseMaxScore(duringContest),
  456. firstAc: parseFirstAcceptedTime(duringContest),
  457. representative: parseRepresentativeSubmission(duringContest),
  458. penalties: parsePenalties(duringContest),
  459. },
  460. after: {
  461. maxScore: parseMaxScore(afterContest),
  462. firstAc: parseFirstAcceptedTime(afterContest),
  463. representative: parseRepresentativeSubmission(afterContest),
  464. },
  465. another: {
  466. maxScore: parseMaxScore(anotherContest),
  467. firstAc: parseFirstAcceptedTime(anotherContest),
  468. representative: parseRepresentativeSubmission(anotherContest),
  469. },
  470. };
  471. };
  472. /**
  473. * 提出状況を表すラベルを生成
  474. */
  475. const generateStatusLabel = (submission, type) => {
  476. if (submission === undefined) {
  477. return "";
  478. }
  479. const isAC = submission.result === "AC";
  480. let className = "";
  481. switch (type) {
  482. case "before":
  483. className = "label-primary";
  484. break;
  485. case "during":
  486. className = isAC ? "label-success" : "label-warning";
  487. break;
  488. case "after":
  489. className = isAC
  490. ? "label-success-after-contest"
  491. : "label-warning-after-contest";
  492. break;
  493. case "another":
  494. className = "label-default";
  495. break;
  496. }
  497. let content = "";
  498. switch (type) {
  499. case "before":
  500. content = "コンテスト前の提出";
  501. break;
  502. case "during":
  503. content = "コンテスト中の提出";
  504. break;
  505. case "after":
  506. content = "コンテスト後の提出";
  507. break;
  508. case "another":
  509. content = "別コンテストの同じ問題への提出";
  510. break;
  511. }
  512. const href = `https://atcoder.jp/contests/${submission.contest_id}/submissions/${submission.id}`;
  513. return `<span class="label ${className}"
  514. data-toggle="tooltip" data-placement="bottom" title="${content}">
  515. <a class="label-status-a" href=${href}>${submission.result}</a>
  516. </span> `;
  517. };
  518. /**
  519. * ペナルティ数を表示
  520. */
  521. const generatePenaltiesCount = (penalties) => {
  522. if (penalties <= 0) {
  523. return "";
  524. }
  525. const content = "コンテスト中のペナルティ数";
  526. return `<span data-toggle="tooltip" data-placement="bottom" title="${content}"class="difficulty-red" style='font-weight: bold; font-size: x-small;'>
  527. (${penalties.toString()})
  528. </span>`;
  529. };
  530. /**
  531. * 最初のACの時間を表示
  532. */
  533. const generateFirstAcTime = (submission) => {
  534. if (submission === undefined) {
  535. return "";
  536. }
  537. const content = "提出時間";
  538. const href = `https://atcoder.jp/contests/${submission.contest_id}/submissions/${submission.id}`;
  539. const elapsed = submission.epoch_second - startTime.unix();
  540. const elapsedSeconds = elapsed % 60;
  541. const elapsedMinutes = Math.trunc(elapsed / 60);
  542. return `<span data-toggle="tooltip" data-placement="bottom" title="${content}">
  543. <a class="difficulty-orange" style='font-weight: bold; font-size: x-small;' href=${href}>
  544. ${elapsedMinutes}:${elapsedSeconds}
  545. </a>
  546. </span>`;
  547. };
  548. /**
  549. * マラソン用に得点を表示するスパンを生成
  550. */
  551. const generateScoreSpan = (submission, type) => {
  552. if (submission === undefined) {
  553. return "";
  554. }
  555. // マラソン用を考えているのでとりあえず1万点未満は表示しない
  556. if (submission.point < 10000) {
  557. return "";
  558. }
  559. let className = "";
  560. switch (type) {
  561. case "before":
  562. className = "difficulty-blue";
  563. break;
  564. case "during":
  565. className = "difficulty-green";
  566. break;
  567. case "after":
  568. className = "difficulty-yellow";
  569. break;
  570. case "another":
  571. className = "difficulty-grey";
  572. break;
  573. }
  574. let content = "";
  575. switch (type) {
  576. case "before":
  577. content = "コンテスト前の提出";
  578. break;
  579. case "during":
  580. content = "コンテスト中の提出";
  581. break;
  582. case "after":
  583. content = "コンテスト後の提出";
  584. break;
  585. case "another":
  586. content = "別コンテストの同じ問題への提出";
  587. break;
  588. }
  589. const href = `https://atcoder.jp/contests/${submission.contest_id}/submissions/${submission.id}`;
  590. return `<span
  591. data-toggle="tooltip" data-placement="bottom" title="${content}">
  592. <a class="${className}" style='font-weight: bold;' href=${href}>
  593. ${submission.point}
  594. </a>
  595. </span> `;
  596. };
  597.  
  598. /**
  599. * 色付け対象の要素の配列を取得する
  600. * * 個別の問題ページのタイトル
  601. * * 問題へのリンク
  602. * * 解説ページのH3の問題名
  603. */
  604. const getElementsColorizable = () => {
  605. const elementsColorizable = [];
  606. // 問題ページのタイトル
  607. if (pageType === "task") {
  608. const element = document.getElementsByClassName("h2")[0];
  609. if (element) {
  610. elementsColorizable.push({ element, taskID, big: true });
  611. }
  612. }
  613. // aタグ要素 問題ページ、提出ページ等のリンクを想定
  614. const aTagsRaw = document.getElementsByTagName("a");
  615. let aTagsArray = Array.prototype.slice.call(aTagsRaw);
  616. // 問題ページの一番左の要素は除く 見た目の問題です
  617. aTagsArray = aTagsArray.filter((element) => !((pageType === "tasks" || pageType === "score") &&
  618. !element.parentElement?.previousElementSibling));
  619. // 左上の日本語/英語切り替えリンクは除く
  620. aTagsArray = aTagsArray.filter((element) => !element.href.includes("?lang="));
  621. // 解説ページの問題名の右のリンクは除く
  622. aTagsArray = aTagsArray.filter((element) => !(pageType === "editorial" &&
  623. element.children[0]?.classList.contains("glyphicon-new-window")));
  624. const aTagsConverted = aTagsArray.map((element) => {
  625. const url = parseURL(element.href);
  626. const taskIDFromURL = (url[url.length - 2] ?? "") === "tasks" ? url[url.length - 1] ?? "" : "";
  627. // 個別の解説ページではbig
  628. const big = element.parentElement?.tagName.includes("H2") ?? false;
  629. // Comfortable AtCoderのドロップダウンではafterbegin
  630. const afterbegin = element.parentElement?.parentElement?.classList.contains("dropdown-menu") ?? false;
  631. return { element, taskID: taskIDFromURL, big, afterbegin };
  632. });
  633. elementsColorizable.push(...aTagsConverted);
  634. // h3タグ要素 解説ページの問題名を想定
  635. const h3TagsRaw = document.getElementsByTagName("h3");
  636. const h3TagsArray = Array.prototype.slice.call(h3TagsRaw);
  637. const h3TagsConverted = h3TagsArray.map((element) => {
  638. const url = parseURL(element.getElementsByTagName("a")[0]?.href ?? "");
  639. const taskIDFromURL = (url[url.length - 2] ?? "") === "tasks" ? url[url.length - 1] ?? "" : "";
  640. return { element, taskID: taskIDFromURL, big: true, afterbegin: true };
  641. });
  642. // FIXME: 別ユーザースクリプトが指定した要素を色付けする機能
  643. // 指定したクラスがあれば対象とすることを考えている
  644. // ユーザースクリプトの実行順はユーザースクリプトマネージャーの設定で変更可能
  645. elementsColorizable.push(...h3TagsConverted);
  646. return elementsColorizable;
  647. };
  648. /**
  649. * 問題ステータス(実行時間制限とメモリ制限が書かれた部分)のHTMLオブジェクトを取得
  650. */
  651. const getElementOfProblemStatus = () => {
  652. if (pageType !== "task")
  653. return undefined;
  654. const psRaw = document
  655. ?.getElementById("main-container")
  656. ?.getElementsByTagName("p");
  657. const ps = Array.prototype.slice.call(psRaw);
  658. if (!psRaw)
  659. return undefined;
  660. const problemStatuses = ps.filter((p) => {
  661. return (p.textContent?.includes("メモリ制限") ||
  662. p.textContent?.includes("Memory Limit"));
  663. });
  664. return problemStatuses[0];
  665. };
  666.  
  667. /** 常設コンテストID一覧 */
  668. const permanentContestIDs = [
  669. "practice",
  670. "APG4b",
  671. "abs",
  672. "practice2",
  673. "typical90",
  674. "math-and-algorithm",
  675. "tessoku-book",
  676. ];
  677. // FIXME: FIXME: Problemsでデータ取れなかったらコンテストが終了していない判定で良さそう
  678. /**
  679. * 開いているページのコンテストが終了していればtrue \
  680. * 例外処理として以下の場合もtrueを返す
  681. * * コンテストが常設コンテスト
  682. * * コンテストのページ以外にいる <https://atcoder.jp/contests/*>
  683. */
  684. var isContestOver = () => {
  685. if (!(URL[3] === "contests" && URL.length >= 5))
  686. return true;
  687. if (permanentContestIDs.includes(contestID))
  688. return true;
  689. return Date.now() > endTime.valueOf();
  690. };
  691.  
  692. /**
  693. * コンテストページ <https://atcoder.jp/contests/*> の処理 \
  694. * メインの処理
  695. */
  696. const contestPageProcess = async () => {
  697. // コンテスト終了前は不要なので無効化する
  698. if (!isContestOver())
  699. return;
  700. // FIXME: ダークテーマ対応
  701. GM_addStyle(css);
  702. /** 問題一覧取得 */
  703. // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  704. // @ts-ignore
  705. const problems = await getProblems();
  706. /** 難易度取得 */
  707. const problemModels = addTypical90Difficulty(
  708. // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  709. // @ts-ignore
  710. await getEstimatedDifficulties(), problems);
  711. // FIXME: PAST対応
  712. // FIXME: JOI非公式難易度表対応
  713. /** 提出状況取得 */
  714. // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  715. // @ts-ignore
  716. const submissions = await getSubmissions(userScreenName);
  717. // 色付け対象の要素の配列を取得する
  718. // 難易度が無いものを除く
  719. const elementsColorizable = getElementsColorizable().filter((element) => element.taskID in problemModels);
  720. // 問題ステータス(個別の問題ページの実行時間制限とメモリ制限が書かれた部分)を取得する
  721. const elementProblemStatus = getElementOfProblemStatus();
  722. /**
  723. * 色付け処理を実行する
  724. */
  725. const colorizeElement = () => {
  726. // 問題見出し、問題リンクを色付け
  727. elementsColorizable.forEach((element) => {
  728. const model = problemModels[element.taskID];
  729. // 難易度がUnavailableならばdifficultyプロパティが無い
  730. // difficultyの値をNaNとする
  731. const difficulty = clipDifficulty(model?.difficulty ?? NaN);
  732. // 色付け
  733. if (!Number.isNaN(difficulty)) {
  734. const color = getRatingColorClass(difficulty);
  735. // eslint-disable-next-line no-param-reassign
  736. element.element.classList.add(color);
  737. }
  738. else {
  739. element.element.classList.add("difficulty-unavailable");
  740. }
  741. // 🧪追加
  742. if (model?.is_experimental) {
  743. element.element.insertAdjacentText("afterbegin", "🧪");
  744. }
  745. // ◒難易度円追加
  746. element.element.insertAdjacentHTML(element.afterbegin ? "afterbegin" : "beforebegin", difficultyCircle(difficulty, element.big, model?.extra_difficulty));
  747. });
  748. // 個別の問題ページのところに難易度等情報を追加
  749. if (elementProblemStatus) {
  750. // 難易度の値を表示する
  751. // 難易度推定の対象外なら、この値はundefined
  752. const model = problemModels[taskID];
  753. // 難易度がUnavailableのときはdifficultyの値をNaNとする
  754. // 難易度がUnavailableならばdifficultyプロパティが無い
  755. const difficulty = clipDifficulty(model?.difficulty ?? NaN);
  756. // 色付け
  757. let className = "";
  758. if (difficulty) {
  759. className = getRatingColorClass(difficulty);
  760. }
  761. else if (model) {
  762. className = "difficulty-unavailable";
  763. }
  764. else {
  765. className = "";
  766. }
  767. // Difficultyの値設定
  768. let value = "";
  769. if (difficulty) {
  770. value = difficulty.toString();
  771. }
  772. else if (model) {
  773. value = "Unavailable";
  774. }
  775. else {
  776. value = "None";
  777. }
  778. // 🧪追加
  779. const experimentalText = model?.is_experimental ? "🧪" : "";
  780. const content = `${experimentalText}${value}`;
  781. elementProblemStatus.insertAdjacentHTML("beforeend", ` / Difficulty:
  782. <span style='font-weight: bold;' class="${className}">${content}</span>`);
  783. /** この問題への提出 提出時間ソート済みと想定 */
  784. const thisTaskSubmissions = submissions.filter((element) => element.problem_id === taskID);
  785. const analyze = analyzeSubmissions(thisTaskSubmissions);
  786. // コンテスト前中後外の提出状況 コンテスト中の解答時間とペナルティ数を表示する
  787. let statuesHTML = "";
  788. statuesHTML += generateStatusLabel(analyze.before.representative, "before");
  789. statuesHTML += generateStatusLabel(analyze.during.representative, "during");
  790. statuesHTML += generateStatusLabel(analyze.after.representative, "after");
  791. statuesHTML += generateStatusLabel(analyze.another.representative, "another");
  792. statuesHTML += generatePenaltiesCount(analyze.during.penalties);
  793. statuesHTML += generateFirstAcTime(analyze.during.firstAc);
  794. if (statuesHTML.length > 0) {
  795. elementProblemStatus.insertAdjacentHTML("beforeend", ` / Status: ${statuesHTML}`);
  796. }
  797. // コンテスト前中後外の1万点以上の最大得点を表示する
  798. // NOTE: マラソン用のため、1万点以上とした
  799. let scoresHTML = "";
  800. scoresHTML += generateScoreSpan(analyze.before.maxScore, "before");
  801. scoresHTML += generateScoreSpan(analyze.during.maxScore, "during");
  802. scoresHTML += generateScoreSpan(analyze.after.maxScore, "after");
  803. scoresHTML += generateScoreSpan(analyze.another.maxScore, "another");
  804. if (scoresHTML.length > 0) {
  805. elementProblemStatus.insertAdjacentHTML("beforeend", ` / Scores: ${scoresHTML}`);
  806. }
  807. }
  808. // bootstrap3のtooltipを有効化 難易度円の値を表示するtooltip
  809. // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  810. // @ts-ignore
  811. // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call, no-undef
  812. $('[data-toggle="tooltip"]').tooltip();
  813. };
  814. // 色付け処理実行
  815. if (!GM_getValue(hideDifficultyID, false)) {
  816. // 設定 ネタバレ防止がOFFなら何もせず実行
  817. colorizeElement();
  818. }
  819. else {
  820. // 設定 ネタバレ防止がONなら
  821. // ページ上部にボタンを追加 押すと色付け処理が実行される
  822. const place = document.getElementsByTagName("h2")[0] ??
  823. document.getElementsByClassName("h2")[0] ??
  824. undefined;
  825. if (place) {
  826. place.insertAdjacentHTML("beforebegin", `<input type="button" id="${hideDifficultyID}" class="btn btn-info"
  827. value="Show Difficulty" />`);
  828. const button = document.getElementById(hideDifficultyID);
  829. if (button) {
  830. button.addEventListener("click", () => {
  831. button.style.display = "none";
  832. colorizeElement();
  833. });
  834. }
  835. }
  836. }
  837. };
  838. /**
  839. * 設定ページ <https://atcoder.jp/settings> の処理 \
  840. * 設定ボタンを追加する
  841. */
  842. const settingPageProcess = () => {
  843. const insertion = document.getElementsByClassName("form-horizontal")[0];
  844. if (insertion === undefined)
  845. return;
  846. insertion.insertAdjacentHTML("afterend", html);
  847. // 設定 ネタバレ防止のチェックボックスの読み込み 切り替え 保存処理を追加
  848. const hideDifficultyChechbox = document.getElementById(hideDifficultyID);
  849. if (hideDifficultyChechbox &&
  850. hideDifficultyChechbox instanceof HTMLInputElement) {
  851. hideDifficultyChechbox.checked = GM_getValue(hideDifficultyID, false);
  852. hideDifficultyChechbox.addEventListener("change", () => {
  853. GM_setValue(hideDifficultyID, hideDifficultyChechbox.checked);
  854. });
  855. }
  856. };
  857. /**
  858. * 最初に実行される部分 \
  859. * 共通の処理を実行した後ページごとの処理を実行する
  860. */
  861. (async () => {
  862. // 共通の処理
  863. backwardCompatibleProcessing();
  864. // ページ別の処理
  865. if (URL[3] === "contests" && URL.length >= 5) {
  866. await contestPageProcess();
  867. }
  868. if (URL[3] === "settings" && URL.length === 4) {
  869. settingPageProcess();
  870. }
  871. })().catch((error) => {
  872. // eslint-disable-next-line no-console
  873. console.error("[AtCoderDifficultyDisplay]", error);
  874. });