您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
AtCoder Problemsの難易度を表示します。
当前为
// ==UserScript== // @name AtCoder Difficulty Display // @namespace https://github.com/hotarunx // @homepage https://github.com/hotarunx/AtCoderDifficultyDisplay // @supportURL https://github.com/hotarunx/AtCoderDifficultyDisplay/issues // @version 1.0.4 // @description AtCoder Problemsの難易度を表示します。 // @description:en display a difficulty of AtCoder Problems. // @author hotarunx // @match https://atcoder.jp/contests/* // @exclude https://atcoder.jp/contests/ // @grant none // @connect https://kenkoooo.com/atcoder/resources/problem-models.json // @connect https://kenkoooo.com/atcoder/atcoder-api/results?user=* // @license MIT // // Copyright(c) 2020 hotarunx // This software is released under the MIT License, see LICENSE or https://github.com/hotarunx/AtCoderMyExtensions/blob/master/LICENSE. // // ==/UserScript== // 現在時間、コンテスト開始時間、コンテスト終了時間(UNIX時間 + 時差) const nowTime = Math.floor(Date.now() / 1000); const contestStartTime = Math.floor(Date.parse(startTime._i) / 1000); const contestEndTime = Math.floor(Date.parse(endTime._i) / 1000); (async function () { // URLから問題ID(ex: abc170_a)を取得 const path = location.pathname.split("/"); const problemId = path[path.length - 1]; const isABS = path[2] == "abs"; // 問題のコンテストが開催中ならば全ての処理をスキップする。 if (!isABS && !isContestOver(nowTime)) return; const diffURL = "https://kenkoooo.com/atcoder/resources/problem-models.json"; const diffKey = "atcoderDifficultyDisplayEstimatedDifficulties"; const submissionsURL = "https://kenkoooo.com/atcoder/atcoder-api/results?user=" + userScreenName; const submissionsKey = "atcoderDifficultyDisplayUserSubmissions"; const estimatedDifficulties = await fetchAPIData(diffURL, diffKey, 1 * 60 * 60); const userSubmissions = await fetchAPIData(submissionsURL, submissionsKey, 1 * 60 * 60); if (path[path.length - 2] == "tasks") { const problemStatus = getElementOfProblemStatus(); const problemTitle = document.getElementsByClassName("h2")[0]; changeProblemTitle(problemId, estimatedDifficulties, problemTitle, false); addDifficultyText(problemId, estimatedDifficulties, problemStatus); if (!isABS) addIsSolvedText(problemId, userSubmissions, problemStatus); } const as = document.getElementsByTagName("a"); for (const item of as) { const h = item.getAttribute("href"); if (typeof (h) != "string") continue; const hpath = h.split("/"); if (hpath[hpath.length - 2] == "tasks") { // itemが*/tasksページの表の一番左なら色付けしない if (item.parentElement.className === "text-center no-break") continue; const hProblemId = hpath[hpath.length - 1]; changeProblemTitle(hProblemId, estimatedDifficulties, item, true); } } })(); // コンテストが終了した? function isContestOver(time) { // 緩衝時間(20分) const bufferTime = 20 * 60; // 現在時間 > コンテスト終了時間 + 緩衝時間? if (time > contestEndTime + bufferTime) return true; return false; } // APIサーバからデータを取得してlocalStorageに保存して返す // 直前の取得から時間が経過していないならば保存したデータを返す async function fetchAPIData(url, keyData, timeInterval) { const keyLastFetch = keyData + "lastFetchedAt"; let jsondata = JSON.parse(localStorage.getItem(keyData)); const fetchTime = parseInt(localStorage.getItem(keyLastFetch)); // コンテストが終了していないならばデータ取得はしない if (!isContestOver(nowTime)) return jsondata; // 次のいずれかを満たすならば取得する // * データが保存されていない // * 直前の取得からtimeInterval経過した // * 直前の取得時にコンテストが終了していなかった let need2Fetch = false; if (isNaN(fetchTime)) need2Fetch = true; else if (nowTime >= timeInterval + fetchTime) need2Fetch = true; else if (!isContestOver(fetchTime)) need2Fetch = true; // データを取得する if (need2Fetch) { // alert(keyData + "is fetched."); jsondata = await (await (fetch(url))).json(); removeUnusedValues(jsondata); localStorage.setItem(keyData, JSON.stringify(jsondata)); localStorage.setItem(keyLastFetch, nowTime); } return jsondata; } function removeUnusedValues(jsondata) { const necessaryKeys = ["difficulty", "is_experimental", "epoch_second", "point", "result", "problem_id"]; for (const item in jsondata) { for (const key in jsondata[item]) { if (!necessaryKeys.includes(key)) { delete jsondata[item][key]; } } } } // Webページの問題ステータス(実行時間制限とメモリ制限が書かれた部分)のHTMLオブジェクトを取得 function getElementOfProblemStatus() { let element_status; const main_container = document.getElementById('main-container'); const elements_p = main_container.getElementsByTagName("p"); for (let i = 0; i < elements_p.length; i++) { const element = elements_p[i]; if (element.textContent.match("メモリ制限:") || element.textContent.match("Memory Limit:")) { element_status = element; break; } } return element_status; } // レーティングに対応する色のカラーコード function colorRating(rating) { if (rating < 400) return '#808080'; // gray else if (rating < 800) return '#804000'; // brown else if (rating < 1200) return '#008000'; // green else if (rating < 1600) return '#00C0C0'; // cyan else if (rating < 2000) return '#0000FF'; // blue else if (rating < 2400) return '#C0C000'; // yellow else if (rating < 2800) return '#FF8000'; // orange return '#FF0000'; // red } // レートを表す難易度円(◒)を生成 function generateDifficultyCircle(rating, isSmall = true) { const size = (isSmall ? 12 : 36); const borderWidth = (isSmall ? 1 : 3); if (rating < 3200) { // 色と円がどのぐらい満ちているかを計算 const color = colorRating(rating); const percentFull = (rating % 400) / 400 * 100; // ◒を生成 return "<span style = 'display: inline-block; border-radius: 50%; border-style: solid;border-width: " + borderWidth + "px; margin-right: 5px; vertical-align: initial; height: " + size + "px; width: " + size + "px;border-color: " + color + "; background: linear-gradient(to top, " + color + " 0%, " + color + " " + percentFull + "%, rgba(0, 0, 0, 0) " + percentFull + "%, rgba(0, 0, 0, 0) 100%); '></span>"; } // 金銀銅は例外処理 else if (rating < 3600) { return '<span style="display: inline-block; border-radius: 50%; border-style: solid;border-width: ' + borderWidth + 'px; margin-right: 5px; vertical-align: initial; height: ' + size + 'px; width: ' + size + 'px; border-color: rgb(150, 92, 44); background: linear-gradient(to right, rgb(150, 92, 44), rgb(255, 218, 189), rgb(150, 92, 44));"></span>'; } else if (rating < 4000) { return '<span style="display: inline-block; border-radius: 50%; border-style: solid;border-width: ' + borderWidth + 'px; margin-right: 5px; vertical-align: initial; height: ' + size + 'px; width: ' + size + 'px; border-color: rgb(128, 128, 128); background: linear-gradient(to right, rgb(128, 128, 128), white, rgb(128, 128, 128));"></span>'; } else { return '<span style="display: inline-block; border-radius: 50%; border-style: solid;border-width: ' + borderWidth + 'px; margin-right: 5px; vertical-align: initial; height: ' + size + 'px; width: ' + size + 'px; border-color: rgb(255, 215, 0); background: linear-gradient(to right, rgb(255, 215, 0), white, rgb(255, 215, 0));"></span>'; } } // 400未満のレーティングを補正 // 参考 https://qiita.com/anqooqie/items/92005e337a0d2569bdbd#性質4-初心者への慈悲 function correctLowerRating(rating) { if (rating >= 400) return rating; do { rating = 400 / Math.exp((400 - rating) / 400); } while (rating < 0); return rating; } function changeProblemTitle(problemId, estimatedDifficulties, problemTitle, isSmall = true) { const problem = estimatedDifficulties[problemId]; // 問題が存在しなければ終了 if (problem == null) return; if (problem.difficulty != null) { const difficulty = correctLowerRating(problem.difficulty).toFixed(); problemTitle.style.color = colorRating(difficulty); if (problem.is_experimental) problemTitle.insertAdjacentHTML("afterbegin", "🧪"); problemTitle.insertAdjacentHTML("beforebegin", generateDifficultyCircle(difficulty, isSmall)); } else { problemTitle.style.color = "#17a2b8"; const unavailableCircle = "(?)"; problemTitle.insertAdjacentHTML("afterbegin", unavailableCircle); } } // 推定難易度文字列を生成 function generateDifficultyText(difficulty, is_experimental) { // 推定難易度を補正して四捨五入 difficulty = correctLowerRating(difficulty); difficulty = difficulty.toFixed(); const textValue = "<span style='font-weight: bold; color: " + colorRating(difficulty) + ";'>" + difficulty + "</span>"; // textDiff = "<a href='https://kenkoooo.com/atcoder/#/table/" + userScreenName + "'>Difficulty</a>"; const textDiff = "Difficulty"; return textDiff + ": " + textValue + (is_experimental ? " (🧪)" : ""); } // 推定難易度表示を追加する function addDifficultyText(problemId, estimatedDifficulties, problemStatus) { const problem = estimatedDifficulties[problemId]; // 問題が存在しなければ終了 if (problem == null) return; if (problem.difficulty != null) { // 難易度を表示する文字列を生成 const text = generateDifficultyText(problem.difficulty, problem.is_experimental); problemStatus.insertAdjacentHTML('beforeend', " / " + text); } else problemStatus.insertAdjacentHTML('beforeend', " / Difficulty: <span style='font-weight: bold; color: #17a2b8;'>Unavailable</span>"); } // AC、コンテスト中AC、ペナルティ数、AC時間、最大得点、コンテスト中最大得点を計算 function searchSubmissionsResult(submissions) { const nonPenaltyJudge = ["AC", "CE", "IE", "WJ", "WR"]; submissions.sort((a, b) => a.epoch_second - b.epoch_second); let accepted = false; let acceptedDuringContest = false; let penalties = 0; let acceptedTime = contestEndTime; let maxPoint = 0; let maxPointDuringContest = 0; for (const item of submissions) { const duringContest = item.epoch_second <= contestEndTime; if (item.result == "AC") { accepted = true; if (duringContest) { acceptedDuringContest = true; acceptedTime = Math.min(item.epoch_second, acceptedTime); } } if (!accepted && duringContest && !nonPenaltyJudge.includes(item.result)) { penalties++; } maxPoint = Math.max(item.point, maxPoint); if (duringContest) maxPointDuringContest = Math.max(item.point, maxPointDuringContest); } return { accepted, acceptedDuringContest, penalties, acceptedTime, maxPoint, maxPointDuringContest }; } function epochTime2HHMM(time) { return Math.floor(time / 60).toFixed() + ":" + (time % 60).toFixed().padStart(2, '0'); } // ACしたか、AC時間、ペナルティ数を表示 function addIsSolvedText(problemId, userSubmissions, problemStatus) { const submissions = userSubmissions.filter(function (item, index) { if (item.problem_id == problemId) return true; }); const submitted = submissions.length > 0; const { accepted, acceptedDuringContest, penalties, acceptedTime, maxPoint, maxPointDuringContest } = searchSubmissionsResult(submissions); let text = "Is Solved: "; if (acceptedDuringContest) text += "<span style='font-weight: bold; color: white; background: green; border-radius: 20%;'>✓</span>"; else if (accepted) text += "<span style='font-weight: bold; color: green;'>✓</span>"; else if (submitted) text += "<span style='font-weight: bold; color: orange;'>✘</span>"; else text += "<span style='font-weight: bold; color: gray;'>✘</span>"; if (acceptedDuringContest) text += " <span style='font-size: x-small; color: grey;'>" + epochTime2HHMM(acceptedTime - contestStartTime) + "</span> "; if (penalties > 0) text += " <span style='font-size: x-small; color: red;'>(" + penalties + ")</span> "; if (maxPoint >= 10000) { text += " <span style='font-size: x-small; color: orange;'>" + maxPoint + "</span> "; if (maxPointDuringContest != maxPoint) text += " <span style='font-size: x-small; color: orange;'>(" + maxPointDuringContest + ")</span> "; } problemStatus.insertAdjacentHTML('beforeend', " / " + text); }