// ==UserScript==
// @name ac-predictor-cn
// @namespace https://github.com/GoodCoder666/ac-predictor-extension-CN
// @icon https://atcoder.jp/favicon.ico
// @version 1.2.16
// @description AtCoder 预测工具 (由GoodCoder666翻译为简体中文)
// @author GoodCoder666
// @license MIT
// @supportURL https://github.com/GoodCoder666/ac-predictor-extension-CN/issues
// @match https://atcoder.jp/*
// @exclude https://atcoder.jp/*/json
// ==/UserScript==
function __awaiter(thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
}
var dom = "<div id=\"predictor-alert\" class=\"row\"><h5 class=\"sidemenu-txt\">加载中…</h5></div>\n<div id=\"predictor-data\" class=\"row\">\n <div class=\"input-group col-xs-12\">\n <span class=\"input-group-addon\">名次\n <style>\n .predictor-tooltip-icon:hover+.tooltip{\n opacity: .9;\n filter: alpha(opacity=90);\n }\n </style>\n <span class=\"predictor-tooltip-icon glyphicon glyphicon-question-sign\"></span>\n <div class=\"tooltip fade bottom\" style=\"pointer-events:none\">\n <div class=\"tooltip-arrow\" style=\"left: 18%;\"></div>\n <div class=\"tooltip-inner\">Rated 范围内的排名,多人同名次时加上人数。</div>\n </div>\n </span>\n <input class=\"form-control\" id=\"predictor-input-rank\">\n <span class=\"input-group-addon\">位</span>\n </div>\n \n <div class=\"input-group col-xs-12\">\n <span class=\"input-group-addon\">Performance</span>\n <input class=\"form-control\" id=\"predictor-input-perf\">\n </div>\n\n <div class=\"input-group col-xs-12\">\n <span class=\"input-group-addon\">预计 Rating</span>\n <input class=\"form-control\" id=\"predictor-input-rate\">\n </div>\n</div>\n<div class=\"row\">\n <div class=\"btn-group\">\n <button class=\"btn btn-default\" id=\"predictor-current\">现在的名次</button>\n <button type=\"button\" class=\"btn btn-primary\" id=\"predictor-reload\" data-loading-text=\"更新中…\">更新</button>\n <!--<button class=\"btn btn-default\" id=\"predictor-solved\" disabled>当前问题AC后</button>-->\n </div>\n</div>";
class Result {
constructor(isRated, isSubmitted, userScreenName, place, ratedRank, oldRating, newRating, competitions, performance, innerPerformance) {
this.IsRated = isRated;
this.IsSubmitted = isSubmitted;
this.UserScreenName = userScreenName;
this.Place = place;
this.RatedRank = ratedRank;
this.OldRating = oldRating;
this.NewRating = newRating;
this.Competitions = competitions;
this.Performance = performance;
this.InnerPerformance = innerPerformance;
}
}
function analyzeStandingsData(fixed, standingsData, aPerfs, defaultAPerf, ratedLimit, isHeuristic) {
function analyze(isUserRated) {
const contestantAPerf = [];
const templateResults = {};
let currentRatedRank = 1;
let lastRank = 0;
const tiedUsers = [];
let ratedInTiedUsers = 0;
function applyTiedUsers() {
tiedUsers.forEach((data) => {
if (isUserRated(data)) {
contestantAPerf.push(aPerfs[data.UserScreenName] || defaultAPerf);
ratedInTiedUsers++;
}
});
const ratedRank = currentRatedRank + Math.max(0, ratedInTiedUsers - 1) / 2;
tiedUsers.forEach((data) => {
templateResults[data.UserScreenName] = new Result(!isHeuristic /* FIXME: Temporary disabled for the AHC rating system */ && isUserRated(data), !isHeuristic || data.TotalResult.Count !== 0, data.UserScreenName, data.Rank, ratedRank, fixed ? data.OldRating : data.Rating, null, data.Competitions, null, null);
});
currentRatedRank += ratedInTiedUsers;
tiedUsers.length = 0;
ratedInTiedUsers = 0;
}
standingsData.forEach((data) => {
if (lastRank !== data.Rank)
applyTiedUsers();
lastRank = data.Rank;
tiedUsers.push(data);
});
applyTiedUsers();
return {
contestantAPerf: contestantAPerf,
templateResults: templateResults,
};
}
let analyzedData = analyze((data) => data.IsRated && (!isHeuristic || data.TotalResult.Count !== 0));
let isRated = true;
if (analyzedData.contestantAPerf.length === 0) {
analyzedData = analyze((data) => data.OldRating < ratedLimit && (!isHeuristic || data.TotalResult.Count !== 0));
isRated = false;
}
const res = analyzedData;
res.isRated = isRated;
return res;
}
class Contest {
constructor(contestScreenName, contestInformation, standings, aPerfs) {
this.ratedLimit = contestInformation.RatedRange[1] + 1;
this.perfLimit = this.ratedLimit + 400;
this.standings = standings;
this.aPerfs = aPerfs;
this.rankMemo = {};
const analyzedData = analyzeStandingsData(standings.Fixed, standings.StandingsData, aPerfs, contestInformation.isHeuristic ? 1000 : ({ 2000: 800, 2800: 1000, Infinity: 1200 }[this.ratedLimit] || 1200), this.ratedLimit, contestInformation.isHeuristic);
this.contestantAPerf = analyzedData.contestantAPerf;
this.templateResults = analyzedData.templateResults;
this.IsRated = analyzedData.isRated;
}
getRatedRank(X) {
if (this.rankMemo[X])
return this.rankMemo[X];
return (this.rankMemo[X] = this.contestantAPerf.reduce((val, APerf) => val + 1.0 / (1.0 + Math.pow(6.0, (X - APerf) / 400.0)), 0.5));
}
getPerf(ratedRank) {
return Math.min(this.getInnerPerf(ratedRank), this.perfLimit);
}
getInnerPerf(ratedRank) {
let upper = 6144;
let lower = -2048;
while (upper - lower > 0.5) {
const mid = (upper + lower) / 2;
if (ratedRank > this.getRatedRank(mid))
upper = mid;
else
lower = mid;
}
return Math.round((upper + lower) / 2);
}
}
class Results {
}
//Copyright © 2017 koba-e964.
//from : https://github.com/koba-e964/atcoder-rating-estimator
const finf = bigf(400);
function bigf(n) {
let pow1 = 1;
let pow2 = 1;
let numerator = 0;
let denominator = 0;
for (let i = 0; i < n; ++i) {
pow1 *= 0.81;
pow2 *= 0.9;
numerator += pow1;
denominator += pow2;
}
return Math.sqrt(numerator) / denominator;
}
function f(n) {
return ((bigf(n) - finf) / (bigf(1) - finf)) * 1200.0;
}
/**
* calculate unpositivized rating from performance history
* @param {Number[]} [history] performance history with ascending order
* @returns {Number} unpositivized rating
*/
function calcRatingFromHistory(history) {
const n = history.length;
let pow = 1;
let numerator = 0.0;
let denominator = 0.0;
for (let i = n - 1; i >= 0; i--) {
pow *= 0.9;
numerator += Math.pow(2, history[i] / 800.0) * pow;
denominator += pow;
}
return Math.log2(numerator / denominator) * 800.0 - f(n);
}
/**
* calculate unpositivized rating from last state
* @param {Number} [last] last unpositivized rating
* @param {Number} [perf] performance
* @param {Number} [ratedMatches] count of participated rated contest
* @returns {number} estimated unpositivized rating
*/
function calcRatingFromLast(last, perf, ratedMatches) {
if (ratedMatches === 0)
return perf - 1200;
last += f(ratedMatches);
const weight = 9 - 9 * Math.pow(0.9, ratedMatches);
const numerator = weight * Math.pow(2, (last / 800.0)) + Math.pow(2, (perf / 800.0));
const denominator = 1 + weight;
return Math.log2(numerator / denominator) * 800.0 - f(ratedMatches + 1);
}
/**
* (-inf, inf) -> (0, inf)
* @param {Number} [rating] unpositivized rating
* @returns {number} positivized rating
*/
function positivizeRating(rating) {
if (rating >= 400.0) {
return rating;
}
return 400.0 * Math.exp((rating - 400.0) / 400.0);
}
/**
* (0, inf) -> (-inf, inf)
* @param {Number} [rating] positivized rating
* @returns {number} unpositivized rating
*/
function unpositivizeRating(rating) {
if (rating >= 400.0) {
return rating;
}
return 400.0 + 400.0 * Math.log(rating / 400.0);
}
/**
* calculate the performance required to reach a target rate
* @param {Number} [targetRating] targeted unpositivized rating
* @param {Number[]} [history] performance history with ascending order
* @returns {number} performance
*/
function calcRequiredPerformance(targetRating, history) {
let valid = 10000.0;
let invalid = -10000.0;
for (let i = 0; i < 100; ++i) {
const mid = (invalid + valid) / 2;
const rating = Math.round(calcRatingFromHistory(history.concat([mid])));
if (targetRating <= rating)
valid = mid;
else
invalid = mid;
}
return valid;
}
const colorNames = ["unrated", "gray", "brown", "green", "cyan", "blue", "yellow", "orange", "red"];
function getColor(rating) {
const colorIndex = rating > 0 ? Math.min(Math.floor(rating / 400) + 1, 8) : 0;
return colorNames[colorIndex];
}
class OnDemandResults extends Results {
constructor(contest, templateResults) {
super();
this.Contest = contest;
this.TemplateResults = templateResults;
}
getUserResult(userScreenName) {
if (!Object.prototype.hasOwnProperty.call(this.TemplateResults, userScreenName))
return null;
const baseResults = this.TemplateResults[userScreenName];
if (!baseResults)
return null;
if (!baseResults.Performance) {
baseResults.InnerPerformance = this.Contest.getInnerPerf(baseResults.RatedRank);
baseResults.Performance = Math.min(baseResults.InnerPerformance, this.Contest.perfLimit);
baseResults.NewRating = Math.round(positivizeRating(calcRatingFromLast(unpositivizeRating(baseResults.OldRating), baseResults.Performance, baseResults.Competitions)));
}
return baseResults;
}
}
class FixedResults extends Results {
constructor(results) {
super();
this.resultsDic = {};
results.forEach((result) => {
this.resultsDic[result.UserScreenName] = result;
});
}
getUserResult(userScreenName) {
return Object.prototype.hasOwnProperty.call(this.resultsDic, userScreenName)
? this.resultsDic[userScreenName]
: null;
}
}
class PredictorModel {
constructor(model) {
this.enabled = model.enabled;
this.contest = model.contest;
this.history = model.history;
this.updateInformation(model.information);
this.updateData(model.rankValue, model.perfValue, model.rateValue);
}
setEnable(state) {
this.enabled = state;
}
updateInformation(information) {
this.information = information;
}
updateData(rankValue, perfValue, rateValue) {
this.rankValue = rankValue;
this.perfValue = perfValue;
this.rateValue = rateValue;
}
}
class CalcFromRankModel extends PredictorModel {
updateData(rankValue, perfValue, rateValue) {
perfValue = this.contest.getPerf(rankValue);
rateValue = positivizeRating(calcRatingFromHistory(this.history.concat([perfValue])));
super.updateData(rankValue, perfValue, rateValue);
}
}
class CalcFromPerfModel extends PredictorModel {
updateData(rankValue, perfValue, rateValue) {
rankValue = this.contest.getRatedRank(perfValue);
rateValue = positivizeRating(calcRatingFromHistory(this.history.concat([perfValue])));
super.updateData(rankValue, perfValue, rateValue);
}
}
class CalcFromRateModel extends PredictorModel {
updateData(rankValue, perfValue, rateValue) {
perfValue = calcRequiredPerformance(unpositivizeRating(rateValue), this.history);
rankValue = this.contest.getRatedRank(perfValue);
super.updateData(rankValue, perfValue, rateValue);
}
}
function roundValue(value, numDigits) {
return Math.round(value * Math.pow(10, numDigits)) / Math.pow(10, numDigits);
}
class ContestInformation {
constructor(canParticipateRange, ratedRange, penalty, isHeuristic) {
this.CanParticipateRange = canParticipateRange;
this.RatedRange = ratedRange;
this.Penalty = penalty;
this.isHeuristic = isHeuristic;
}
}
function parseRangeString(s) {
s = s.trim();
if (s === "-")
return [0, -1];
if (s === "All")
return [0, Infinity];
if (!/[-~]/.test(s))
return [0, -1];
const res = s.split(/[-~]/).map((x) => parseInt(x.trim()));
if (isNaN(res[0]))
res[0] = 0;
if (isNaN(res[1]))
res[1] = Infinity;
return res;
}
function parseDurationString(s) {
if (s === "None" || s === "なし")
return 0;
if (!/(\d+[^\d]+)/.test(s))
return NaN;
const durationDic = {
日: 24 * 60 * 60 * 1000,
day: 24 * 60 * 60 * 1000,
days: 24 * 60 * 60 * 1000,
時間: 60 * 60 * 1000,
hour: 60 * 60 * 1000,
hours: 60 * 60 * 1000,
分: 60 * 1000,
minute: 60 * 1000,
minutes: 60 * 1000,
秒: 1000,
second: 1000,
seconds: 1000,
};
let res = 0;
s.match(/(\d+[^\d]+)/g).forEach((x) => {
var _a;
const trimmed = x.trim();
const num = parseInt(/\d+/.exec(trimmed)[0]);
const unit = /[^\d]+/.exec(trimmed)[0];
const duration = (_a = durationDic[unit]) !== null && _a !== void 0 ? _a : 0;
res += num * duration;
});
return res;
}
function fetchJsonDataAsync(url) {
return __awaiter(this, void 0, void 0, function* () {
const response = yield fetch(url);
if (response.ok)
return (yield response.json());
throw new Error(`request to ${url} returns ${response.status}`);
});
}
function fetchTextDataAsync(url) {
return __awaiter(this, void 0, void 0, function* () {
const response = yield fetch(url);
if (response.ok)
return response.text();
throw new Error(`request to ${url} returns ${response.status}`);
});
}
function getStandingsDataAsync(contestScreenName) {
return __awaiter(this, void 0, void 0, function* () {
return yield fetchJsonDataAsync(`https://atcoder.jp/contests/${contestScreenName}/standings/json`);
});
}
function getAPerfsDataAsync(contestScreenName) {
return __awaiter(this, void 0, void 0, function* () {
let url = `https://data.ac-predictor.com/aperfs/${contestScreenName}.json`;
// if (contestScreenName === "arc119") url = `https://raw.githubusercontent.com/key-moon/ac-predictor-data/master/aperfs/${contestScreenName}.json`;
return yield fetchJsonDataAsync(url);
});
}
function getResultsDataAsync(contestScreenName) {
return __awaiter(this, void 0, void 0, function* () {
return yield fetchJsonDataAsync(`https://atcoder.jp/contests/${contestScreenName}/results/json`);
});
}
function getHistoryDataAsync(userScreenName) {
return __awaiter(this, void 0, void 0, function* () {
return yield fetchJsonDataAsync(`https://atcoder.jp/users/${userScreenName}/history/json`);
});
}
function getContestInformationAsync(contestScreenName) {
return __awaiter(this, void 0, void 0, function* () {
const html = yield fetchTextDataAsync(`https://atcoder.jp/contests/${contestScreenName}`);
const topPageDom = new DOMParser().parseFromString(html, "text/html");
const dataParagraph = topPageDom.getElementsByClassName("small")[0];
const data = Array.from(dataParagraph.children).map((x) => x.innerHTML.split(":")[1].trim());
const isAHC = /^ahc\d{3}$/.test(contestScreenName) || html.includes("This contest is rated for AHC rating");
return new ContestInformation(parseRangeString(data[0]), parseRangeString(data[1]), parseDurationString(data[2]), isAHC);
});
}
/**
* ユーザーのPerformance履歴を時間昇順で取得
*/
function getPerformanceHistories(history) {
const onlyRated = history.filter((x) => x.IsRated);
onlyRated.sort((a, b) => {
return new Date(a.EndTime).getTime() - new Date(b.EndTime).getTime();
});
return onlyRated.map((x) => x.Performance);
}
/**
* サイドメニューに追加される要素のクラス
*/
class SideMenuElement {
shouldDisplayed(url) {
return this.match.test(url);
}
/**
* 要素のHTMLを取得
*/
GetHTML() {
return `<div class="menu-wrapper">
<div class="menu-header">
<h4 class="sidemenu-txt">${this.title}<span class="glyphicon glyphicon-menu-up" style="float: right"></span></h4>
</div>
<div class="menu-box"><div class="menu-content" id="${this.id}">${this.document}</div></div>
</div>`;
}
}
function getGlobalVals() {
const script = [...document.querySelectorAll("head script:not([src])")].map((x) => x.innerHTML).join("\n");
const res = {};
script.match(/var [^ ]+ = .+$/gm).forEach((statement) => {
const match = /var ([^ ]+) = (.+)$/m.exec(statement);
function safeEval(val) {
function trim(val) {
while (val.endsWith(";") || val.endsWith(" "))
val = val.substr(0, val.length - 1);
while (val.startsWith(" "))
val = val.substr(1, val.length - 1);
return val;
}
function isStringToken(val) {
return 1 < val.length && val.startsWith('"') && val.endsWith('"');
}
function evalStringToken(val) {
if (!isStringToken(val))
throw new Error();
return val.substr(1, val.length - 2); // TODO: parse escape
}
val = trim(val);
if (isStringToken(val))
return evalStringToken(val);
if (val.startsWith("moment("))
return new Date(evalStringToken(trim(val.substr(7, val.length - (7 + 1)))));
return val;
}
res[match[1]] = safeEval(match[2]);
});
return res;
}
const globalVals = getGlobalVals();
const userScreenName = globalVals["userScreenName"];
const contestScreenName = globalVals["contestScreenName"];
const startTime = globalVals["startTime"];
class AllRowUpdater {
update(table) {
Array.from(table.rows).forEach((row) => this.rowModifier.modifyRow(row));
}
}
class StandingsRowModifier {
isHeader(row) {
return row.parentElement.tagName.toLowerCase() == "thead";
}
isFooter(row) {
return row.firstElementChild.hasAttribute("colspan") && row.firstElementChild.getAttribute("colspan") == "3";
}
modifyRow(row) {
if (this.isHeader(row))
this.modifyHeader(row);
else if (this.isFooter(row))
this.modifyFooter(row);
else
this.modifyContent(row);
}
}
class PerfAndRateChangeAppender extends StandingsRowModifier {
modifyContent(content) {
var _a;
this.removeOldElem(content);
if (content.firstElementChild.textContent === "-") {
const longCell = content.getElementsByClassName("standings-result")[0];
longCell.setAttribute("colspan", (parseInt(longCell.getAttribute("colspan")) + 2).toString());
return;
}
const userScreenName = content.querySelector(".standings-username .username span").textContent;
const result = (_a = this.results) === null || _a === void 0 ? void 0 : _a.getUserResult(userScreenName);
const perfElem = (result === null || result === void 0 ? void 0 : result.IsSubmitted) ? this.getRatingSpan(Math.round(positivizeRating(result.Performance)))
: "-";
const ratingElem = result
? (result === null || result === void 0 ? void 0 : result.IsRated) && (this === null || this === void 0 ? void 0 : this.isRated)
? this.getChangedRatingElem(result.OldRating, result.NewRating)
: this.getUnratedElem(result.OldRating)
: "-";
content.insertAdjacentHTML("beforeend", `<td class="standings-result standings-perf">${perfElem}</td>`);
content.insertAdjacentHTML("beforeend", `<td class="standings-result standings-rate">${ratingElem}</td>`);
}
getChangedRatingElem(oldRate, newRate) {
const oldRateSpan = this.getRatingSpan(oldRate);
const newRateSpan = this.getRatingSpan(newRate);
const diff = this.toSignedString(newRate - oldRate);
return `<span class="bold">${oldRateSpan}</span> → <span class="bold">${newRateSpan}</span> <span class="grey">(${diff})</span>`;
}
toSignedString(n) {
return `${n >= 0 ? "+" : ""}${n}`;
}
getUnratedElem(rate) {
return `<span class="bold">${this.getRatingSpan(rate)}</span> <span class="grey">(unrated)</span>`;
}
getRatingSpan(rate) {
return `<span class="user-${getColor(rate)}">${rate}</span>`;
}
modifyFooter(footer) {
this.removeOldElem(footer);
footer.insertAdjacentHTML("beforeend", '<td class="standings-result standings-perf standings-rate" colspan="2">-</td>');
}
modifyHeader(header) {
this.removeOldElem(header);
header.insertAdjacentHTML("beforeend", '<th class="standings-result-th standings-perf" style="width:84px;min-width:84px;">Performance</th><th class="standings-result-th standings-rate" style="width:168px;min-width:168px;">Rating 变化</th>');
}
removeOldElem(row) {
row.querySelectorAll(".standings-perf, .standings-rate").forEach((elem) => elem.remove());
}
}
class PredictorElement extends SideMenuElement {
constructor() {
super(...arguments);
this.id = "predictor";
this.title = "Predictor";
this.match = /atcoder.jp\/contests\/.+/;
this.document = dom;
this.historyData = [];
this.contestOnUpdated = [];
this.resultsOnUpdated = [];
}
set contest(val) {
this._contest = val;
this.contestOnUpdated.forEach((func) => func(val));
}
get contest() {
return this._contest;
}
set results(val) {
this._results = val;
this.resultsOnUpdated.forEach((func) => func(val));
}
get results() {
return this._results;
}
isStandingsPage() {
return /standings([^/]*)?$/.test(document.location.href);
}
afterAppend() {
const loaded = () => !!document.getElementById("standings-tbody");
if (!this.isStandingsPage() || loaded()) {
void this.initialize();
return;
}
const loadingElem = document.getElementById("vue-standings").getElementsByClassName("loading-show")[0];
new MutationObserver(() => {
if (loaded())
void this.initialize();
}).observe(loadingElem, { attributes: true });
}
initialize() {
var _a;
return __awaiter(this, void 0, void 0, function* () {
const firstContestDate = new Date(2016, 6, 16, 21);
const predictorElements = [
"predictor-input-rank",
"predictor-input-perf",
"predictor-input-rate",
"predictor-current",
"predictor-reload",
];
const isStandingsPage = this.isStandingsPage();
const contestInformation = yield getContestInformationAsync(contestScreenName);
const rowUpdater = new PerfAndRateChangeAppender();
this.resultsOnUpdated.push((val) => {
rowUpdater.results = val;
});
this.contestOnUpdated.push((val) => {
rowUpdater.isRated = val.IsRated;
});
const tableUpdater = new AllRowUpdater();
tableUpdater.rowModifier = rowUpdater;
const tableElement = (_a = document.getElementById("standings-tbody")) === null || _a === void 0 ? void 0 : _a.parentElement;
let model = new PredictorModel({
rankValue: 0,
perfValue: 0,
rateValue: 0,
enabled: false,
history: this.historyData,
});
const updateData = (aperfs, standings) => __awaiter(this, void 0, void 0, function* () {
this.contest = new Contest(contestScreenName, contestInformation, standings, aperfs);
model.contest = this.contest;
if (this.contest.standings.Fixed && this.contest.IsRated) {
const rawResult = yield getResultsDataAsync(contestScreenName);
rawResult.sort((a, b) => (a.Place !== b.Place ? a.Place - b.Place : b.OldRating - a.OldRating));
const sortedStandingsData = Array.from(this.contest.standings.StandingsData);
if (contestInformation.isHeuristic) sortedStandingsData.filter((x) => x.TotalResult.Count !== 0);
sortedStandingsData.sort((a, b) => {
if (a.TotalResult.Count === 0 && b.TotalResult.Count === 0)
return 0;
if (a.TotalResult.Count === 0)
return 1;
if (b.TotalResult.Count === 0)
return -1;
if (a.Rank !== b.Rank)
return a.Rank - b.Rank;
if (b.OldRating !== a.OldRating)
return b.OldRating - a.OldRating;
if (a.UserIsDeleted)
return -1;
if (b.UserIsDeleted)
return 1;
return 0;
});
let lastPerformance = this.contest.perfLimit;
let deletedCount = 0;
this.results = new FixedResults(sortedStandingsData.map((data, index) => {
let result = rawResult[index - deletedCount];
if (!result || data.OldRating !== result.OldRating) {
deletedCount++;
result = null;
}
return new Result(result ? result.IsRated : false, !contestInformation.isHeuristic || data.TotalResult.Count !== 0, data.UserScreenName, data.Rank, -1, data.OldRating, result ? result.NewRating : 0, 0, result && result.IsRated ? (lastPerformance = result.Performance) : lastPerformance, result ? result.InnerPerformance : 0);
}));
}
else {
this.results = new OnDemandResults(this.contest, this.contest.templateResults);
}
});
if (!shouldEnabledPredictor().verdict) {
model.updateInformation(shouldEnabledPredictor().message);
updateView();
return;
}
try {
let aPerfs;
let standings;
try {
standings = yield getStandingsDataAsync(contestScreenName);
}
catch (e) {
throw new Error("Standings读取失败。");
}
try {
aPerfs = yield getAPerfsDataAsync(contestScreenName);
}
catch (e) {
throw new Error("APerf获取失败。");
}
yield updateData(aPerfs, standings);
model.setEnable(true);
model.updateInformation(`最后更新时间: ${new Date().toTimeString().split(" ")[0]}`);
if (isStandingsPage) {
new MutationObserver(() => {
tableUpdater.update(tableElement);
}).observe(tableElement.tBodies[0], {
childList: true,
});
const refreshElem = document.getElementById("refresh");
if (refreshElem)
new MutationObserver((mutationRecord) => {
const disabled = mutationRecord[0].target.classList.contains("disabled");
if (disabled) {
void (() => __awaiter(this, void 0, void 0, function* () {
yield updateStandingsFromAPI();
updateView();
}))();
}
}).observe(refreshElem, {
attributes: true,
attributeFilter: ["class"],
});
}
}
catch (e) {
model.updateInformation(e.message);
model.setEnable(false);
}
updateView();
{
const reloadButton = document.getElementById("predictor-reload");
reloadButton.addEventListener("click", () => {
void (() => __awaiter(this, void 0, void 0, function* () {
model.updateInformation("");
reloadButton.disabled = true;
updateView();
yield updateStandingsFromAPI();
reloadButton.disabled = false;
updateView();
}))();
});
document.getElementById("predictor-current").addEventListener("click", () => {
const myResult = this.contest.templateResults[userScreenName];
if (!myResult)
return;
model = new CalcFromRankModel(model);
model.updateData(myResult.RatedRank, model.perfValue, model.rateValue);
updateView();
});
document.getElementById("predictor-input-rank").addEventListener("keyup", () => {
const inputString = document.getElementById("predictor-input-rank").value;
const inputNumber = parseInt(inputString);
if (!isFinite(inputNumber))
return;
model = new CalcFromRankModel(model);
model.updateData(inputNumber, 0, 0);
updateView();
});
document.getElementById("predictor-input-perf").addEventListener("keyup", () => {
const inputString = document.getElementById("predictor-input-perf").value;
const inputNumber = parseInt(inputString);
if (!isFinite(inputNumber))
return;
model = new CalcFromPerfModel(model);
model.updateData(0, inputNumber, 0);
updateView();
});
document.getElementById("predictor-input-rate").addEventListener("keyup", () => {
const inputString = document.getElementById("predictor-input-rate").value;
const inputNumber = parseInt(inputString);
if (!isFinite(inputNumber))
return;
model = new CalcFromRateModel(model);
model.updateData(0, 0, inputNumber);
updateView();
});
}
function updateStandingsFromAPI() {
return __awaiter(this, void 0, void 0, function* () {
try {
const shouldEnabled = shouldEnabledPredictor();
if (!shouldEnabled.verdict) {
model.updateInformation(shouldEnabled.message);
model.setEnable(false);
return;
}
const standings = yield getStandingsDataAsync(contestScreenName);
const aperfs = yield getAPerfsDataAsync(contestScreenName);
yield updateData(aperfs, standings);
model.updateInformation(`最后更新时间: ${new Date().toTimeString().split(" ")[0]}`);
model.setEnable(true);
}
catch (e) {
model.updateInformation(e.message);
model.setEnable(false);
}
});
}
function shouldEnabledPredictor() {
if (new Date() < startTime)
return { verdict: false, message: "比赛暂未开始" };
if (startTime < firstContestDate)
return {
verdict: false,
message: "这场比赛是在使用现行 Rating 制度之前举行的,无法准确计算 Rating 数据。",
};
if (contestInformation.RatedRange[0] > contestInformation.RatedRange[1])
return {
verdict: false,
message: "This contest is unrated.",
};
return { verdict: true, message: "" };
}
function updateView() {
const roundedRankValue = isFinite(model.rankValue) ? roundValue(model.rankValue, 2).toString() : "";
const roundedPerfValue = isFinite(model.perfValue) ? roundValue(model.perfValue, 2).toString() : "";
const roundedRateValue = isFinite(model.rateValue) ? roundValue(model.rateValue, 2).toString() : "";
document.getElementById("predictor-input-rank").value = roundedRankValue;
document.getElementById("predictor-input-perf").value = roundedPerfValue;
document.getElementById("predictor-input-rate").value = roundedRateValue;
document.getElementById("predictor-alert").innerHTML = `<h5 class='sidemenu-txt'>${model.information}</h5>`;
if (model.enabled)
enabled();
else
disabled();
if (isStandingsPage && shouldEnabledPredictor().verdict) {
tableUpdater.update(tableElement);
}
function enabled() {
predictorElements.forEach((element) => {
document.getElementById(element).disabled = false;
});
}
function disabled() {
predictorElements.forEach((element) => {
document.getElementById(element).disabled = false;
});
}
}
});
}
afterOpen() {
return __awaiter(this, void 0, void 0, function* () {
getPerformanceHistories(yield getHistoryDataAsync(userScreenName)).forEach((elem) => this.historyData.push(elem));
});
}
}
const predictor = new PredictorElement();
var dom$1 = "<div id=\"estimator-alert\"></div>\n<div class=\"row\">\n\t<div class=\"input-group\">\n\t\t<span class=\"input-group-addon\" id=\"estimator-input-desc\"></span>\n\t\t<input type=\"number\" class=\"form-control\" id=\"estimator-input\">\n\t</div>\n</div>\n<div class=\"row\">\n\t<div class=\"input-group\">\n\t\t<span class=\"input-group-addon\" id=\"estimator-res-desc\"></span>\n\t\t<input class=\"form-control\" id=\"estimator-res\" disabled=\"disabled\">\n\t\t<span class=\"input-group-btn\">\n\t\t\t<button class=\"btn btn-default\" id=\"estimator-toggle\">交换</button>\n\t\t</span>\n\t</div>\n</div>\n<div class=\"row\" style=\"margin: 10px 0px;\">\n\t<a class=\"btn btn-default col-xs-offset-8 col-xs-4\" rel=\"nofollow\" onclick=\"window.open(encodeURI(decodeURI(this.href)),'twwindow','width=550, height=450, personalbar=0, toolbar=0, scrollbars=1'); return false;\" id=\"estimator-tweet\">Tweet</a>\n</div>";
class EstimatorModel {
constructor(inputValue, perfHistory) {
this.inputDesc = "";
this.resultDesc = "";
this.perfHistory = perfHistory;
this.updateInput(inputValue);
}
updateInput(value) {
this.inputValue = value;
this.resultValue = this.calcResult(value);
}
toggle() {
return null;
}
calcResult(input) {
return input;
}
}
class CalcRatingModel extends EstimatorModel {
constructor(inputValue, perfHistory) {
super(inputValue, perfHistory);
this.inputDesc = "Performance";
this.resultDesc = "预计 Rating";
}
toggle() {
return new CalcPerfModel(this.resultValue, this.perfHistory);
}
calcResult(input) {
return positivizeRating(calcRatingFromHistory(this.perfHistory.concat([input])));
}
}
class CalcPerfModel extends EstimatorModel {
constructor(inputValue, perfHistory) {
super(inputValue, perfHistory);
this.inputDesc = "目标 Rating";
this.resultDesc = "所需 Performance";
}
toggle() {
return new CalcRatingModel(this.resultValue, this.perfHistory);
}
calcResult(input) {
return calcRequiredPerformance(unpositivizeRating(input), this.perfHistory);
}
}
function GetEmbedTweetLink(content, url) {
return `https://twitter.com/share?text=${encodeURI(content)}&url=${encodeURI(url)}`;
}
function getLS(key) {
const val = localStorage.getItem(key);
return (val ? JSON.parse(val) : val);
}
function setLS(key, val) {
try {
localStorage.setItem(key, JSON.stringify(val));
}
catch (error) {
console.log(error);
}
}
const models = [CalcPerfModel, CalcRatingModel];
function GetModelFromStateCode(state, value, history) {
let model = models.find((model) => model.name === state);
if (!model)
model = CalcPerfModel;
return new model(value, history);
}
class EstimatorElement extends SideMenuElement {
constructor() {
super(...arguments);
this.id = "estimator";
this.title = "Estimator";
this.document = dom$1;
this.match = /atcoder.jp/;
}
afterAppend() {
//nothing to do
}
// nothing to do
afterOpen() {
return __awaiter(this, void 0, void 0, function* () {
const estimatorInputSelector = document.getElementById("estimator-input");
const estimatorResultSelector = document.getElementById("estimator-res");
let model = GetModelFromStateCode(getLS("sidemenu_estimator_state"), getLS("sidemenu_estimator_value"), getPerformanceHistories(yield getHistoryDataAsync(userScreenName)));
updateView();
document.getElementById("estimator-toggle").addEventListener("click", () => {
model = model.toggle();
updateLocalStorage();
updateView();
});
estimatorInputSelector.addEventListener("keyup", () => {
updateModel();
updateLocalStorage();
updateView();
});
/** modelをinputの値に応じて更新 */
function updateModel() {
const inputNumber = estimatorInputSelector.valueAsNumber;
if (!isFinite(inputNumber))
return;
model.updateInput(inputNumber);
}
/** modelの状態をLSに保存 */
function updateLocalStorage() {
setLS("sidemenu_estimator_value", model.inputValue);
setLS("sidemenu_estimator_state", model.constructor.name);
}
/** modelを元にviewを更新 */
function updateView() {
const roundedInput = roundValue(model.inputValue, 2);
const roundedResult = roundValue(model.resultValue, 2);
document.getElementById("estimator-input-desc").innerText = model.inputDesc;
document.getElementById("estimator-res-desc").innerText = model.resultDesc;
estimatorInputSelector.value = String(roundedInput);
estimatorResultSelector.value = String(roundedResult);
const tweetStr = `AtCoderのハンドルネーム: ${userScreenName}\n${model.inputDesc}: ${roundedInput}\n${model.resultDesc}: ${roundedResult}\n`;
document.getElementById("estimator-tweet").href = GetEmbedTweetLink(tweetStr, "https://greasyfork.org/ja/scripts/369954-ac-predictor");
}
});
}
}
const estimator = new EstimatorElement();
var sidemenuHtml = "<style>\n #menu-wrap {\n display: block;\n position: fixed;\n top: 0;\n z-index: 20;\n width: 400px;\n right: -350px;\n transition: all 150ms 0ms ease;\n margin-top: 50px;\n }\n\n #sidemenu {\n background: #000;\n opacity: 0.85;\n }\n #sidemenu-key {\n border-radius: 5px 0px 0px 5px;\n background: #000;\n opacity: 0.85;\n color: #FFF;\n padding: 30px 0;\n cursor: pointer;\n margin-top: 100px;\n text-align: center;\n }\n\n #sidemenu {\n display: inline-block;\n width: 350px;\n float: right;\n }\n\n #sidemenu-key {\n display: inline-block;\n width: 50px;\n float: right;\n }\n\n .sidemenu-active {\n transform: translateX(-350px);\n }\n\n .sidemenu-txt {\n color: #DDD;\n }\n\n .menu-wrapper {\n border-bottom: 1px solid #FFF;\n }\n\n .menu-header {\n margin: 10px 20px 10px 20px;\n user-select: none;\n }\n\n .menu-box {\n overflow: hidden;\n transition: all 300ms 0s ease;\n }\n .menu-box-collapse {\n height: 0px !important;\n }\n .menu-box-collapse .menu-content {\n transform: translateY(-100%);\n }\n .menu-content {\n padding: 10px 20px 10px 20px;\n transition: all 300ms 0s ease;\n }\n .cnvtb-fixed {\n z-index: 19;\n }\n</style>\n<div id=\"menu-wrap\">\n <div id=\"sidemenu\" class=\"container\"></div>\n <div id=\"sidemenu-key\" class=\"glyphicon glyphicon-menu-left\"></div>\n</div>";
//import "./sidemenu.scss";
class SideMenu {
constructor() {
this.pendingElements = [];
this.Generate();
}
Generate() {
document.getElementById("main-div").insertAdjacentHTML("afterbegin", sidemenuHtml);
resizeSidemenuHeight();
const key = document.getElementById("sidemenu-key");
const wrap = document.getElementById("menu-wrap");
key.addEventListener("click", () => {
this.pendingElements.forEach((elem) => {
elem.afterOpen();
});
this.pendingElements.length = 0;
key.classList.toggle("glyphicon-menu-left");
key.classList.toggle("glyphicon-menu-right");
wrap.classList.toggle("sidemenu-active");
});
window.addEventListener("onresize", resizeSidemenuHeight);
document.getElementById("sidemenu").addEventListener("click", (event) => {
const target = event.target;
const header = target.closest(".menu-header");
if (!header)
return;
const box = target.closest(".menu-wrapper").querySelector(".menu-box");
box.classList.toggle("menu-box-collapse");
const arrow = target.querySelector(".glyphicon");
arrow.classList.toggle("glyphicon-menu-down");
arrow.classList.toggle("glyphicon-menu-up");
});
function resizeSidemenuHeight() {
document.getElementById("sidemenu").style.height = `${window.innerHeight}px`;
}
}
addElement(element) {
if (!element.shouldDisplayed(document.location.href))
return;
const sidemenu = document.getElementById("sidemenu");
sidemenu.insertAdjacentHTML("afterbegin", element.GetHTML());
const content = sidemenu.querySelector(".menu-content");
content.parentElement.style.height = `${content.offsetHeight}px`;
element.afterAppend();
this.pendingElements.push(element);
}
}
const sidemenu = new SideMenu();
const elements = [predictor, estimator];
for (let i = elements.length - 1; i >= 0; i--) {
sidemenu.addElement(elements[i]);
}