// ==UserScript==
// @name ac-history-perf-estimator-with-AJL
// @namespace http://ac-history-perf-filler.example.com
// @version 1.0.3
// @description ac-history-perf-estimatorをAJLスコアに対応させました
// @match https://atcoder.jp/users/*/history*
// @grant none
// @license MIT
// ==/UserScript==
// should not be here
function isDebugMode() {
return isDebug;
}
const colorNames = ["unrated", "gray", "brown", "green", "cyan", "blue", "yellow", "orange", "red"];
async function getColor(rating) {
const colorIndex = rating > 0 ? Math.min(Math.floor(rating / 400) + 1, 8) : 0;
return colorNames[colorIndex];
}
async function getAPerfs(contestScreenName) {
const result = await fetch(`https://data.ac-predictor.com/aperfs/${contestScreenName}.json`);
if (!result.ok) {
throw new Error(`Failed to fetch aperfs: ${result.status}`);
}
return await result.json();
}
// [start, end]
class Range {
start;
end;
constructor(start, end) {
this.start = start;
this.end = end;
}
contains(val) {
return this.start <= val && val <= this.end;
}
hasValue() {
return this.start <= this.end;
}
}
class ContestDetails {
contestName;
contestScreenName;
contestType;
startTime;
duration;
ratedrange;
constructor(contestName, contestScreenName, contestType, startTime, duration, ratedRange) {
this.contestName = contestName;
this.contestScreenName = contestScreenName;
this.contestType = contestType;
this.startTime = startTime;
this.duration = duration;
this.ratedrange = ratedRange;
}
get endTime() {
return new Date(this.startTime.getTime() + this.duration * 1000);
}
get defaultAPerf() {
if (this.contestType == "heuristic")
return 1000;
if (!this.ratedrange.hasValue()) {
throw new Error("unrated contest");
}
// value is not relevant as it is never used
if (!this.ratedrange.contains(0))
return 800;
if (this.ratedrange.end == 1199)
return 800;
if (this.ratedrange.end == 1999)
return 800;
const DEFAULT_CHANGED_AT = new Date("2019-05-25"); // maybe wrong
if (this.ratedrange.end == 2799) {
if (this.startTime < DEFAULT_CHANGED_AT)
return 1600;
else
return 1000;
}
if (4000 <= this.ratedrange.end) {
if (this.startTime < DEFAULT_CHANGED_AT)
return 1600;
else
return 1200;
}
throw new Error("unknown contest type");
}
get performanceCap() {
if (this.contestType == "heuristic")
return Infinity;
if (!this.ratedrange.hasValue()) {
throw new Error("unrated contest");
}
if (4000 <= this.ratedrange.end)
return Infinity;
return this.ratedrange.end + 1 + 400;
}
beforeContest(dateTime) {
return dateTime < this.startTime;
}
duringContest(dateTime) {
return this.startTime < dateTime && dateTime < this.endTime;
}
isOver(dateTime) {
return this.endTime < dateTime;
}
}
async function getContestDetails() {
const result = await fetch(`https://data.ac-predictor.com/contest-details.json`);
if (!result.ok) {
throw new Error(`Failed to fetch contest details: ${result.status}`);
}
const parsed = await result.json();
const res = [];
for (const elem of parsed) {
if (typeof elem !== "object")
throw new Error("invalid object returned");
if (typeof elem.contestName !== "string")
throw new Error("invalid object returned");
const contestName = elem.contestName;
if (typeof elem.contestScreenName !== "string")
throw new Error("invalid object returned");
const contestScreenName = elem.contestScreenName;
if (elem.contestType !== "algorithm" && elem.contestType !== "heuristic")
throw new Error("invalid object returned");
const contestType = elem.contestType;
if (typeof elem.startTime !== "number")
throw new Error("invalid object returned");
const startTime = new Date(elem.startTime * 1000);
if (typeof elem.duration !== "number")
throw new Error("invalid object returned");
const duration = elem.duration;
if (typeof elem.ratedrange !== "object" || typeof elem.ratedrange[0] !== "number" || typeof elem.ratedrange[1] !== "number")
throw new Error("invalid object returned");
const ratedRange = new Range(elem.ratedrange[0], elem.ratedrange[1]);
res.push(new ContestDetails(contestName, contestScreenName, contestType, startTime, duration, ratedRange));
}
return res;
}
class Cache {
cacheDuration;
cacheExpires = new Map();
cacheData = new Map();
constructor(cacheDuration) {
this.cacheDuration = cacheDuration;
}
has(key) {
return this.cacheExpires.has(key) || Date.now() <= this.cacheExpires.get(key);
}
set(key, content) {
const expire = Date.now() + this.cacheDuration;
this.cacheExpires.set(key, expire);
this.cacheData.set(key, content);
}
get(key) {
if (!this.has(key)) {
throw new Error(`invalid key: ${key}`);
}
return this.cacheData.get(key);
}
}
class EloPerformanceProvider {
ranks;
ratings;
cap;
rankMemo = new Map();
constructor(ranks, ratings, cap) {
this.ranks = ranks;
this.ratings = ratings;
this.cap = cap;
}
availableFor(userScreenName) {
return this.ranks.has(userScreenName);
}
getPerformance(userScreenName) {
if (!this.availableFor(userScreenName)) {
throw new Error(`User ${userScreenName} not found`);
}
const rank = this.ranks.get(userScreenName);
return this.getPerformanceForRank(rank);
}
getPerformances() {
const performances = new Map();
for (const userScreenName of this.ranks.keys()) {
performances.set(userScreenName, this.getPerformance(userScreenName));
}
return performances;
}
getPerformanceForRank(rank) {
let upper = 6144;
let lower = -2048;
while (upper - lower > 0.5) {
const mid = (upper + lower) / 2;
if (rank > this.getRankForPerformance(mid))
upper = mid;
else
lower = mid;
}
return Math.min(this.cap, Math.round((upper + lower) / 2));
}
getRankForPerformance(performance) {
if (this.rankMemo.has(performance))
return this.rankMemo.get(performance);
const res = this.ratings.reduce((val, APerf) => val + 1.0 / (1.0 + Math.pow(6.0, (performance - APerf) / 400.0)), 0.5);
this.rankMemo.set(performance, res);
return res;
}
}
function getRankToUsers(ranks) {
const rankToUsers = new Map();
for (const [userScreenName, rank] of ranks) {
if (!rankToUsers.has(rank))
rankToUsers.set(rank, []);
rankToUsers.get(rank).push(userScreenName);
}
return rankToUsers;
}
function getMaxRank(ranks) {
return Math.max(...ranks.values());
}
class InterpolatePerformanceProvider {
ranks;
maxRank;
rankToUsers;
baseProvider;
constructor(ranks, baseProvider) {
this.ranks = ranks;
this.maxRank = getMaxRank(ranks);
this.rankToUsers = getRankToUsers(ranks);
this.baseProvider = baseProvider;
}
availableFor(userScreenName) {
return this.ranks.has(userScreenName);
}
getPerformance(userScreenName) {
if (!this.availableFor(userScreenName)) {
throw new Error(`User ${userScreenName} not found`);
}
if (this.performanceCache.has(userScreenName))
return this.performanceCache.get(userScreenName);
let rank = this.ranks.get(userScreenName);
while (rank <= this.maxRank) {
const perf = this.getPerformanceIfAvailable(rank);
if (perf !== null) {
return perf;
}
rank++;
}
this.performanceCache.set(userScreenName, -Infinity);
return -Infinity;
}
performanceCache = new Map();
getPerformances() {
let currentPerformance = -Infinity;
const res = new Map();
for (let rank = this.maxRank; rank >= 0; rank--) {
const users = this.rankToUsers.get(rank);
if (users === undefined)
continue;
const perf = this.getPerformanceIfAvailable(rank);
if (perf !== null)
currentPerformance = perf;
for (const userScreenName of users) {
res.set(userScreenName, currentPerformance);
}
}
this.performanceCache = res;
return res;
}
cacheForRank = new Map();
getPerformanceIfAvailable(rank) {
if (!this.rankToUsers.has(rank))
return null;
if (this.cacheForRank.has(rank))
return this.cacheForRank.get(rank);
for (const userScreenName of this.rankToUsers.get(rank)) {
if (!this.baseProvider.availableFor(userScreenName))
continue;
const perf = this.baseProvider.getPerformance(userScreenName);
this.cacheForRank.set(rank, perf);
return perf;
}
return null;
}
}
function normalizeRank(ranks) {
const rankValues = [...new Set(ranks.values()).values()];
const rankToUsers = new Map();
for (const [userScreenName, rank] of ranks) {
if (!rankToUsers.has(rank))
rankToUsers.set(rank, []);
rankToUsers.get(rank).push(userScreenName);
}
rankValues.sort((a, b) => a - b);
const res = new Map();
let currentRank = 1;
for (const rank of rankValues) {
const users = rankToUsers.get(rank);
const averageRank = currentRank + (users.length - 1) / 2;
for (const userScreenName of users) {
res.set(userScreenName, averageRank);
}
currentRank += users.length;
}
return res;
}
//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 calcAlgRatingFromHistory(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 calcAlgRatingFromLast(last, perf, ratedMatches) {
if (ratedMatches === 0)
return perf - 1200;
last += f(ratedMatches);
const weight = 9 - 9 * 0.9 ** ratedMatches;
const numerator = weight * 2 ** (last / 800.0) + 2 ** (perf / 800.0);
const denominator = 1 + weight;
return Math.log2(numerator / denominator) * 800.0 - f(ratedMatches + 1);
}
/**
* 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(calcAlgRatingFromHistory(history.concat([mid])));
if (targetRating <= rating)
valid = mid;
else
invalid = mid;
}
return valid;
}
/**
* Gets the weight used in the heuristic rating calculation
* based on its start and end dates
* @param {Date} startAt - The start date of the contest.
* @param {Date} endAt - The end date of the contest.
* @returns {number} The weight of the contest.
*/
function getWeight(startAt, endAt) {
const isShortContest = endAt.getTime() - startAt.getTime() < 24 * 60 * 60 * 1000;
if (endAt < new Date("2025-01-01T00:00:00+09:00")) {
return 1;
}
return isShortContest ? 0.5 : 1;
}
/**
* calculate unpositivized rating from performance history
* @param {RatingMaterial[]} [history] performance histories
* @returns {Number} unpositivized rating
*/
function calcHeuristicRatingFromHistory(history) {
const S = 724.4744301;
const R = 0.8271973364;
const qs = [];
for (const material of history) {
const adjustedPerformance = material.Performance + 150 - 100 * material.DaysFromLatestContest / 365;
for (let i = 1; i <= 100; i++) {
qs.push({ q: adjustedPerformance - S * Math.log(i), weight: material.Weight });
}
}
qs.sort((a, b) => b.q - a.q);
let r = 0.0;
let s = 0.0;
for (const { q, weight } of qs) {
s += weight;
r += q * (R ** (s - weight) - R ** s);
}
return r;
}
/**
* (-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);
}
function hasOwnProperty(obj, key) {
return Object.prototype.hasOwnProperty.call(obj, key);
}
let StandingsWrapper$1 = class StandingsWrapper {
data;
constructor(data) {
this.data = data;
}
toRanks(onlyRated = false, contestType = "algorithm") {
const res = new Map();
for (const data of this.data.StandingsData) {
if (onlyRated && !this.isRated(data, contestType))
continue;
res.set(data.UserScreenName, data.Rank);
}
return res;
}
toRatedUsers(contestType) {
const res = [];
for (const data of this.data.StandingsData) {
if (this.isRated(data, contestType)) {
res.push(data.UserScreenName);
}
}
return res;
}
toScore(user) {
for (const data of this.data.StandingsData) {
if (data.UserScreenName == user) return data.TotalResult.Score
}
}
isRated(data, contestType = "algorithm") {
if (contestType === "algorithm") {
return data.IsRated;
}
if (contestType === "heuristic") {
return data.IsRated && data.TotalResult.Count !== 0;
}
throw new Error("unreachable");
}
};
const STANDINGS_CACHE_DURATION$1 = 10 * 1000;
const cache$1 = new Cache(STANDINGS_CACHE_DURATION$1);
async function getStandings(contestScreenName) {
if (!cache$1.has(contestScreenName)) {
const result = await fetch(`https://atcoder.jp/contests/${contestScreenName}/standings/json`);
if (!result.ok) {
throw new Error(`Failed to fetch standings: ${result.status}`);
}
cache$1.set(contestScreenName, await result.json());
}
return new StandingsWrapper$1(cache$1.get(contestScreenName));
}
async function loadPerformances() {
'use strict';
const pathParts = location.pathname.split('/');
const user = pathParts[2];
// コンテスト詳細一覧を取得
const contestDetailsList = await getContestDetails();
// 各行に処理を並列で適用
const rowPromises = Array.from(document.querySelectorAll('#history tbody tr')).map(async (row) => {
const perfCell = row.children[3];
if (perfCell && perfCell.textContent.trim() === '-') {
const link = row.children[1].querySelector('a');
if (!link) return;
const parts = link.pathname.split('/');
const contestScreenName = parts[2];
const contestDetails = contestDetailsList.find(details => details.contestScreenName == contestScreenName);
if (!contestDetails) return;
const aperfsDict = await getAPerfs(contestDetails.contestScreenName);
const defaultAPerf = contestDetails.defaultAPerf;
const standings = await getStandings(contestDetails.contestScreenName);
const score = standings.toScore(user);
const normalizedRanks = normalizeRank(standings.toRanks(true, contestDetails.contestType));
const aperfsList = standings.toRatedUsers(contestDetails.contestType).map(user => hasOwnProperty(aperfsDict, user) ? aperfsDict[user] : defaultAPerf);
const basePerformanceProvider = new EloPerformanceProvider(normalizedRanks, aperfsList, contestDetails.performanceCap);
const performanceProvider = new InterpolatePerformanceProvider(standings.toRanks(), basePerformanceProvider);
const perfRaw = score == 0 ? 0 : parseInt(positivizeRating(performanceProvider.getPerformance(user)));
const span = document.createElement("span");
span.textContent = perfRaw.toString();
span.style.color = await getColor(perfRaw);
span.style.opacity = "0.6";
perfCell.innerHTML = "";
perfCell.appendChild(span);
}
});
await Promise.all(rowPromises); // 全ての行の処理を待つ
}
(async () => {
await loadPerformances(); // 完了まで待つ
console.debug("Loading AJL Scores");
const table = Array.from(document.querySelector("#history").rows);
const tableTitleElem = document.createElement("th");
tableTitleElem.style["text-align"] = "center";
tableTitleElem.textContent = "AJL";
tableTitleElem.classList.add("sorting", "ajl");
tableTitleElem.addEventListener("click", () => {
document.querySelector("th:nth-child(4)").click();
});
table[0].insertBefore(tableTitleElem, table[0].childNodes[5]);
const scores = [];
table.slice(1).forEach((element) => {
const perf = Number(element.childNodes[7].textContent);
const ajlScore = perf == 0 ? 0 : Math.round(Math.pow(2, perf / 400) * 1000);
const ajlScoreElem = document.createElement("td");
ajlScoreElem.textContent = ajlScore;
if (!isNaN(ajlScore)) {
scores.push(ajlScore);
}
const ratingElem = element.childNodes[11];
element.insertBefore(ajlScoreElem, ratingElem);
});
const labelEle = document.createElement("label");
labelEle.textContent = "AJL Calculator (Please enter the latest number of doses you would like.) ->";
labelEle.htmlFor = "ajl-cal";
const numEle = document.createElement("input");
numEle.type = "number";
numEle.id = "ajl-cal";
numEle.value = table.slice(1).length;
const titleEle = document.querySelector("div.col-sm-12:has(h2) #user-nav-tabs");
titleEle.parentNode.insertBefore(numEle, titleEle);
numEle.parentNode.insertBefore(labelEle, numEle);
const ansEle = document.createElement("p");
titleEle.parentNode.insertBefore(ansEle, titleEle);
function getScore(count) {
const src = scores.slice(0, count).sort((a, b) => b - a);
let sum = 0, cnt = 0;
for (let i = 0; i < src.length && cnt < 10; i++) {
if (isNaN(src[i])) continue;
sum += src[i];
cnt++;
}
ansEle.textContent = "AJL Score: " + sum;
}
getScore(numEle.value);
numEle.addEventListener("change", () => getScore(numEle.value));
})();