Greasy Fork is available in English.

atcoder-difficulty-display

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

ของเมื่อวันที่ 09-07-2023 ดู เวอร์ชันล่าสุด

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