AtCoderStandingsAnalysis

順位表のjsonを集計し、上部にテーブルを追加します。

Från och med 2020-03-22. Se den senaste versionen.

  1. // ==UserScript==
  2. // @name AtCoderStandingsAnalysis
  3. // @namespace https://github.com/RTnF/AtCoderStandingsAnalysis
  4. // @version 0.1.0
  5. // @description 順位表のjsonを集計し、上部にテーブルを追加します。
  6. // @author RTnF
  7. // @match https://atcoder.jp/*standings*
  8. // @exclude https://atcoder.jp/*standings/json
  9. // @grant none
  10. // @license CC0-1.0
  11. // ==/UserScript==
  12.  
  13. // ソート済み配列のうちval未満が何個あるか求める
  14. function countLower(arr, val) {
  15. var lo = -1;
  16. var hi = arr.length;
  17. while (hi - lo > 1) {
  18. var mid = Math.floor((hi + lo) / 2);
  19. if (arr[mid] < val) {
  20. lo = mid;
  21. } else {
  22. hi = mid;
  23. }
  24. }
  25. return hi;
  26. }
  27.  
  28. // 換算: Rating -> innerRating
  29. function innerRating(rate, comp) {
  30. var ret = rate;
  31. if (ret < 400) {
  32. ret = 400 * (1 - Math.log(400 / rate));
  33. }
  34. ret += 1200 * (Math.sqrt(1 - Math.pow(0.81, comp)) / (1 - Math.pow(0.9, comp)) - 1) / (Math.sqrt(19) - 1);
  35. return ret;
  36. }
  37.  
  38. $(function () {
  39. 'use strict';
  40.  
  41. // 表のヘッダーを先頭に追加する
  42. $('#vue-standings').prepend(`
  43. <div>
  44. <table id="acsa-table" class="table table-bordered table-hover th-center td-center td-middle">
  45. <thead>
  46. <tr>
  47. <th>問題</th>
  48. <th>得点</th>
  49. <th>人数</th>
  50. <th>正解率</th>
  51. <th>平均ペナ</th>
  52. <th>ペナ率</th>
  53. <th>レート</th>
  54. </tr>
  55. </thead>
  56. <tbody>
  57. </tbody>
  58. </table>
  59. </div>
  60. `);
  61.  
  62. // 表の更新
  63. setInterval(function () {
  64. var data;
  65. var task = vueStandings.standings.TaskInfo;
  66. if (vueStandings.filtered) {
  67. data = vueStandings.filteredStandings;
  68. } else {
  69. data = vueStandings.standings.StandingsData;
  70. }
  71. const cols = ["#808080", "#804000", "#008000", "#00C0C0", "#0000FF", "#C0C000", "#FF8000", "#FF0000"];
  72. const threshold = [-10000, 400, 800, 1200, 1600, 2000, 2400, 2800];
  73. const canvasWidth = 250;
  74. const canvasHeight = 20;
  75.  
  76. $('#acsa-table > tbody').empty();
  77. for (let i = 0; i < task.length; i++) {
  78. var isTried = vueStandings.tries[i] > 0;
  79. $('#acsa-table > tbody').append(`
  80. <tr>
  81. <td>` + task[i].Assignment + `</td>
  82. <td>-</td>
  83. <td>` + vueStandings.ac[i] + ` / ` + vueStandings.tries[i] + `</td>
  84. <td>` + (isTried ? (vueStandings.ac[i] / vueStandings.tries[i] * 100).toFixed(2) + "%" : "-") + `</td>
  85. <td>-</td>
  86. <td>-</td>
  87. <td><canvas width="` + canvasWidth + `px" height="` + canvasHeight +`px"></canvas></td>
  88. </tr>
  89. `);
  90. if (!isTried) {
  91. continue;
  92. }
  93.  
  94. // トップの得点を満点とみなす
  95. var maxScore = -1;
  96. var myScore = -1;
  97. // 不正解数 / 提出者数
  98. var avePenalty = 0;
  99. // ペナルティ >= 1 の人数 / 提出者数
  100. var ratioPenalty = 0;
  101. var rates = [];
  102. for (let j = 0; j < data.length; j++) {
  103. // 参加登録していない
  104. if (!data[j].TaskResults) {
  105. continue;
  106. }
  107. var result = data[j].TaskResults[task[i].TaskScreenName];
  108. // 未提出のときresult === undefined
  109. if (result) {
  110. if (data[j].UserScreenName === vueStandings.userScreenName) {
  111. myScore = result.Score;
  112. }
  113. // 赤い括弧内の数字
  114. var penalty = result.Score === 0 ? result.Failure : result.Penalty;
  115. avePenalty += penalty;
  116. if (penalty > 0) {
  117. ratioPenalty++;
  118. }
  119. if (maxScore < result.Score) {
  120. maxScore = result.Score;
  121. }
  122. }
  123. }
  124.  
  125. // 正解者の内部レート配列を作成する
  126. for (let j = 0; j < data.length; j++) {
  127. if (data[j].Competitions > 0
  128. && data[j].TaskResults[task[i].TaskScreenName]
  129. && data[j].TaskResults[task[i].TaskScreenName].Score === maxScore) {
  130. rates.push(innerRating(data[j].Rating, data[j].Competitions));
  131. }
  132. }
  133. rates.sort(function (a, b) { return a - b; });
  134.  
  135. myScore /= 100;
  136. maxScore /= 100;
  137. avePenalty /= vueStandings.tries[i];
  138. ratioPenalty /= vueStandings.tries[i];
  139. ratioPenalty *= 100;
  140.  
  141. $('#acsa-table > tbody > tr:eq(' + i + ') > td:eq(1)').text(myScore >= 0 ? myScore.toFixed() : "-");
  142. $('#acsa-table > tbody > tr:eq(' + i + ') > td:eq(4)').text(avePenalty.toFixed(2));
  143. $('#acsa-table > tbody > tr:eq(' + i + ') > td:eq(5)').text(ratioPenalty.toFixed(2) + "%");
  144. var canvas = $('#acsa-table > tbody > tr:eq(' + i + ') > td:eq(6) > canvas')[0];
  145. if (canvas.getContext) {
  146. var context = canvas.getContext('2d');
  147. for (let k = 0; k < 8; k++) {
  148. context.fillStyle = cols[k];
  149. // 色の境界から右端までの矩形描画
  150. var x = Math.round(countLower(rates, threshold[k]) / rates.length * canvasWidth);
  151. context.fillRect(x, 0, canvasWidth - x, canvasHeight);
  152. }
  153. }
  154. }
  155. }, 5000);
  156. });