AtCoderStandingsAnalysis

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

Pada tanggal 22 Maret 2020. Lihat %(latest_version_link).

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