// ==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)