您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
AtCoder の提出待ち時間を表示します.
当前为
// ==UserScript== // @name atcoder-wait-time-display // @namespace iilj // @version 2021.8.2 // @description AtCoder の提出待ち時間を表示します. // @author iilj // @license MIT // @supportURL https://github.com/iilj/atcoder-wait-time-display/issues // @match https://atcoder.jp/contests/*/tasks/* // @grant GM_addStyle // ==/UserScript== const pad = (num, length = 2) => `00${num}`.slice(-length); const formatTime = (hours, minutes, seconds) => { return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`; }; const secondsToString = (diffWholeSecs) => { const diffSecs = diffWholeSecs % 60; const diffMinutes = Math.floor(diffWholeSecs / 60) % 60; const diffHours = Math.floor(diffWholeSecs / 3600) % 24; const diffDate = Math.floor(diffWholeSecs / (3600 * 24)); if (diffDate > 0) return `${diffDate}日`; return formatTime(diffHours, diffMinutes, diffSecs); }; 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}"; class Timer { constructor(lastSubmitTime, submitIntervalSecs) { this.lastSubmitTime = lastSubmitTime; this.submitIntervalSecs = submitIntervalSecs; GM_addStyle(css); this.element = document.createElement('div'); this.element.id = Timer.ELEMENT_ID; this.element.title = `間隔:${this.submitIntervalSecs} 秒`; document.body.appendChild(this.element); this.top = document.createElement('div'); this.top.classList.add('js-awtd-timer-top'); this.element.appendChild(this.top); this.bottom = document.createElement('div'); this.bottom.classList.add('js-awtd-timer-bottom'); this.element.appendChild(this.bottom); this.prevSeconds = -1; this.intervalID = window.setInterval(() => { this.updateTime(); }, 100); this.displayInterval = false; this.element.addEventListener('click', () => { this.displayInterval = !this.displayInterval; this.prevSeconds = -1; this.updateTime(); }); } updateTime() { const currentTime = moment(); const seconds = currentTime.seconds(); if (seconds === this.prevSeconds) return; if (this.displayInterval) { this.top.textContent = '提出間隔'; this.bottom.textContent = `${this.submitIntervalSecs} 秒`; } else { if (this.lastSubmitTime !== null) { // 経過時間を表示 const elapsedMilliseconds = currentTime.diff(this.lastSubmitTime); const elapsedSeconds = Math.floor(elapsedMilliseconds / 1000); this.top.textContent = `経過:${secondsToString(elapsedSeconds)}`; const waitTime = Math.max(0, this.submitIntervalSecs - elapsedSeconds); this.bottom.textContent = `待ち:${secondsToString(waitTime)}`; // if (waitTime > 0) this.bottom.style.color = '#cc0000'; // else this.bottom.style.color = 'inherit'; } else { this.top.textContent = 'この問題は'; this.bottom.textContent = '未提出です'; } } } } Timer.ELEMENT_ID = 'js-awtd-timer'; const extractContestAndProblemSlugs = (url) => { // https://atcoder.jp/contests/*/tasks/* const urlMatchArray = /^https?:\/\/atcoder\.jp\/contests\/([^/]+)\/tasks\/([^/]+)/.exec(url); if (urlMatchArray === null) { throw new Error('url が不正です'); } return [urlMatchArray[1], urlMatchArray[2]]; }; const getRecentSubmissions = async (contestSlug, taskSlug) => { const res = await fetch(`https://atcoder.jp/contests/${contestSlug}/submissions/me?f.Task=${taskSlug}`); const text = await res.text(); const dom = new DOMParser().parseFromString(text, 'text/html'); // console.log(dom); // 2021-05-29 16:15:34+0900 const rows = dom.querySelectorAll('#main-container div.panel.panel-default.panel-submission > div.table-responsive > table > tbody > tr'); const ret = []; rows.forEach((row) => { var _a; const problem = row.querySelector(`a[href^="/contests/${contestSlug}/tasks/${taskSlug}"]`); if (problem === null) { throw new Error('テーブルに提出先不明の行があります'); } const time = row.querySelector('time.fixtime-second'); if (time === null) { throw new Error('テーブルに提出時刻不明の行があります'); } const [contestSlugTmp, taskSlugTmp] = extractContestAndProblemSlugs(problem.href); if (contestSlugTmp !== contestSlug || taskSlugTmp !== taskSlug) { throw new Error('異なる問題への提出記録が紛れています'); } const submission = row.querySelector(`a[href^="/contests/${contestSlug}/submissions/"]`); if (submission === null) { throw new Error('テーブルに提出 ID 不明の行があります'); } const statusLabel = row.querySelector('span.label'); if (statusLabel === null) { throw new Error('提出ステータス不明の行があります'); } const label = (_a = statusLabel.textContent) === null || _a === void 0 ? void 0 : _a.trim(); if (label === undefined) { throw new Error('提出ステータスが空の行があります'); } const submitTime = moment(time.innerText); ret.push([submission.href, label, submitTime]); }); return ret; }; const getSubmitIntervalSecs = async (contestSlug) => { var _a; const res = await fetch(`https://atcoder.jp/contests/${contestSlug}?lang=ja`); const text = await res.text(); const dom = new DOMParser().parseFromString(text, 'text/html'); // 例外的な処理 if (contestSlug === 'wn2017_1') { return 3600; } else if (contestSlug === 'caddi2019') { return 300; } // AHC/HTTF/日本橋ハーフマラソン/Future 仕様の文字列を検索 const candidates = dom.getElementsByTagName('strong'); for (let i = 0; i < candidates.length; ++i) { const content = (_a = candidates[i].textContent) === null || _a === void 0 ? void 0 : _a.trim(); if (content === undefined) continue; // 5分以上の間隔 const matchArray = /^(\d+)(秒|分|時間)以上の間隔/.exec(content); if (matchArray === null) continue; if (matchArray[2] === '秒') return Number(matchArray[1]); if (matchArray[2] === '分') return Number(matchArray[1]) * 60; if (matchArray[2] === '時間') return Number(matchArray[1]) * 3600; } const statement = dom.getElementById('contest-statement'); if (statement === null) { throw new Error('コンテスト説明文が見つかりませんでした'); } const statementText = statement.textContent; if (statementText === null) { throw new Error('コンテスト説明文が空です'); } // Asprova 仕様 // 「提出間隔:プログラム提出後10分間は再提出できません。」 // 「提出後1時間は再提出できません」 // Hitachi Hokudai 仕様 // 「提出直後の1時間は再提出することができません」 // 「提出直後の1時間は、再提出することができません」 // ヤマトコン仕様 // 「提出後30分は再提出することはできません」 { const matchArray = /提出[^\d]{1,5}(\d+)(秒|分|時間).{1,5}再提出/.exec(statementText); if (matchArray !== null) { if (matchArray[2] === '秒') return Number(matchArray[1]); if (matchArray[2] === '分') return Number(matchArray[1]) * 60; if (matchArray[2] === '時間') return Number(matchArray[1]) * 3600; } } // PAST 仕様 // 「同じ問題に1分以内に再提出することはできません」 { const matchArray = /(\d+)(秒|分|時間).{1,5}再提出.{0,10}できません/.exec(statementText); if (matchArray !== null) { if (matchArray[2] === '秒') return Number(matchArray[1]); if (matchArray[2] === '分') return Number(matchArray[1]) * 60; if (matchArray[2] === '時間') return Number(matchArray[1]) * 3600; } } // Chokudai Contest 仕様 // 「CEの提出を除いて5分に1回しか提出できません」 // 「前の提出から30秒以上開けての提出をお願いします」 // 「前の提出から5分以上開けての提出をお願いします」 // Introduction to Heuristics Contest 仕様 // 「提出の間隔は5分以上空ける必要があります」 { const matchArray = /提出[^\d]{1,5}(\d+)(秒|分|時間)以上(?:空け|開け)/.exec(statementText); // console.log(matchArray); if (matchArray !== null) { if (matchArray[2] === '秒') return Number(matchArray[1]); if (matchArray[2] === '分') return Number(matchArray[1]) * 60; if (matchArray[2] === '時間') return Number(matchArray[1]) * 3600; } } { const matchArray = /(\d+)(秒|分|時間)に1回.{1,5}提出/.exec(statementText); if (matchArray !== null) { if (matchArray[2] === '秒') return Number(matchArray[1]); if (matchArray[2] === '分') return Number(matchArray[1]) * 60; if (matchArray[2] === '時間') return Number(matchArray[1]) * 3600; } } // ゲノコン2021 仕様 // 「提出時間の間隔は,8/28 21:00までは10分,8/28 21:00以降は2時間となります」 { const matchArray = /提出[^\d]{1,5}間隔[^\d]+?(?:(\d+)\/(\d+) (\d\d):(\d\d)(まで|以降)は(\d+)(秒|分|時間)[,、,\s]?)+/.exec(statementText); // console.log(matchArray); if (matchArray !== null) { const re = /(\d+)\/(\d+) (\d+):(\d\d)(まで|以降)は(\d+)(秒|分|時間)/g; let matchArrayInner; const currentTime = moment(); while ((matchArrayInner = re.exec(matchArray[0]))) { console.log(matchArrayInner); const momentInput = { year: startTime.year(), month: Number(matchArrayInner[1]) - 1, days: Number(matchArrayInner[2]), hours: Number(matchArrayInner[3]), minutes: Number(matchArrayInner[4]), }; const timeThreshold = moment(momentInput); if (matchArrayInner[5] === 'まで') { if (currentTime.isBefore(timeThreshold)) { if (matchArrayInner[7] === '秒') return Number(matchArrayInner[6]); if (matchArrayInner[7] === '分') return Number(matchArrayInner[6]) * 60; if (matchArrayInner[7] === '時間') return Number(matchArrayInner[6]) * 3600; } } else { if (currentTime.isAfter(timeThreshold)) { if (matchArrayInner[7] === '秒') return Number(matchArrayInner[6]); if (matchArrayInner[7] === '分') return Number(matchArrayInner[6]) * 60; if (matchArrayInner[7] === '時間') return Number(matchArrayInner[6]) * 3600; } } } } } { const matchArray = /提出[^\d]{1,5}間隔.+?(\d+)(秒|分|時間)/.exec(statementText); if (matchArray !== null) { if (matchArray[2] === '秒') return Number(matchArray[1]); if (matchArray[2] === '分') return Number(matchArray[1]) * 60; if (matchArray[2] === '時間') return Number(matchArray[1]) * 3600; } } return 5; }; void (async () => { // 終了後のコンテストに対しては処理しない? //if (moment() >= endTime) return; const [contestSlug, taskSlug] = extractContestAndProblemSlugs(document.location.href); if (contestSlug !== contestScreenName) { throw new Error('url が不正です'); } const submitIntervalSecs = await getSubmitIntervalSecs(contestSlug); const recentSubmissions = await getRecentSubmissions(contestSlug, taskSlug); const lastSubmitTime = recentSubmissions.reduce((prev, [, statusLabel, submitTime]) => { if (statusLabel === 'CE') return prev; if (prev === null || submitTime > prev) return submitTime; return prev; }, null); new Timer(lastSubmitTime, submitIntervalSecs); })();