atcoder-tasks-page-colorize-during-contests

tasks ページにおいて,提出した問題に色付けを行います.開催中のコンテストの色付けについて,atcoder-tasks-page-colorizer が対応していないため,これを補完します.

当前为 2021-07-10 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name atcoder-tasks-page-colorize-during-contests
  3. // @namespace iilj
  4. // @version 2021.7.11.0
  5. // @description tasks ページにおいて,提出した問題に色付けを行います.開催中のコンテストの色付けについて,atcoder-tasks-page-colorizer が対応していないため,これを補完します.
  6. // @author iilj
  7. // @supportURL https://github.com/iilj/atcoder-tasks-page-colorize-during-contests/issues
  8. // @match https://atcoder.jp/contests/*/tasks
  9. // ==/UserScript==
  10.  
  11. /**
  12. * 問題ごとの結果エントリ
  13. * @typedef {Object} TaskResultEntry
  14. * @property {any} Additional 謎
  15. * @property {number} Count 提出回数
  16. * @property {number} Elapsed コンテスト開始からの経過時間 [ns].
  17. * @property {number} Failure 非 AC の提出数(ACするまではペナルティではない).
  18. * @property {boolean} Frozen アカウントが凍結済みかどうか?
  19. * @property {number} Penalty ペナルティ数
  20. * @property {boolean} Pending ジャッジ中かどうか?
  21. * @property {number} Score 得点(×100)
  22. * @property {number} Status 1 のとき満点? 6 のとき部分点?
  23. */
  24.  
  25. /**
  26. * 全問題の結果
  27. * @typedef {Object} TotalResultEntry
  28. * @property {number} Accepted 正解した問題数
  29. * @property {any} Additional 謎
  30. * @property {number} Count 提出回数
  31. * @property {number} Elapsed コンテスト開始からの経過時間 [ns].
  32. * @property {boolean} Frozen アカウントが凍結済みかどうか?
  33. * @property {number} Penalty ペナルティ数
  34. * @property {number} Score 得点(×100)
  35. */
  36.  
  37. /**
  38. * 順位表エントリ
  39. * @typedef {Object} StandingsEntry
  40. * @property {any} Additional 謎
  41. * @property {string} Affiliation 所属.IsTeam = true のときは,チームメンバを「, 」で結合した文字列.
  42. * @property {number} AtCoderRank AtCoder 内順位
  43. * @property {number} Competitions Rated コンテスト参加回数
  44. * @property {string} Country 国ラベル."JP" など.
  45. * @property {string} DisplayName 表示名."hitonanode" など.
  46. * @property {number} EntireRank コンテスト順位?
  47. * @property {boolean} IsRated Rated かどうか
  48. * @property {boolean} IsTeam チームかどうか
  49. * @property {number} OldRating コンテスト前のレーティング.コンテスト後のみ有効.
  50. * @property {number} Rank コンテスト順位?
  51. * @property {number} Rating コンテスト後のレーティング
  52. * @property {{[key: string]: TaskResultEntry}} TaskResults 問題ごとの結果.参加登録していない人は空.
  53. * @property {TotalResultEntry} TotalResult 全体の結果
  54. * @property {boolean} UserIsDeleted ユーザアカウントが削除済みかどうか
  55. * @property {string} UserName ユーザ名."hitonanode" など.
  56. * @property {string} UserScreenName ユーザの表示名."hitonanode" など.
  57. */
  58.  
  59. /**
  60. * 問題エントリ
  61. * @typedef {Object} TaskInfoEntry
  62. * @property {string} Assignment 問題ラベル."A" など.
  63. * @property {string} TaskName 問題名.
  64. * @property {string} TaskScreenName 問題の slug. "abc185_a" など.
  65. */
  66.  
  67. /**
  68. * 順位表情報
  69. * @typedef {Object} Standings
  70. * @property {any} AdditionalColumns 謎
  71. * @property {boolean} Fixed 謎
  72. * @property {StandingsEntry[]} StandingsData 順位表データ
  73. * @property {TaskInfoEntry[]} TaskInfo 問題データ
  74. */
  75.  
  76. /* globals $, contestScreenName, startTime, endTime, userScreenName, moment */
  77.  
  78. (async () => {
  79. 'use strict';
  80.  
  81. // 終了後のコンテストに対する処理は以下のスクリプトに譲る:
  82. // https://greasyfork.org/ja/scripts/380404-atcoder-tasks-page-colorizer
  83. if (moment() >= endTime) return;
  84.  
  85. /** @type {Map<string, [number, string]>} */
  86. const problemId2Info = new Map();
  87. // 自分の得点状況の取得
  88. {
  89. const res = await fetch(`https://atcoder.jp/contests/${contestScreenName}/score`);
  90. const scoreHtml = await res.text();
  91. const parser = new DOMParser();
  92. const doc = parser.parseFromString(scoreHtml, 'text/html');
  93. doc.querySelectorAll('#main-div tbody tr').forEach((tableRow, key) => {
  94. const problemId = tableRow.querySelector('td:nth-child(1) a').getAttribute('href').split('/').pop();
  95. const score = Number(tableRow.querySelector('td:nth-child(3)').textContent);
  96. const datetimeString = tableRow.querySelector('td:nth-child(4)').textContent;
  97. // console.log(problemId, score, datetimeString);
  98. problemId2Info.set(problemId, [score, datetimeString]);
  99. });
  100. }
  101.  
  102. // テーブルの更新
  103. {
  104. document.querySelector('#main-div thead th:last-child').insertAdjacentHTML(
  105. 'beforebegin', '<th width="10%" class="text-center">得点</th><th class="text-center">提出日時</th>');
  106. document.querySelectorAll('#main-div tbody tr').forEach((tableRow, key) => {
  107. const problemId = tableRow.querySelector('td:nth-child(2) a').getAttribute('href').split('/').pop();
  108. const scoreCellId = `score-cell-${problemId}`;
  109. const datetimeCellId = `datetime-cell-${problemId}`;
  110. if (problemId2Info.has(problemId)) {
  111. const [score, datetimeString] = problemId2Info.get(problemId);
  112. tableRow.querySelector('td:last-child').insertAdjacentHTML(
  113. 'beforebegin',
  114. `<td class="text-center" id="${scoreCellId}">${score}</td>
  115. <td class="text-center" id="${datetimeCellId}">${datetimeString}</td>`);
  116. if (datetimeString !== '-') {
  117. tableRow.classList.add(score > 0 ? 'success' : 'danger');
  118. }
  119. } else {
  120. // ここに来ることは本来はないはず(未提出の問題だと [0, '-'] のはず)
  121. tableRow.querySelector('td:last-child').insertAdjacentHTML(
  122. 'beforebegin',
  123. `<td class="text-center" id="${scoreCellId}">-</td>
  124. <td class="text-center" id="${datetimeCellId}">-</td>`);
  125. }
  126. });
  127. }
  128.  
  129. const res = await fetch(`https://atcoder.jp/contests/${contestScreenName}/standings/json`);
  130. /** @type {Standings} */
  131. const standings = await res.json();
  132.  
  133. const standingsEntry = standings.StandingsData.find((_standingsEntry) => _standingsEntry.UserScreenName == userScreenName);
  134.  
  135. document.querySelectorAll('#main-div tbody tr').forEach((tableRow, key) => {
  136. const problemId = tableRow.querySelector('td:nth-child(2) a').getAttribute('href').split('/').pop();
  137. const scoreCellId = `score-cell-${problemId}`;
  138. const datetimeCellId = `datetime-cell-${problemId}`;
  139. if (standingsEntry !== undefined && problemId in standingsEntry.TaskResults) {
  140. const taskResultEntry = standingsEntry.TaskResults[problemId];
  141. const dt = startTime.clone().add(taskResultEntry.Elapsed / 1000000000, 's');
  142. // console.log(dt.format());
  143.  
  144. const [score, datetimeString] = problemId2Info.get(problemId);
  145. const scoreFromStandings = taskResultEntry.Score / 100;
  146. if (scoreFromStandings >= score) {
  147. tableRow.querySelector(`#${scoreCellId}`).textContent = `${scoreFromStandings}`;
  148. tableRow.querySelector(`#${datetimeCellId}`).textContent = `${dt.format("YYYY/MM/DD HH:mm:ss")}`;
  149. }
  150. if (taskResultEntry.Status === 1) {
  151. if (tableRow.classList.contains('danger')) tableRow.classList.remove('danger');
  152. tableRow.classList.add('success');
  153. } else {
  154. if (tableRow.classList.contains('success')) tableRow.classList.remove('success');
  155. tableRow.classList.add('danger');
  156. }
  157. }
  158. });
  159. })();