AtCoder Problems Penalty Counter

AtCoder Problems のテーブルページ上で問題ごとにコンテスト中のペナルティ数を表示します

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         AtCoder Problems Penalty Counter
// @namespace    iilj
// @version      2020.01.28.2
// @description  AtCoder Problems のテーブルページ上で問題ごとにコンテスト中のペナルティ数を表示します
// @author       iilj
// @supportURL   https://github.com/iilj/AtCoderProblemsExt/issues
// @match        https://kenkoooo.com/atcoder/*
// @grant        GM_addStyle
// ==/UserScript==

(function () {
    'use strict';

    GM_addStyle(`
span.appc-penalty {
    color: red;
    margin-left: .4rem;
    margin-right: .1rem;
}
`);

    /**
     * AtCoder コンテストの URL を返す.
     *
     * @param {string} contestId コンテスト ID
     * @returns {string} AtCoder コンテストの URL
     */
    const getContestUrl = (contestId) => `https://atcoder.jp/contests/${contestId}`;

    /**
     * AtCoder コンテストの問題 URL を返す.
     *
     * @param {string} contestId コンテスト ID
     * @param {string} problemId 問題 ID
     * @returns {string} AtCoder コンテストの問題 URL
     */
    const getProblemUrl = (contestId, problemId) => `${getContestUrl(contestId)}/tasks/${problemId}`;

    /**
     * url string to json object
     *
     * @date 2020-01-15
     * @param {string} uri 取得するリソースのURI
     * @returns {Promise<Object[]>} 配列
     */
    async function getJson(uri) {
        const response = await fetch(uri);
        /** @type {Object[]} */
        const obj = await response.json();
        return obj;
    }

    /**
     * get contestId->contest map and contestUrl->contestId map
     *
     * @date 2020-01-15
     * @returns {Promise<Object[]>} array [contestId->contest map, contestUrl->contestId map]
     */
    async function getContestsMap() {
        const contests = await getJson('https://kenkoooo.com/atcoder/resources/contests.json');
        const contestsMap = contests.reduce((hash, contest) => {
            hash[contest.id] = contest;
            return hash;
        }, {});
        const contestsUrl2Id = contests.reduce((hash, contest) => {
            hash[getContestUrl(contest.id)] = contest.id;
            return hash;
        }, {});
        return [contestsMap, contestsUrl2Id];
    }

    /**
     * return problemUrl->penalty map from userId string
     *
     * @date 2020-01-15
     * @param {string} userId
     * @returns {Promise<Object>} problemUrl->penalty map
     */
    async function getUserPenaltyMap(userId, contestsMap) {
        const userResults = await getJson(`https://kenkoooo.com/atcoder/atcoder-api/results?user=${userId}`);
        const userPenaltyMap = userResults.reduce((hash, submit) => {
            const key = getProblemUrl(submit.contest_id, submit.problem_id);
            const contest = contestsMap[submit.contest_id];
            if (!(key in hash)) {
                hash[key] = 0;
            }
            if (submit.epoch_second <= contest.start_epoch_second + contest.duration_second && submit.result != 'AC') {
                hash[key]++;
            }
            return hash;
        }, {});
        return userPenaltyMap;
    }

    /**
     * Table 表示ページで "Show Accepted" の変更検知に利用する MutationObserver
     *
     * @type {MutationObserver}
     */
    let tableObserver;

    /**
     * Table 表示ページで表のセルの色を塗り分ける.
     *
     * @date 2020-01-16
     * @param {string} userId
     */
    async function processTable(userId) {
        const [contestsMap, contestsUrl2Id] = await getContestsMap();
        const userPenaltyMap = await getUserPenaltyMap(userId, contestsMap);

        const tableChanged = () => {
            if (tableObserver) {
                tableObserver.disconnect();
            }
            document.querySelectorAll('span.appc-penalty').forEach(spanPenalty => {
                spanPenalty.innerText = '';
            });
            document.querySelectorAll('td.table-problem').forEach(td => {
                const lnk = td.querySelector('a[href]');
                if (lnk && lnk.href in userPenaltyMap && userPenaltyMap[lnk.href] > 0) {
                    const userPenalty = userPenaltyMap[lnk.href];
                    let divTimespan = td.querySelector('.table-problem-timespan');
                    if (!divTimespan) {
                        divTimespan = document.createElement("div");
                        divTimespan.classList.add('table-problem-timespan');
                        td.insertAdjacentElement('beforeend', divTimespan);
                    }
                    let spanPenalty = divTimespan.querySelector('span.appc-penalty');
                    if (!spanPenalty) {
                        spanPenalty = document.createElement("span");
                        spanPenalty.classList.add('appc-penalty');
                        divTimespan.insertAdjacentElement('beforeend', spanPenalty);
                    }
                    spanPenalty.innerText = `(${userPenalty})`;
                }
            });
            if (tableObserver) {
                document.querySelectorAll('.react-bs-container-body').forEach(div => {
                    tableObserver.observe(div, { childList: true, subtree: true });
                });
            }
        };

        tableObserver = new MutationObserver(mutations => tableChanged());
        tableChanged();
        document.querySelectorAll('.react-bs-container-body').forEach(div => {
            tableObserver.observe(div, { childList: true, subtree: true });
        });
    }

    /**
     * ページ URL が変化した際のルートイベントハンドラ.
     *
     * @date 2020-01-15
     */
    const hrefChanged = () => {
        if (tableObserver) {
            tableObserver.disconnect();
        }

        /** @type {RegExpMatchArray} */
        let result;
        if (result = location.href.match(/^https?:\/\/kenkoooo\.com\/atcoder\/#\/table\/([^/?#]+)/)) {
            const userId = result[1];
            processTable(userId);
        }
    };

    let href = location.href;
    const observer = new MutationObserver(mutations => {
        if (href === location.href) {
            return;
        }
        // href changed
        href = location.href;
        hrefChanged();
    });
    observer.observe(document, { childList: true, subtree: true });
    hrefChanged();
})();