AtCoder Problems Team Standings (beta ver.)

Problemsのバーチャルコンテストで(身内用)チーム順位表を作成します。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         AtCoder Problems Team Standings (beta ver.)
// @namespace    AtCoder Problems
// @version      0.3
// @description  Problemsのバーチャルコンテストで(身内用)チーム順位表を作成します。
// @author       harurun
// @match        https://kenkoooo.com/atcoder/*
// @grant        GM_getValue
// @grant        GM_setValue
// @license      MIT
// ==/UserScript==

/*グローバル変数*/
//ユーザ名とチーム名の対応連想配列
var user_team={};
//上記の逆の連想配列
var user_to_team={};
//コンテスト名
var contest="";
//各問題の点数
var problem=[];
//チームスコア
var team_sc_data={};
//チームの集合
var teams=new Set();
//settingユーザ集合
var users=new Set();
//ペナタイム
var penalty_time=300;
//各問題を管理
var problem_name={};
//開始時間
var start_time=0;
//終了時間
var end_time=0;
//standingを変数に格納
var standing;
//team_standing 変数
var team_standing;
//debug cnt
var debug_cnt=0;
//tr_elements
var tr_elements;


/*コンテスト名を返す*/
function get_contest(){
  contest=document.getElementsByTagName("h1")[0].textContent;
  penalty_time=parseInt(document.getElementsByTagName("th")[1].nextElementSibling.textContent.split(" ")[0]);
  return;
}

/*チームスコアの初期化関数*/
function set_team(cnt/*問題数*/){
  var team_list=[...teams];
  for(var i in team_list){
    team_sc_data[team_list[i]]=[];
    for(var j=0;j<cnt;j++){
      team_sc_data[team_list[i]].push([-1,0]);
    }
  }
  return;
}

/*各チームの点数を数える*/
function get_users(i,tr_elements,cnt){
  //console.log(`cnt:${cnt}`)
  for(;i<tr_elements.length;i++){
    var u=tr_elements[i].children
    //console.log(u);
    var user_name=u[1].children[0].textContent.replace(/\s+/g, "");//名前
    //console.log(`user_name:${user_name}`,"users.has:",users.has(user_name));
    if(!users.has(user_name))continue;
    var team=user_team[user_name];
    for(var j=3;j<cnt+3;j++){
      //console.log(u[j]);
      //if(u[j]==undefined)break;
      if(u[j].textContent==="-")continue;//未提出
      var t=u[j].children[0].children;//スコアとペナ//
      //console.log(`t_length:${t.length}`);
      if(t.length===0)continue;
      var sc=t[0].textContent;//点数//部分点はそもそもProblemsにないので使わない
      var pnl_int=0;
      if(t.length==2){//0ペナでも必ず2つあるらしい
        var pnl_str=t[1].textContent;//ペナ数
        if(!(pnl_str==="")){
          pnl_int=pnl_str.substr(1,pnl_str.length-2);//ペナ数の()を外す
        }
      }
      //ペナを足す
      team_sc_data[team][j-3][1]+=parseInt(pnl_int);
      //console.log(`sc:${sc}`);
      if(sc==="0")continue;//WA
      var spend_time=u[j].children[1].textContent.split(":");//時間
      //素の時間で比較する。ペナは足すだけ
      if(parseInt(team_sc_data[team][j-3][0])===-1){
        team_sc_data[team][j-3][0]=parseInt(spend_time[0]*3600)+parseInt(spend_time[1])*60+parseInt(spend_time[2]);
      }else if(parseInt(spend_time[0]*3600)+parseInt(spend_time[1])*60+parseInt(spend_time[2])<parseInt(team_sc_data[team][j-3][0])){
        team_sc_data[team][j-3][0]=parseInt(spend_time[0]*3600)+parseInt(spend_time[1])*60+parseInt(spend_time[2]);
      }
      //console.log(team_sc_data);
    }/*各ユーザの点数for*/
  }/*各ユーザ*/
  return;
}

