AtCoder Jump to Submissions from Standings

順位表の得点をダブルクリックすると、該当するコンテスタントの実装を見ることができます。

// ==UserScript==
// @name         AtCoder Jump to Submissions from Standings
// @namespace    http://tampermonkey.net/
// @version      0.2
// @description  順位表の得点をダブルクリックすると、該当するコンテスタントの実装を見ることができます。
// @match        https://atcoder.jp/contests/*/standings*
// @require      https://code.jquery.com/jquery-3.6.1.min.js
// @author       hiro_hiro
// @license      CC0
// @supportURL   https://github.com/KATO-Hiro/AtCoder-Jump-to-Submissions-from-Standings/issues
// @grant        none
// ==/UserScript==

$(function () {
    'use strict';

    $(document).on('dblclick', '.standings-result', function () {
        const $standingsType = getStandingsType($('body'));

        const $prefix = addPrefixIfNeeds($standingsType);

        const $clickedColumnIndex = getClickedColumnIndex(this, $standingsType);
        const $taskUrls = $('body').find('thead a');
        const $taskId = getTaskId($taskUrls, $clickedColumnIndex);

        const $username = getUserName(this);

        const $displayLanguage = getDisplayLanguage($(location));
        const $suffix = addSuffixIfNeeds($displayLanguage);

        // 順位表の範囲外なら、提出ページに遷移しない
        if ($clickedColumnIndex < $taskUrls.length) {
            jumpToPersonalSubmissions($prefix, $taskId, $username, $suffix);
        }
    });
})();

function getStandingsType(object) {
    let $standingsType = '';
    let isVirtual = $(object).find('script:contains("virtual")')[0];
    let isMultiply = $(object).find('script:contains("multiply_ranks")')[0];
    let isTeam = $(object).find('script:contains("team")')[0];
    let isExtended = $(object).find('script:contains("extended")')[0];

    // HACK: if分岐はメンテナンス的によくないかも
    // HACK: 他の言語のEnumに相当する構文がデフォルトで存在しない?
    if (isVirtual) {
        $standingsType = 'virtual';
    } else if (isMultiply) {
        $standingsType = 'multiply';
    } else if (isTeam) {
        $standingsType = 'team';
    } else if (isExtended) {
        $standingsType = 'extended';
    } else {
        $standingsType = 'general';
    }

    return $standingsType
}

function addPrefixIfNeeds(standingsType) {
    let prefix = '';

    if (standingsType != 'general') {
        prefix = '../';
    }

    return prefix
}

function getTaskId(taskUrls, clickedColumnIndex) {
    let $taskId = '';

    taskUrls.each((index) => {
        if (index == clickedColumnIndex) {
            const $url = taskUrls[index].pathname;
            const $elements = $url.split('/');
            const $length = $elements.length;

            $taskId = $elements[$length - 1]; // 0-indexed
        }
    });

    return $taskId
}

// HACK: 順位表の列数に応じた処理をしているため、AtCoderのUIが変更されると動かなくなる可能性がある
// WHY : 順位表の得点の欄に、問題のIDが含まれていないため
function getClickedColumnIndex(object, standingsType) {
    let $clickedColumnIndex = $(object)[0].cellIndex;

    // コンテスト当日の順位表と延長戦順位表・バーチャル順位表の列の並びに違いがある
    // 当日の順位表の並びに合わせる
    if (standingsType == 'virtual' || standingsType == 'extended') {
        $clickedColumnIndex -= 1
    }

    // 順位とユーザ名の欄を扱わずに済むようにインデックスを補正
    // A問題がindex = 0となるようにしている
    $clickedColumnIndex -= 3

    return $clickedColumnIndex
}

function getUserName(object) {
    const $standings = $(object).siblings('td');
    const $username = $standings.find('.username span').text();

    return $username
}

function getDisplayLanguage(location) {
    let language = 'jp';
    const params = location.attr('search');

    if (params.match(/lang=en/)) {
        language = 'en';
    }

    return language
}

function addSuffixIfNeeds($displayLanguage) {
    let suffix = '&lang=';

    if ($displayLanguage == 'en') {
        suffix += 'en';
    } else {
        suffix = '';
    }

    return suffix
}

function jumpToPersonalSubmissions(prefix, taskId, username, suffix) {
    setTimeout(function () {
        location.href = `${prefix}submissions?f.Task=${taskId}&f.Language=&f.Status=AC&f.User=${username}${suffix}`;
    }, 250)
}