atcoder-wait-time-display

AtCoder の提出待ち時間を表示します.

当前为 2021-08-23 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name atcoder-wait-time-display
  3. // @namespace iilj
  4. // @version 2021.8.2
  5. // @description AtCoder の提出待ち時間を表示します.
  6. // @author iilj
  7. // @license MIT
  8. // @supportURL https://github.com/iilj/atcoder-wait-time-display/issues
  9. // @match https://atcoder.jp/contests/*/tasks/*
  10. // @grant GM_addStyle
  11. // ==/UserScript==
  12. const pad = (num, length = 2) => `00${num}`.slice(-length);
  13. const formatTime = (hours, minutes, seconds) => {
  14. return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;
  15. };
  16. const secondsToString = (diffWholeSecs) => {
  17. const diffSecs = diffWholeSecs % 60;
  18. const diffMinutes = Math.floor(diffWholeSecs / 60) % 60;
  19. const diffHours = Math.floor(diffWholeSecs / 3600) % 24;
  20. const diffDate = Math.floor(diffWholeSecs / (3600 * 24));
  21. if (diffDate > 0)
  22. return `${diffDate}日`;
  23. return formatTime(diffHours, diffMinutes, diffSecs);
  24. };
  25.  
  26. var css = "div#js-awtd-timer {\n position: fixed;\n right: 10px;\n bottom: 80px;\n width: 160px;\n height: 80px;\n margin: 0;\n padding: 20px 0;\n background-image: url(\"//img.atcoder.jp/assets/contest/digitalclock.png\");\n text-align: center;\n line-height: 20px;\n font-size: 15px;\n cursor: pointer;\n z-index: 50;\n}\ndiv#js-awtd-timer .js-awtd-timer-top {\n color: inherit;\n}\ndiv#js-awtd-timer .js-awtd-timer-bottom {\n color: #cc0000;\n}\n\np#fixed-server-timer {\n box-sizing: border-box;\n}";
  27.  
  28. class Timer {
  29. constructor(lastSubmitTime, submitIntervalSecs) {
  30. this.lastSubmitTime = lastSubmitTime;
  31. this.submitIntervalSecs = submitIntervalSecs;
  32. GM_addStyle(css);
  33. this.element = document.createElement('div');
  34. this.element.id = Timer.ELEMENT_ID;
  35. this.element.title = `間隔:${this.submitIntervalSecs} 秒`;
  36. document.body.appendChild(this.element);
  37. this.top = document.createElement('div');
  38. this.top.classList.add('js-awtd-timer-top');
  39. this.element.appendChild(this.top);
  40. this.bottom = document.createElement('div');
  41. this.bottom.classList.add('js-awtd-timer-bottom');
  42. this.element.appendChild(this.bottom);
  43. this.prevSeconds = -1;
  44. this.intervalID = window.setInterval(() => {
  45. this.updateTime();
  46. }, 100);
  47. this.displayInterval = false;
  48. this.element.addEventListener('click', () => {
  49. this.displayInterval = !this.displayInterval;
  50. this.prevSeconds = -1;
  51. this.updateTime();
  52. });
  53. }
  54. updateTime() {
  55. const currentTime = moment();
  56. const seconds = currentTime.seconds();
  57. if (seconds === this.prevSeconds)
  58. return;
  59. if (this.displayInterval) {
  60. this.top.textContent = '提出間隔';
  61. this.bottom.textContent = `${this.submitIntervalSecs} 秒`;
  62. }
  63. else {
  64. if (this.lastSubmitTime !== null) {
  65. // 経過時間を表示
  66. const elapsedMilliseconds = currentTime.diff(this.lastSubmitTime);
  67. const elapsedSeconds = Math.floor(elapsedMilliseconds / 1000);
  68. this.top.textContent = `経過:${secondsToString(elapsedSeconds)}`;
  69. const waitTime = Math.max(0, this.submitIntervalSecs - elapsedSeconds);
  70. this.bottom.textContent = `待ち:${secondsToString(waitTime)}`;
  71. // if (waitTime > 0) this.bottom.style.color = '#cc0000';
  72. // else this.bottom.style.color = 'inherit';
  73. }
  74. else {
  75. this.top.textContent = 'この問題は';
  76. this.bottom.textContent = '未提出です';
  77. }
  78. }
  79. }
  80. }
  81. Timer.ELEMENT_ID = 'js-awtd-timer';
  82.  
  83. const extractContestAndProblemSlugs = (url) => {
  84. // https://atcoder.jp/contests/*/tasks/*
  85. const urlMatchArray = /^https?:\/\/atcoder\.jp\/contests\/([^/]+)\/tasks\/([^/]+)/.exec(url);
  86. if (urlMatchArray === null) {
  87. throw new Error('url が不正です');
  88. }
  89. return [urlMatchArray[1], urlMatchArray[2]];
  90. };
  91.  
  92. const getRecentSubmissions = async (contestSlug, taskSlug) => {
  93. const res = await fetch(`https://atcoder.jp/contests/${contestSlug}/submissions/me?f.Task=${taskSlug}`);
  94. const text = await res.text();
  95. const dom = new DOMParser().parseFromString(text, 'text/html');
  96. // console.log(dom);
  97. // 2021-05-29 16:15:34+0900
  98. const rows = dom.querySelectorAll('#main-container div.panel.panel-default.panel-submission > div.table-responsive > table > tbody > tr');
  99. const ret = [];
  100. rows.forEach((row) => {
  101. var _a;
  102. const problem = row.querySelector(`a[href^="/contests/${contestSlug}/tasks/${taskSlug}"]`);
  103. if (problem === null) {
  104. throw new Error('テーブルに提出先不明の行があります');
  105. }
  106. const time = row.querySelector('time.fixtime-second');
  107. if (time === null) {
  108. throw new Error('テーブルに提出時刻不明の行があります');
  109. }
  110. const [contestSlugTmp, taskSlugTmp] = extractContestAndProblemSlugs(problem.href);
  111. if (contestSlugTmp !== contestSlug || taskSlugTmp !== taskSlug) {
  112. throw new Error('異なる問題への提出記録が紛れています');
  113. }
  114. const submission = row.querySelector(`a[href^="/contests/${contestSlug}/submissions/"]`);
  115. if (submission === null) {
  116. throw new Error('テーブルに提出 ID 不明の行があります');
  117. }
  118. const statusLabel = row.querySelector('span.label');
  119. if (statusLabel === null) {
  120. throw new Error('提出ステータス不明の行があります');
  121. }
  122. const label = (_a = statusLabel.textContent) === null || _a === void 0 ? void 0 : _a.trim();
  123. if (label === undefined) {
  124. throw new Error('提出ステータスが空の行があります');
  125. }
  126. const submitTime = moment(time.innerText);
  127. ret.push([submission.href, label, submitTime]);
  128. });
  129. return ret;
  130. };
  131. const getSubmitIntervalSecs = async (contestSlug) => {
  132. var _a;
  133. const res = await fetch(`https://atcoder.jp/contests/${contestSlug}?lang=ja`);
  134. const text = await res.text();
  135. const dom = new DOMParser().parseFromString(text, 'text/html');
  136. // 例外的な処理
  137. if (contestSlug === 'wn2017_1') {
  138. return 3600;
  139. }
  140. else if (contestSlug === 'caddi2019') {
  141. return 300;
  142. }
  143. // AHC/HTTF/日本橋ハーフマラソン/Future 仕様の文字列を検索
  144. const candidates = dom.getElementsByTagName('strong');
  145. for (let i = 0; i < candidates.length; ++i) {
  146. const content = (_a = candidates[i].textContent) === null || _a === void 0 ? void 0 : _a.trim();
  147. if (content === undefined)
  148. continue;
  149. // 5分以上の間隔
  150. const matchArray = /^(\d+)(秒|分|時間)以上の間隔/.exec(content);
  151. if (matchArray === null)
  152. continue;
  153. if (matchArray[2] === '秒')
  154. return Number(matchArray[1]);
  155. if (matchArray[2] === '分')
  156. return Number(matchArray[1]) * 60;
  157. if (matchArray[2] === '時間')
  158. return Number(matchArray[1]) * 3600;
  159. }
  160. const statement = dom.getElementById('contest-statement');
  161. if (statement === null) {
  162. throw new Error('コンテスト説明文が見つかりませんでした');
  163. }
  164. const statementText = statement.textContent;
  165. if (statementText === null) {
  166. throw new Error('コンテスト説明文が空です');
  167. }
  168. // Asprova 仕様
  169. // 「提出間隔:プログラム提出後10分間は再提出できません。」
  170. // 「提出後1時間は再提出できません」
  171. // Hitachi Hokudai 仕様
  172. // 「提出直後の1時間は再提出することができません」
  173. // 「提出直後の1時間は、再提出することができません」
  174. // ヤマトコン仕様
  175. // 「提出後30分は再提出することはできません」
  176. {
  177. const matchArray = /提出[^\d]{1,5}(\d+)(秒|分|時間).{1,5}再提出/.exec(statementText);
  178. if (matchArray !== null) {
  179. if (matchArray[2] === '秒')
  180. return Number(matchArray[1]);
  181. if (matchArray[2] === '分')
  182. return Number(matchArray[1]) * 60;
  183. if (matchArray[2] === '時間')
  184. return Number(matchArray[1]) * 3600;
  185. }
  186. }
  187. // PAST 仕様
  188. // 「同じ問題に1分以内に再提出することはできません」
  189. {
  190. const matchArray = /(\d+)(秒|分|時間).{1,5}再提出.{0,10}できません/.exec(statementText);
  191. if (matchArray !== null) {
  192. if (matchArray[2] === '秒')
  193. return Number(matchArray[1]);
  194. if (matchArray[2] === '分')
  195. return Number(matchArray[1]) * 60;
  196. if (matchArray[2] === '時間')
  197. return Number(matchArray[1]) * 3600;
  198. }
  199. }
  200. // Chokudai Contest 仕様
  201. // 「CEの提出を除いて5分に1回しか提出できません」
  202. // 「前の提出から30秒以上開けての提出をお願いします」
  203. // 「前の提出から5分以上開けての提出をお願いします」
  204. // Introduction to Heuristics Contest 仕様
  205. // 「提出の間隔は5分以上空ける必要があります」
  206. {
  207. const matchArray = /提出[^\d]{1,5}(\d+)(秒|分|時間)以上(?:空け|開け)/.exec(statementText);
  208. // console.log(matchArray);
  209. if (matchArray !== null) {
  210. if (matchArray[2] === '秒')
  211. return Number(matchArray[1]);
  212. if (matchArray[2] === '分')
  213. return Number(matchArray[1]) * 60;
  214. if (matchArray[2] === '時間')
  215. return Number(matchArray[1]) * 3600;
  216. }
  217. }
  218. {
  219. const matchArray = /(\d+)(秒|分|時間)に1回.{1,5}提出/.exec(statementText);
  220. if (matchArray !== null) {
  221. if (matchArray[2] === '秒')
  222. return Number(matchArray[1]);
  223. if (matchArray[2] === '分')
  224. return Number(matchArray[1]) * 60;
  225. if (matchArray[2] === '時間')
  226. return Number(matchArray[1]) * 3600;
  227. }
  228. }
  229. // ゲノコン2021 仕様
  230. // 「提出時間の間隔は,8/28 21:00までは10分,8/28 21:00以降は2時間となります」
  231. {
  232. const matchArray = /提出[^\d]{1,5}間隔[^\d]+?(?:(\d+)\/(\d+) (\d\d):(\d\d)(まで|以降)は(\d+)(秒|分|時間)[,、,\s]?)+/.exec(statementText);
  233. // console.log(matchArray);
  234. if (matchArray !== null) {
  235. const re = /(\d+)\/(\d+) (\d+):(\d\d)(まで|以降)は(\d+)(秒|分|時間)/g;
  236. let matchArrayInner;
  237. const currentTime = moment();
  238. while ((matchArrayInner = re.exec(matchArray[0]))) {
  239. console.log(matchArrayInner);
  240. const momentInput = {
  241. year: startTime.year(),
  242. month: Number(matchArrayInner[1]) - 1,
  243. days: Number(matchArrayInner[2]),
  244. hours: Number(matchArrayInner[3]),
  245. minutes: Number(matchArrayInner[4]),
  246. };
  247. const timeThreshold = moment(momentInput);
  248. if (matchArrayInner[5] === 'まで') {
  249. if (currentTime.isBefore(timeThreshold)) {
  250. if (matchArrayInner[7] === '秒')
  251. return Number(matchArrayInner[6]);
  252. if (matchArrayInner[7] === '分')
  253. return Number(matchArrayInner[6]) * 60;
  254. if (matchArrayInner[7] === '時間')
  255. return Number(matchArrayInner[6]) * 3600;
  256. }
  257. }
  258. else {
  259. if (currentTime.isAfter(timeThreshold)) {
  260. if (matchArrayInner[7] === '秒')
  261. return Number(matchArrayInner[6]);
  262. if (matchArrayInner[7] === '分')
  263. return Number(matchArrayInner[6]) * 60;
  264. if (matchArrayInner[7] === '時間')
  265. return Number(matchArrayInner[6]) * 3600;
  266. }
  267. }
  268. }
  269. }
  270. }
  271. {
  272. const matchArray = /提出[^\d]{1,5}間隔.+?(\d+)(秒|分|時間)/.exec(statementText);
  273. if (matchArray !== null) {
  274. if (matchArray[2] === '秒')
  275. return Number(matchArray[1]);
  276. if (matchArray[2] === '分')
  277. return Number(matchArray[1]) * 60;
  278. if (matchArray[2] === '時間')
  279. return Number(matchArray[1]) * 3600;
  280. }
  281. }
  282. return 5;
  283. };
  284.  
  285. void (async () => {
  286. // 終了後のコンテストに対しては処理しない?
  287. //if (moment() >= endTime) return;
  288. const [contestSlug, taskSlug] = extractContestAndProblemSlugs(document.location.href);
  289. if (contestSlug !== contestScreenName) {
  290. throw new Error('url が不正です');
  291. }
  292. const submitIntervalSecs = await getSubmitIntervalSecs(contestSlug);
  293. const recentSubmissions = await getRecentSubmissions(contestSlug, taskSlug);
  294. const lastSubmitTime = recentSubmissions.reduce((prev, [, statusLabel, submitTime]) => {
  295. if (statusLabel === 'CE')
  296. return prev;
  297. if (prev === null || submitTime > prev)
  298. return submitTime;
  299. return prev;
  300. }, null);
  301. new Timer(lastSubmitTime, submitIntervalSecs);
  302. })();