//問題の点数を数える関数
function get_score(){
  var start_flag=false;
  //var tr_elements=document.getElementsByTagName("tr");
  //console.log(document.getElementsByTagName("tr"));
  for(var i=0;i<tr_elements.length;i++){
    var f=tr_elements[i].children;
    //console.log(f);
    //終了
    if(f[0].textContent==="#"){
    //各ユーザの時間を取得
      set_team(problem.length);
      get_users(i+1,tr_elements,problem.length);
      break;
    }
    //開始
    if(f[f.length-1].textContent==="Score"){
      start_flag=true;
      continue;
    }
    //カウント
    if(start_flag){
      var ret=f[f.length-1].textContent;
      if(ret===""){
        ret=1;
      }
      problem.push(parseInt(ret));
      var prob_url_txt=String(f[1].children[0].href).split("/");
      problem_name[parseInt(f[0].textContent)-1]=prob_url_txt[prob_url_txt.length-1];
    }
  }
  //console.log(team_sc_data);
  display_score();
  return;
}

/*チームスコアを表示する*/
function display_score(){
  //console.log(`start display_score:${debug_cnt}回目`);
  //console.log(team_sc_data);
  debug_cnt++;
  //それぞれのチームの得点を計算してソートする。
  var scores=[];//[合計,ペナ入り時間,チーム名]
  for(var key in team_sc_data){
    var team_sc=team_sc_data[key];
    var point=0;
    var time_sec=0;
    var cnt_pnl=0;
    var pure_time=0;
    for(var i=0;i<team_sc.length;i++){
      if(team_sc[i][0]===-1)continue;
      if(team_sc[i][0]!==-1){
        point+=problem[i];
      }
      if(team_sc[i][1]!==-1){
        time_sec=Math.max(time_sec,team_sc[i][0]+team_sc[i][1]*penalty_time);
        pure_time=Math.max(pure_time,team_sc[i][0]);
        cnt_pnl+=team_sc[i][1];
      }
    }
    scores.push([point,time_sec,key,pure_time,cnt_pnl]);
  }
  scores.sort(function(a,b){
    if(a[0]===b[0]){
      if(a[1]===b[1]){
        return a[4]-b[4];//ペナが少ない方
      }
      return a[1]-b[1];//スコアが同じ時はペナ入りの時間で
    }else{
      return b[0]-a[0];
    }
  })

  var div_my2=document.createElement("div");
  div_my2.id="standing-frame";
  team_standing=div_my2;
  var div_title=document.createElement("div");

  var title_h4=document.createElement("h4");
  var h4_text=document.createTextNode("Team-Standings")
  title_h4.appendChild(h4_text);
  div_title.appendChild(title_h4);

  div_my2.appendChild(div_title);

  //div1,div2->
  var div1 = document.createElement("div");
  div1.classList.add("row");

  var div2=document.createElement("div");
  div2.classList.add("col-sm-12");

  //table->
  var main_table = document.createElement("table");

  //thead->
  var main_thead = document.createElement("thead");
  var head_tr=document.createElement("tr");
  head_tr.classList.add("text-center");

  var sharp_th=document.createElement("th");
  var sharp_text=document.createTextNode("##");
  sharp_th.appendChild(sharp_text);
  head_tr.appendChild(sharp_th);

  var te_th=document.createElement("th");
  var te_text=document.createTextNode("Team");
  te_th.appendChild(te_text);
  head_tr.appendChild(te_th);

  var sc_th=document.createElement("th");
  var sc_text=document.createTextNode("Score");
  sc_th.appendChild(sc_text);
  head_tr.appendChild(sc_th);

  for(var i=0;i<problem.length;i++){
    var num_th=document.createElement("th");
    var num_text=document.createTextNode(String(i+1));
    num_th.appendChild(num_text);
    head_tr.appendChild(num_th);
  }

  main_table.appendChild(head_tr);
  //<-thead

  //tbody->
  var team_list=[...teams];
  var main_tbody=document.createElement("tbody");
  for(var i=0;i<team_list.length;i++){
    var now_team=scores[i];
    var create_tr=document.createElement("tr");
    //thを追加していく
    //id を付ける

    //順位
    var create_rank=document.createElement("th");
    var rank_text=document.createTextNode(String(i+1));
    create_rank.appendChild(rank_text);
    create_tr.appendChild(create_rank);

    //チーム名
    var team_th=document.createElement("th");
    var display_name=String(now_team[2])+"(";
    for(var l=0;l<user_to_team[now_team[2]].length;l++){
      display_name+=user_to_team[now_team[2]][l];
      display_name+=","
    }
    display_name=display_name.slice(0,-1)+")";
    var team_text=document.createTextNode(display_name);
    team_th.appendChild(team_text);
    create_tr.appendChild(team_th);

    //スコア合計
    var sum_td=document.createElement("td");

    var score_p1=document.createElement("p");
    score_p1.style="text-align: center; margin: 0px;";

    var score_span1=document.createElement("span");
    score_span1.style="color: limegreen; font-weight: bold;";
    var span1_text=document.createTextNode(String(now_team[0]));
    score_span1.appendChild(span1_text);
    score_p1.appendChild(score_span1);

    var score_span2=document.createElement("span");
    score_span2.style="color: red;";
    if(now_team[4]!==0){
      var span2_text=document.createTextNode(" ("+String(now_team[4])+")");
      score_span2.appendChild(span2_text);
    }
    score_p1.appendChild(score_span2);

    sum_td.appendChild(score_p1);

    if(now_team[1]!==0){

    var score_p2=document.createElement("p");
    score_p2.style="text-align: center; margin: 0px;";

    var score_span3=document.createElement("span");
    score_span3.style="color: gray;";
    var time_hour=now_team[1]/3600|0;
    var time_min=(now_team[1]-time_hour*3600)/60|0;
    var time_s=now_team[1]-time_hour*3600-time_min*60;
    var span3_text=document.createTextNode(String(time_hour)+":"+(("00"+String(time_min)).slice(-2))+":"+(("00"+String(time_s)).slice(-2)));//合計はペナ入り時間
    score_span3.appendChild(span3_text);
    score_p2.appendChild(score_span3);

    sum_td.appendChild(score_p2);
    }else{
      var score_p2=document.createElement("p");
      score_p2.style="text-align: center; margin: 0px;";
      var score_span3=document.createElement("span");
      score_span3.style="color: gray;";
      var span3_text=document.createTextNode("-");
      score_span3.appendChild(span3_text);
      score_p2.appendChild(score_span3);

      sum_td.appendChild(score_p2);
    }
    create_tr.appendChild(sum_td);
    //<-スコア

    //各問題の点数
    var now_team_sc=team_sc_data[now_team[2]];
    for(var j=0;j<problem.length;j++){
      var num_td=document.createElement("td");

      var score_p1=document.createElement("p");
      score_p1.style="text-align: center; margin: 0px;";

      var sp_time=now_team_sc[j][0];
      var pn_cnt=now_team_sc[j][1];

      if(sp_time!==-1){
        var score_span1=document.createElement("span");
        score_span1.style="color: limegreen; font-weight: bold;";
        var span1_text=document.createTextNode(String(problem[j]));
        score_span1.appendChild(span1_text);
        score_p1.appendChild(score_span1);

        var score_span2=document.createElement("span");
        score_span2.style="color: red;";
        if(pn_cnt!==0){
          var span2_text=document.createTextNode(" ("+String(pn_cnt)+")");
          score_span2.appendChild(span2_text);
        }
        score_p1.appendChild(score_span2);

        num_td.appendChild(score_p1);

        var score_p2=document.createElement("p");
        score_p2.style="text-align: center; margin: 0px;"

        var score_span3=document.createElement("span");
        score_span3.style="color: gray;";
        var time_hour=sp_time/3600|0;
        var time_min=(sp_time-time_hour*3600)/60|0;
        var time_s=sp_time-time_hour*3600-time_min*60;
        var span3_text=document.createTextNode(String(time_hour)+":"+(("00"+String(time_min)).slice(-2))+":"+(("00"+String(time_s)).slice(-2)));
        score_span3.appendChild(span3_text);
        score_p2.appendChild(score_span3);

        num_td.appendChild(score_p2);
        create_tr.appendChild(num_td);
      }else{//未提出
        num_td.classList.add("text-center");
        var non_text=document.createTextNode("-");
        num_td.appendChild(non_text);
        create_tr.appendChild(num_td);
      }
    }
    main_tbody.appendChild(create_tr);
  }
  main_table.appendChild(main_tbody);
  div2.appendChild(main_table);
  div1.appendChild(div2);
  div_my2.appendChild(div1);

  var before_node=document.getElementsByClassName("my-2");
  before_node[2].parentNode.insertBefore(div_my2,before_node[2].nextElementSibling);
  return;
}

