Greasy Fork is available in English.

AtCoderACPercentage

自分と同じくらいのレートの人々の何%がその問題を解けているかを表示する。

質問やレビューの投稿はこちらへ、スクリプトの通報はこちらへお寄せください。
// ==UserScript==
// @name         AtCoderACPercentage
// @namespace    https://github.com/null-null-programming
// @version      0.4
// @description  自分と同じくらいのレートの人々の何%がその問題を解けているかを表示する。
// @author       null_null
// @license      MIT
// @include        https://atcoder.jp/contests/*/standings*
// @exclude        https://atcoder.jp/contests/*/standings/json
// @require      https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js
// ==/UserScript==

(async function () {
    //参加者自身のRating
    const userRating = await getUserRating(userScreenName);

    //問題名のリスト
    const problemNames = standings.TaskInfo.map(task => task.TaskScreenName);
    //問題名の記号(AとかBとか)配列 <-これを作っておかないとF1 F2 などが来たときにバグる
    const Assignment = standings.TaskInfo.map(task => task.Assignment);

    let solvedPercentage;

    initView();
    updateData();
    updateView();

    //ビューのupdate監視
    new MutationObserver(updateView).observe(
        document.getElementById("standings-tbody"),
        { childList: true }
    );

    //リフレッシュボタンの監視(押されたとき/自動更新時に一瞬だけ無効化される)
    new MutationObserver(async mutationRecord => {
        const isDisabled = mutationRecord[0].target.classList.contains(
            "disabled"
        );
        if (isDisabled) {
            updateData();
            updateView();
        }
    })
    .observe(document.getElementById("refresh"), {
        attributes: true,
        attributeFilter: ["class"]
    });

    function updateData(){
        //コンテスト情報を辞書型に直す userScreenName->Data
        const contestResultData = {};
        //参加回数が15回以上のコンテスト参加者のuserScreenNameリスト
        let contestUserName = [];

        standings.StandingsData.forEach(res => {
            //辞書型に変換
            contestResultData[res.UserScreenName] = res;

            //コンテスト参加回数10回未満、自分自身、未提出者は除いてリストに入れる
            if (res.Competitions >= 10 && res.UserScreenName !== userScreenName && res.TotalResult.Count > 0)
                contestUserName.push(res.UserScreenName);
        });

        //TODO:評価関数の洗練
        //自身のレートとの絶対値の差が小さい順に並び替え。
        contestUserName.sort(function (x, y) {
            return Math.abs(userRating - contestResultData[x].Rating) - Math.abs(userRating - contestResultData[y].Rating);
        });

        //TODO:選抜者人数の見直し
        //選抜者人数の10パーセントを選抜者人数とする。
        const USER_NUM = contestUserName.length * 0.1;

        //自身のレートに近いUSER_NUM人の参加者を選抜
        contestUserName = contestUserName.slice(0, USER_NUM);

        //何人が解けたかを問題ごとに集計
        solvedPercentage = problemNames.map(problemName => {
            let sum = 0;

            //各ユーザーごとに集計
            contestUserName.forEach(userName => {
                if ((contestResultData[userName].TaskResults[problemName] || -1).Score > 0) sum++;
            });

            //小数第1位までパーセントを表示
            return Math.round(sum * 10 * 100 / USER_NUM) / 10;
        });
    }

    function initView(){
        //結果を表示するテーブルを作成する。
        //行を追加
        let table = document.getElementById('standings-tbody');
        let row = table.insertRow(-1);

        row.id = 'ac-percentage-row';

        //列を追加
        let cells = [];

        for (let i = 0; i < problemNames.length + 1; i++) {
            cells[i] = row.insertCell(i);

            if (i === 0) {
                //行の左端  題名を書き込む
                cells[i].innerText = 'AC Percentage';
                cells[i].style.color = '#00AA3E';
                cells[i].colSpan = '3';
            } else {
                cells[i].style.color = '#888888';
            }
            cells[i].style.fontSize = '80%';
        }
    }

    function updateView(){
        //結果を表示するテーブルを作成する。
        //行を取得
        let row = document.getElementById('ac-percentage-row');
        if (!row) initView();

        for (let i = 1; i < problemNames.length + 1; i++) {
            let cell = row.children[i];
            cell.innerText = Assignment[i - 1] + ':' + solvedPercentage[i - 1] + '%';
        }
    }
})();

//参加しているコンテスト名を取得する。
function getContestName() {
    let contestURL = location.href;
    let contestArray = contestURL.split('/');
    return contestArray[contestArray.length - 2];
}

async function getUserRating(userScreenName) {
    let parser = new DOMParser();
    let archiveDom = parser.parseFromString((await $.get('https://atcoder.jp/users/' + userScreenName)), "text/html");
    let userRating = archiveDom.querySelector("#main-container > div.row > div.col-sm-9 > table > tbody > tr:nth-child(2) > td > span");
    return Number(userRating.innerText);
}