/*GMから設定を読み込む*/
function get_settings() {
  var settings=GM_getValue(contest)
  if(settings===undefined){
    return false;
  }
  var config=settings.split(",");
  for(var i=0;i<config.length;i++){
    var user=config[i].split(":")[0];
    var team=config[i].split(":")[1];
    user_team[user]=team;
    if(user_to_team[team]===undefined){
      user_to_team[team]=[user];
    }else{
      user_to_team[team].push(user);
    }
    teams.add(team);
    users.add(user);
  }
  return true;
}

/*GMに設定を保存する*/
function save_settings() {
  var settings="";
  for(var key in user_team){
    var ret=key+":"+user_team[key]+",";
    settings+=ret;
  }
  settings=settings.slice(0,-1);
  GM_setValue(contest,settings);
  location.reload();//saveしたあとはリロード
  return;
}

/*ファイルを開いて内容をGMに保存する*/
function open_file(evt){
  var reader=new FileReader();
  reader.readAsText(evt.target.files[0]);
  reader.addEventListener("load",()=>{
    var ts=reader.result.replace(/\r?\n/g,"").replace("{","").replace("}","").replace(/\s+/g, "");
    var tx=ts.split(",");
    user_team={};//初期化
    user_to_team={};//初期化
    teams.clear();//初期化
    users.clear();//初期化
    for(var i=0;i<tx.length;i++){
      var te=tx[i].split(":");
      user_team[te[0]]=te[1];
      if(user_to_team[te[1]]===undefined){
        user_to_team[te[1]]=[te[0]];
      }else{
        user_to_team[te[1]].push(te[0]);
      }
      teams.add(te[1]);
      users.add(te[0]);
    }
    save_settings();
    get_score();
    display_score();
  })
  setting_flag=true;
  console.log("setting is saved");
  return;
}

/*ファイルを開くボタンを追加する*/
function add_file_button() {
  var button=document.createElement("input");
  button.id="add_file";
  button.type="file";
  var ref=document.getElementsByTagName("h4")[0];
  var my_parent=ref.parentNode;
  my_parent.appendChild(button);
  button.addEventListener('change',open_file,false);
  console.log("file button added")
  return;
}

/*auto_refreshを削除*/
function remove_auto(){
  var auto_button=document.getElementById("autoRefresh");
  //auto_button.checked=true;
  var auto_label=auto_button.nextElementSibling;
  //auto_label.remove();
  //auto_button.remove();removeすると更新されない
  return;
}

/*pin meを削除*/
function remove_pin(){
  try{
    var pin_me=document.getElementById("pinMe");
    pin_me.checked=false;
    var pin_label=pin_me.nextElementSibling;
    pin_label.remove();
    pin_me.remove();
  }catch(e){
    return;
  }
}

function get_contest_time(){
  var elements_tbody=document.getElementsByTagName("tbody");
  var time_list=elements_tbody[0].children[0].children[1].textContent.split(" ");
  start_time_str=time_list[0]+" "+time_list[1];
  end_time_str=time_list[4]+" "+time_list[5];
  start_time=Date.parse(start_time_str);
  end_time=Date.parse(end_time_str);
  return;
}

function out_file(){
  var file_text=`{"contest":"${contest}","start_time":${start_time},"end_time":${end_time},"penalty_time":${penalty_time},`;
  var problems_text="\"problems\":{";
  for(var i=0; i<problem.length;i++){
    problems_text+=`${i+1}:{"problem_id":"${problem_name[i]}","score":${problem[i]}},`;
  }
  problems_text=problems_text.slice(0,-1)+"}";
  file_text+=problems_text+",";
  var team_text="\"teams\":{";
  for(var key in user_to_team){
    var ret=`"${key}":[`;
    var local_users=user_to_team[key];
    for(var i=0; i<local_users.length;i++){
      ret+=`"${local_users[i]}",`;
    }
    team_text+=ret.slice(0,-1)+"],";
  }
  file_text+=team_text.slice(0,-1)+"}}";
  return file_text;
}

function add_download_button(){
  if(new Date(end_time)-new Date()>0)return;
  var file_text=out_file();
  var file_name=`${contest}_result.json`;
  var download_link=document.createElement("a");
  download_link.href="data:text/plain,"+encodeURIComponent(file_text);
  download_link.download=file_name;
  download_link.textContent="result";
  var ref=document.getElementById("add_file");
  var my_parent=ref.parentNode;
  my_parent.appendChild(download_link);
  console.log("download link is added");
  return;
}

function set_standing_id(){
  standing=document.getElementsByTagName("h3")[1].parentNode.parentNode.parentNode.parentNode.childNodes[1];
  standing.id="personal-standing";
  //console.log(document.getElementsByTagName("h3")[1].parentNode.parentNode.parentNode.parentNode.childNodes[1]);
  return;
}

function get_tr(){
  tr_elements=document.getElementsByTagName("tr");
  return;
}



function main(){
  //コンテスト以外のページの場合は即return
  if(!location.href.match("https://kenkoooo.com/atcoder/#/contest/show/.*")){
    return;
  }
  get_tr();
  set_standing_id();//idを追加
  get_contest();//コンテスト名を取得
  add_file_button();//ファイルを開くボタンを追加
  //remove_auto();//auto_refreshを削除する(確実にバグるので)=>beta版では実装する予定
  remove_pin();//pin_meを削除=>背景色を付ける予定(未定)
  get_contest_time();
  var flags=get_settings();//GM_getValueする
  if(flags){
    get_score();//スコアを取得する
    //display_score();//チームスコアを表示
    add_download_button();
    /*変更感知*/
    var observer=new MutationObserver(()=>{
      //console.log("変更を感知しました!");
      team_standing.parentNode.removeChild(team_standing);
      if(!location.href.match("https://kenkoooo.com/atcoder/#/contest/show/.*")){
        return;
      }
      //setTimeout(()=>{
        problem=[];
        get_score();
        //console.log(user_team,user_to_team,team_sc_data,problem_name);
        //display_score();
      //},5000);
    });
    const config={
      attributes:true,
      childList:true,
      characterData:true,
      subtree:true
    };
    observer.observe(standing,config);
    var page=document.getElementById("root").childNodes[0].childNodes[0];
    //console.log(page);
    var page_observer=new MutationObserver(()=>{
      //console.log("ページ全体の更新");
      //ページ上部の更新を見る(全体だと時間を含むので毎秒確認することになってしまう)
      if(!location.href.match("https://kenkoooo.com/atcoder/#/contest/show/.*")){
        team_standing.parentNode.removeChild(team_standing);
        location.reload();
        return;
      }
    });
    page_observer.observe(page,config);
  }
  return;
}

setTimeout(()=>{
  main();
},5000)