├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── authenticate.js ├── cron ├── add_cook_off.js ├── add_long.js └── add_lunchtime.js ├── generate.js ├── helper.js ├── package.json ├── public ├── css │ ├── landing.css │ └── rating.css └── js │ ├── landing.js │ └── rating.js ├── server.js ├── status.js ├── verify.js └── views ├── error.ejs ├── landing.ejs ├── landing_down.ejs └── rating.ejs /.gitattributes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dush1729/codechef-rating-predictor/94e1f06e85985a0ed2f6a0ea8862b8f4ef6dd863/.gitattributes -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | cache 3 | process.txt 4 | .vscode/ 5 | .gitignore 6 | app-env -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 vsp4 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # codechef-rating-predictor 2 | A script that predicts rating of contests at codechef 3 | ## Project link - [https://ccpredict.herokuapp.com/](https://ccpredict.herokuapp.com/) -------------------------------------------------------------------------------- /authenticate.js: -------------------------------------------------------------------------------- 1 | var axios = require('axios'); 2 | var data = null 3 | var lastFetched = null 4 | 5 | module.exports.getBearer = function (callback) { 6 | var time = new Date().getTime() / 1000 7 | if (!lastFetched || time - lastFetched >= data["result"]["data"]["expires in"]) { 8 | axios.post("https://api.codechef.com/oauth/token", 9 | { "grant_type": "client_credentials", "scope": "public", "client_id": process.env.CLIEND_ID, "client_secret": process.env.CLIENT_SECRET }) 10 | .then(function (response) { 11 | data = response.data 12 | lastFetched = new Date().getTime() / 1000 13 | callback(null, response.data) 14 | }) 15 | .catch(function (error) { 16 | callback(error, null) 17 | }); 18 | } else { 19 | callback(null, data) 20 | } 21 | } -------------------------------------------------------------------------------- /cron/add_cook_off.js: -------------------------------------------------------------------------------- 1 | // Run at 15:30 GMT every Sunday (30 15 * * 0) 2 | exports = function () { 3 | var date = new Date(); 4 | date.setHours(date.getHours() + 5); 5 | date.setMinutes(date.getMinutes() + 30); 6 | const currentMonth = date.getMonth(); 7 | date.setDate(date.getDate() - 2); 8 | var fridaysPassed = 0; 9 | while (date.getMonth() == currentMonth) { 10 | fridaysPassed++; 11 | date.setDate(date.getDate() - 7); 12 | } 13 | if (fridaysPassed != 3) return; 14 | 15 | date = new Date(); 16 | date.setHours(date.getHours() + 5); 17 | date.setMinutes(date.getMinutes() + 30); 18 | const year = date.getFullYear(); 19 | const month = date.getMonth(); 20 | const monthsPassed = (2019 - year) * 12 + month; 21 | 22 | console.log("Adding: Cookoff at " + new Date()); 23 | const db = context.services.get("codechef-rating-predictor").db("test"); 24 | db.collection("cache").deleteMany({}); 25 | db.collection("checklist").deleteMany({}); 26 | db.collection("data").deleteMany({}); 27 | db.collection("lastupdate").deleteMany({}); 28 | db.collection("status").deleteMany({}); 29 | db.collection("user").deleteMany({}); 30 | 31 | const cookOffCode = monthsPassed + 102; 32 | db.collection("checklist").insertMany([ 33 | { 34 | "contest": "COOK" + cookOffCode + "A", 35 | "parse": ["all", "short"] 36 | }, 37 | { 38 | "contest": "COOK" + cookOffCode + "B", 39 | "parse": ["all", "short"] 40 | }]); 41 | 42 | console.log("Added: Cookoff at " + new Date()); 43 | }; -------------------------------------------------------------------------------- /cron/add_long.js: -------------------------------------------------------------------------------- 1 | // Run at 09:00 GMT every Friday (0 9 * * 0) 2 | exports = function () { 3 | var date = new Date(); 4 | date.setHours(date.getHours() + 5); 5 | date.setMinutes(date.getMinutes() + 30); 6 | const currentMonth = date.getMonth(); 7 | var fridaysPassed = 0; 8 | while (date.getMonth() == currentMonth) { 9 | fridaysPassed++; 10 | date.setDate(date.getDate() - 7); 11 | } 12 | if (fridaysPassed != 1) return; 13 | 14 | date = new Date(); 15 | date.setHours(date.getHours() + 5); 16 | date.setMinutes(date.getMinutes() + 30); 17 | const year = date.getFullYear(); 18 | const month = date.getMonth(); 19 | 20 | console.log("Adding: Long at " + new Date()); 21 | const db = context.services.get("codechef-rating-predictor").db("test"); 22 | db.collection("cache").deleteMany({}); 23 | db.collection("checklist").deleteMany({}); 24 | db.collection("data").deleteMany({}); 25 | db.collection("lastupdate").deleteMany({}); 26 | db.collection("status").deleteMany({}); 27 | db.collection("user").deleteMany({}); 28 | 29 | const longCode = [ 30 | "JAN", "FEB", "MARCH", "APRIL", "MAY", "JUNE", 31 | "JULY", "AUG", "SEPT", "OCT", "NOV", "DEC" 32 | ]; 33 | db.collection("checklist").insertMany([ 34 | { 35 | "contest": longCode[month] + year.toString().substring(2) + "A", 36 | "parse": ["all", "long"] 37 | }, 38 | { 39 | "contest": longCode[month] + year.toString().substring(2) + "B", 40 | "parse": ["all", "long"] 41 | }]); 42 | 43 | console.log("Added: Long at " + new Date()); 44 | }; -------------------------------------------------------------------------------- /cron/add_lunchtime.js: -------------------------------------------------------------------------------- 1 | // Run at 13:30 GMT every Saturday (30 13 * * 6) 2 | exports = function () { 3 | var date = new Date(); 4 | date.setHours(date.getHours() + 5); 5 | date.setMinutes(date.getMinutes() + 30); 6 | const currentMonth = date.getMonth(); 7 | var saturdayRemaining = 0; 8 | while (date.getMonth() == currentMonth) { 9 | saturdayRemaining++; 10 | date.setDate(date.getDate() + 7); 11 | } 12 | if (saturdayRemaining != 1) return; 13 | 14 | date = new Date(); 15 | date.setHours(date.getHours() + 5); 16 | date.setMinutes(date.getMinutes() + 30); 17 | const year = date.getFullYear(); 18 | const month = date.getMonth(); 19 | const monthsPassed = (2019 - year) * 12 + month; 20 | 21 | console.log("Adding: Lunchtime at " + new Date()); 22 | const db = context.services.get("codechef-rating-predictor").db("test"); 23 | db.collection("cache").deleteMany({}); 24 | db.collection("checklist").deleteMany({}); 25 | db.collection("data").deleteMany({}); 26 | db.collection("lastupdate").deleteMany({}); 27 | db.collection("status").deleteMany({}); 28 | db.collection("user").deleteMany({}); 29 | 30 | const lunchtimeCode = monthsPassed + 68; 31 | db.collection("checklist").insertMany([ 32 | { 33 | "contest": "LTIME" + lunchtimeCode + "A", 34 | "parse": ["all", "ltime"] 35 | }, 36 | { 37 | "contest": "LTIME" + lunchtimeCode + "B", 38 | "parse": ["all", "ltime"] 39 | }]); 40 | 41 | console.log("Added: Lunchtime at " + new Date()); 42 | }; -------------------------------------------------------------------------------- /generate.js: -------------------------------------------------------------------------------- 1 | var http = require("http"); 2 | var https = require("https"); 3 | var util = require("util"); 4 | var cheerio = require("cheerio"); 5 | var fs = require("fs"); 6 | var path = require("path"); 7 | var async = require("async"); 8 | var authenticate = require("./authenticate.js"); 9 | 10 | var cacheDir = "cache"; 11 | var parseTypes = ["all", "long", "short", "ltime"]; //date_versus_rating_all"];//, "date_versus_rating_long", "date_versus_rating_short", "date_versus_rating_ltime"]; 12 | 13 | var rankData = {}; 14 | var originalData = {}; 15 | var lastRank; 16 | 17 | var contestid; 18 | 19 | var usercollection; 20 | var datacollection; 21 | var lastupdatecollection; 22 | var cachecollection; 23 | 24 | function getCachedResponseUser(user, func, usecache, trycount) { 25 | cachecollection.findOne({ contest: contestid, user: user }, function (err, res) { 26 | if (res && usecache) { 27 | var jsonobj = res.data; 28 | func(jsonobj); 29 | } 30 | else { 31 | var url = util.format('https://www.codechef.com/users/%s', user); 32 | execHttps(url, function (source) { 33 | var doupdate = true; 34 | 35 | if (source.indexOf("date_versus_rating") == -1) { 36 | if (source.indexOf("Could not find page you requested for") != -1) { 37 | //codechef bug 38 | //continue on 39 | doupdate = false; 40 | console.log("Error in parsing user page, codechef user " + user); 41 | } 42 | else { 43 | if (trycount == 0) { 44 | doupdate = false; 45 | console.log("Error in cached response, Exceeded try counts " + user); 46 | } 47 | else { 48 | //bad response 49 | getCachedResponseUser(user, func, usecache, trycount - 1); 50 | return; 51 | } 52 | } 53 | } 54 | 55 | var jsonobj = {}; 56 | 57 | { 58 | var search = "\"date_versus_rating\":"; 59 | var index = source.indexOf(search); 60 | 61 | if (index != -1) { 62 | index += search.length; 63 | var endindex = source.indexOf("});", index); 64 | jsonobj = JSON.parse(source.substring(index, endindex)); 65 | } 66 | } 67 | 68 | if (doupdate) { 69 | cachecollection.update({ contest: contestid, user: user }, { contest: contestid, user: user, data: jsonobj }, { upsert: true }, function () { 70 | func(jsonobj); 71 | }); 72 | } 73 | else { 74 | func(jsonobj); 75 | } 76 | }, 4); 77 | } 78 | }); 79 | } 80 | 81 | var time = 0; 82 | 83 | function getVolatility(userdata, callback, retry) { 84 | retry = (typeof retry === 'undefined') ? true : retry; 85 | 86 | getCachedResponseUser(userdata.user, function (obj) { 87 | var volObj = {}; 88 | var timesObj = {}; 89 | var lastAllRatingObj = {}; 90 | var currentRatingObj = {}; 91 | var isempty = true; 92 | 93 | for (var i in parseTypes) { 94 | var timesPlayed = 0; 95 | var Ra = 1500; 96 | var Va = 125; 97 | 98 | for (var j in obj[parseTypes[i]]) { 99 | var currContest = obj[parseTypes[i]][j]; 100 | 101 | if (currContest.code == contestid) { 102 | currentRatingObj[parseTypes[i]] = parseInt(currContest.rating); 103 | break; 104 | } 105 | 106 | if (currContest.code == "") { 107 | //stupid late bug 108 | continue; 109 | } 110 | 111 | isempty = false; 112 | 113 | var VWa = (0.5 * timesPlayed + 0.8) / (timesPlayed + 0.6); 114 | var NRa = parseInt(currContest.rating); 115 | 116 | Va = Math.sqrt(1.0 * (VWa * (NRa - Ra) * (NRa - Ra) + Va * Va) / (VWa + 1.1)); 117 | Ra = parseInt(NRa); 118 | timesPlayed++; 119 | 120 | Va = Math.max(Va, 75); 121 | Va = Math.min(Va, 200); 122 | } 123 | 124 | lastAllRatingObj[parseTypes[i]] = Ra; 125 | volObj[parseTypes[i]] = Va; 126 | timesObj[parseTypes[i]] = timesPlayed; 127 | } 128 | 129 | originalData[userdata.user] = currentRatingObj; 130 | 131 | if (!(userdata.user in rankData)) { 132 | rankData[userdata.user] = { rank: lastRank, handle: userdata.user }; 133 | } 134 | else { 135 | if ((rankData[userdata.user].oldrating > 0) && (rankData[userdata.user].oldrating != 1500) && isempty && retry) { 136 | console.log("Mismatch, need to recalculate", userdata.user, rankData[userdata.user].oldrating); 137 | getVolatility(userdata, callback, false); 138 | return; 139 | } 140 | } 141 | 142 | rankData[userdata.user].rating = lastAllRatingObj; 143 | rankData[userdata.user].volatility = volObj; 144 | rankData[userdata.user].times = timesObj; 145 | 146 | //console.log("Volatility generated for ", userdata.user); 147 | 148 | setImmediate(callback); 149 | }, retry, 2); 150 | } 151 | 152 | function generateVolatility(callback) { 153 | usercollection.find({ contestid: contestid }).toArray(function (err, data) { 154 | console.log("Generating volatility", new Date().toString()); 155 | 156 | async.eachLimit(data, 5, getVolatility, function (err) { 157 | if (err) { 158 | console.log("ERROR Volatility generation: " + err); 159 | } 160 | callback(); 161 | }); 162 | }); 163 | } 164 | 165 | function generateRanklist(contestid, pageno, func) { 166 | var url = util.format('https://api.codechef.com/rankings/%s?fields=username%2Crank%2Crating&offset=%s', contestid, pageno * 1500); 167 | 168 | /* 169 | //for debugging 170 | var filepath = path.resolve(path.join(cacheDir, pageno + ".json")); 171 | if (fs.existsSync(filepath)) 172 | { 173 | var source = fs.readFileSync(filepath).toString(); 174 | 175 | var obj = JSON.parse(source); 176 | 177 | obj.list.forEach(function(data) 178 | { 179 | rankData[data.user_handle] = {rank: data.rank, handle: data.user_handle, oldrating: data.rating}; 180 | try 181 | { 182 | usercollection.insert({contestid: contestid, user: data.user_handle}); 183 | } 184 | catch (ex) 185 | { 186 | } 187 | }); 188 | 189 | var lastpage = obj.availablePages; 190 | 191 | if (pageno < lastpage) 192 | { 193 | setImmediate(generateRanklist, contestid, pageno+1, func); 194 | } 195 | else 196 | { 197 | func(); 198 | } 199 | } 200 | else 201 | { 202 | //fs.writeFile(filepath, source, 'utf8'); 203 | } 204 | */ 205 | authenticate.getBearer((err, result) => { 206 | if (err) { 207 | console.log(err) 208 | } else { 209 | var accessToken = result["result"]["data"]["access_token"] 210 | execHttps(url, function (source) { 211 | if (source.indexOf("unable to fetch rank list") != -1) { 212 | func(); 213 | return; 214 | } else if (source.indexOf("Rank List successfully fetched.") == -1) { 215 | generateRanklist(contestid, pageno, func); 216 | return; 217 | } 218 | 219 | var obj = JSON.parse(source).result.data.content; 220 | 221 | obj.forEach(function (data) { 222 | rankData[data.username] = { rank: parseInt(data.rank), handle: data.username, oldrating: data.rating }; 223 | try { 224 | usercollection.update( 225 | { contestid: contestid, user: data.username }, 226 | { contestid: contestid, user: data.username }, 227 | { upsert: true }); 228 | } 229 | catch (ex) { 230 | } 231 | //console.log(data.user_handle, data.rank, data.rating); 232 | }); 233 | 234 | setImmediate(generateRanklist, contestid, pageno + 1, func); 235 | }, 4, accessToken); 236 | } 237 | }) 238 | } 239 | 240 | function calculateRating(callback) { 241 | var N = Object.keys(rankData).length; 242 | 243 | console.log("Calculating rating", new Date().toString()); 244 | 245 | var rankKeys = Object.keys(rankData); 246 | var log4 = Math.log(4); 247 | 248 | async.each(parseTypes, function (type, cbnext) { 249 | var beforemeasure = new Date(); 250 | 251 | console.log("Calculating rating", contestid, type, new Date().toString()); 252 | 253 | var readabletype = type.replace("date_versus_rating_", ""); 254 | 255 | var countRank = new Array(N + 5).fill(0); 256 | var VASquaresum = 0; 257 | var ratingSum = 0; 258 | 259 | rankKeys.forEach(function (key) { 260 | countRank[rankData[key].rank]++; 261 | VASquaresum += rankData[key].volatility[type] * rankData[key].volatility[type]; 262 | ratingSum += rankData[key].rating[type]; 263 | }); 264 | 265 | var Ravg = ratingSum / N; 266 | var ratingDiffSquare = 0; 267 | 268 | rankKeys.forEach(function (key) { 269 | ratingDiffSquare += (rankData[key].rating[type] - Ravg) * (rankData[key].rating[type] - Ravg); 270 | }); 271 | 272 | var Cf = Math.sqrt(VASquaresum / N + ratingDiffSquare / (N - 1)); 273 | 274 | var dataInsertions = []; 275 | 276 | var RVList = []; 277 | 278 | for (var i = 0; i < rankKeys.length; i++) { 279 | var Vb = rankData[rankKeys[i]].volatility[type]; 280 | RVList[i] = [rankData[rankKeys[i]].rating[type], Vb * Vb]; 281 | } 282 | 283 | async.eachLimit(rankData, 5, function (curr, nextcalculatecallback) { 284 | var Ra = curr.rating[type]; 285 | var RWa = (0.4 * curr.times[type] + 0.2) / (0.7 * curr.times[type] + 0.6); 286 | var Va = curr.volatility[type]; 287 | 288 | var add = countRank[curr.rank] / 2; 289 | 290 | var APerf = Math.log(N / (curr.rank - 1 + add) - 1) / log4; 291 | 292 | /* 293 | if (curr.rank == 1) 294 | { 295 | add = 1/2; 296 | } 297 | */ 298 | 299 | var EPerf = 0; 300 | 301 | var VaSq = Va * Va; 302 | 303 | RVList.forEach(function (key) { 304 | //var Rb = key[0]; //rankData[key].rating[type]; 305 | //var Vb = key[1]; //rankData[key].volatility[type]; 306 | EPerf += 1 / (1 + Math.pow(4, (Ra - key[0]) / Math.sqrt(VaSq + key[1]))); 307 | }); 308 | 309 | //var tEPerf = Math.log(N/EPerf - 1)/Math.log(4); 310 | 311 | var ECPerf = Math.log((N / (curr.rank - 1 + add) - 1) / (N / EPerf - 1)); 312 | 313 | var NRa = Ra + ECPerf * Cf * RWa / log4; 314 | 315 | var maxChange = 100 + 75 / (curr.times[type] + 1) + (100 * 500) / (Math.abs(Ra - 1500) + 500); 316 | 317 | if (Math.abs(NRa - Ra) > maxChange) { 318 | if (NRa > Ra) { 319 | NRa = Ra + maxChange; 320 | } 321 | else { 322 | NRa = Ra - maxChange; 323 | } 324 | } 325 | 326 | NRa = Math.ceil(NRa); 327 | 328 | if (isNaN(NRa)) { 329 | console.log(N, curr.rank, add, maxChange, Ra, tempPerf, Cf, RWa, countRank[curr.rank]); 330 | } 331 | 332 | var data = { 333 | contest: contestid, 334 | type: readabletype, 335 | user: curr.handle 336 | }; 337 | data.rank = curr.rank; 338 | data.previous = curr.rating[type]; 339 | data.rating = NRa; 340 | data.erank = Math.floor(EPerf); 341 | 342 | dataInsertions.push(data); 343 | 344 | setImmediate(nextcalculatecallback); 345 | 346 | /* 347 | Debugging 348 | if (originalData[curr.handle][type] != undefined) 349 | { 350 | console.log(curr.handle, curr.rating[type], Math.ceil(NRa), originalData[curr.handle][type], Math.abs(originalData[curr.handle][type] - Math.ceil(NRa))); 351 | } 352 | else 353 | { 354 | console.log(user, curr.rank, maxChange, NRa, Ra, ECPerf, RWa); 355 | } 356 | */ 357 | }, 358 | function (err) { 359 | datacollection.deleteMany({ contest: contestid, type: readabletype }, function (err) { 360 | console.log("Deleted previous records for ", contestid, readabletype); 361 | 362 | if (dataInsertions.length > 0) { 363 | datacollection.insertMany(dataInsertions, function (err) { 364 | console.log("Inserted new records for ", contestid, readabletype); 365 | 366 | var aftermeasure = new Date(); 367 | 368 | time += aftermeasure - beforemeasure; 369 | console.log(beforemeasure.toString(), aftermeasure.toString()); 370 | 371 | setImmediate(cbnext); 372 | }); 373 | } 374 | else { 375 | console.log("Empty records for ", contestid, readabletype); 376 | setImmediate(cbnext); 377 | } 378 | }); 379 | }); 380 | }, 381 | function (err) { 382 | lastupdatecollection.update({ contest: contestid }, { contest: contestid, date: new Date() }, { upsert: true }, function () { 383 | setImmediate(callback); 384 | }); 385 | }); 386 | } 387 | 388 | require("./helper.js")(); 389 | 390 | module.exports = function (nextcall) { 391 | MongoClient.connect(mongourl, function (err, db) { 392 | if (err) { 393 | throw err; 394 | } 395 | 396 | /* 397 | if (!fs.existsSync(cacheDir)) 398 | { 399 | fs.mkdirSync(cacheDir); 400 | } 401 | */ 402 | 403 | usercollection = db.collection("user"); 404 | datacollection = db.collection("data"); 405 | lastupdatecollection = db.collection("lastupdate"); 406 | cachecollection = db.collection("cache"); 407 | 408 | var contestLists = []; 409 | 410 | var processContests = function () { 411 | time = 0; 412 | 413 | async.eachSeries(contestLists, function (ciid, callback) { 414 | rankData = {}; 415 | originalData = {}; 416 | lastRank = 0; 417 | contestid = ciid.contest; 418 | parseTypes = ciid.parse; 419 | 420 | /* 421 | if (!fs.existsSync(path.join(cacheDir, contestid))) 422 | { 423 | fs.mkdirSync(path.join(cacheDir, contestid)); 424 | } 425 | */ 426 | 427 | generateRanklist(contestid, 0, function () { 428 | lastRank = Object.keys(rankData).length + 1; 429 | generateVolatility(function () { 430 | calculateRating(function () { 431 | console.log("Completed", contestid); 432 | callback(); 433 | }); 434 | }); 435 | }); 436 | }, 437 | function (err) { 438 | if (err) 439 | console.log("Error", err); 440 | 441 | db.close(); 442 | console.log("Completed ALL", time); 443 | 444 | setImmediate(nextcall); 445 | }); 446 | }; 447 | 448 | db.collection("checklist").find({}).toArray(function (err, cdatas) { 449 | if (cdatas) { 450 | cdatas.forEach(function (x) { 451 | contestLists.push(x); 452 | }); 453 | } 454 | 455 | processContests(); 456 | }); 457 | 458 | }); 459 | }; 460 | -------------------------------------------------------------------------------- /helper.js: -------------------------------------------------------------------------------- 1 | var http = require("http"); 2 | var https = require("https"); 3 | var requrl = require("url"); 4 | 5 | module.exports = function () { 6 | this.execHttps = function (url, func, retry, accessToken) { 7 | console.log("Parsing", url, retry); 8 | var httpobj = http; 9 | 10 | if (url.startsWith("https://")) { 11 | httpobj = https; 12 | } 13 | 14 | var bearer = "Bearer " + accessToken 15 | const parsedURL = requrl.parse(url); 16 | const options = { 17 | protocol: parsedURL.protocol, 18 | hostname: parsedURL.hostname, 19 | path: parsedURL.path, 20 | headers: { 'User-Agent': 'Mozilla/5.0', 'Authorization': bearer } 21 | }; 22 | 23 | var source = ""; 24 | httpobj.get(options, function (res) { 25 | res.setEncoding("utf8"); 26 | 27 | res.on("data", function (data) { 28 | source += data; 29 | }); 30 | 31 | res.on("end", function () { 32 | if (source.indexOf("Server cannot process your request") != -1) { 33 | if (retry == 0) { 34 | func(""); 35 | } 36 | else { 37 | //bad server 38 | execHttps(url, func, retry - 1); 39 | } 40 | } 41 | else { 42 | func(source); 43 | } 44 | }); 45 | }).on("error", function (err) { 46 | console.log(err); 47 | setTimeout(function () { 48 | func(source) 49 | }, 1000); 50 | }); 51 | } 52 | 53 | this.MongoClient = require('mongodb').MongoClient; 54 | this.mongourl = "mongodb://localhost:27017/codechefratingpredictor"; 55 | 56 | //openshift configuration 57 | if (process.env.MONGODB_PASSWORD) { 58 | this.mongourl = "mongodb://" + process.env.MONGODB_USER + ":" + process.env.MONGODB_PASSWORD + "@" + 59 | process.env.MONGODB_SERVICE_HOST 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codechef-rating-predictor", 3 | "version": "1.0.0", 4 | "description": "A script that predicts rating of contests at codechef", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 0", 8 | "start": "node server.js" 9 | }, 10 | "keywords": [ 11 | "rating", 12 | "elo", 13 | "codechef" 14 | ], 15 | "engine": { 16 | "node": "4.*", 17 | "npm": "*" 18 | }, 19 | "author": "vsp4 ", 20 | "license": "MIT", 21 | "dependencies": { 22 | "async": "^2.4.1", 23 | "axios": "^0.19.0", 24 | "cheerio": "^0.22.0", 25 | "cookie-parser": "^1.4.4", 26 | "cron": "^1.7.1", 27 | "ejs": "^2.6.1", 28 | "express": "^4.17.1", 29 | "mongodb": "^2.2.36" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /public/css/landing.css: -------------------------------------------------------------------------------- 1 | h1, h2, h3, h4, h5, h6, 2 | .h1, .h2, .h3, .h4, .h5, .h6 { 3 | margin-bottom: 0.5rem; 4 | font-family: inherit; 5 | font-weight: 500; 6 | line-height: 1.2; 7 | color: white; 8 | } 9 | 10 | html, body { 11 | background: #006666; /* fallback for old browsers */ 12 | background: -webkit-linear-gradient(to right, #000000, #006666); /* Chrome 10-25, Safari 5.1-6 */ 13 | background: linear-gradient(to right, #000000, #006666); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */ 14 | height: 100%; 15 | } 16 | 17 | .card { 18 | background-color: #32383e; 19 | } -------------------------------------------------------------------------------- /public/css/rating.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: #fff; 3 | background: #1e1e24; 4 | } 5 | 6 | .table .thead-dark th { 7 | color: #fff; 8 | background-color: #292721; 9 | border-color: #32383e; 10 | } 11 | 12 | .bg-primary { 13 | background-color: #0e4b8d !important; 14 | } 15 | 16 | .bg-success { 17 | background-color: #096b32 !important; 18 | } 19 | 20 | .bg-danger { 21 | background-color: #a10c1b !important; 22 | } 23 | 24 | .page-link { 25 | background-color: #292721; 26 | border-color: #32383e; 27 | } 28 | 29 | .page-link:hover { 30 | background-color: darkgray; 31 | } 32 | 33 | .card { 34 | background-color: #32383e; 35 | } -------------------------------------------------------------------------------- /public/js/landing.js: -------------------------------------------------------------------------------- 1 | function createCookie(name, value, days) { 2 | var expires = ""; 3 | if (days) { 4 | var date = new Date(); 5 | date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); 6 | expires = "; expires=" + date.toUTCString(); 7 | } 8 | document.cookie = name + "=" + (value || "") + expires + "; path=/"; 9 | } 10 | 11 | function getCookie(c_name) { 12 | if (document.cookie.length > 0) { 13 | c_start = document.cookie.indexOf(c_name + "="); 14 | if (c_start != -1) { 15 | c_start = c_start + c_name.length + 1; 16 | c_end = document.cookie.indexOf(";", c_start); 17 | if (c_end == -1) { 18 | c_end = document.cookie.length; 19 | } 20 | return unescape(document.cookie.substring(c_start, c_end)); 21 | } 22 | } 23 | return 0; 24 | } 25 | 26 | function getUrl() { 27 | const longCode = [ 28 | "JAN", "FEB", "MARCH", "APRIL", "MAY", "JUNE", 29 | "JULY", "AUG", "SEPT", "OCT", "NOV", "DEC"]; 30 | const ratingTypes = ["", "long", "short", "ltime"]; 31 | 32 | var url = "http://ccpredict.herokuapp.com/contest/"; 33 | 34 | var contestType = document.getElementById("contestType").value; 35 | var contestDivision = document.getElementById("contestDivision").value; 36 | var ratingType = document.getElementById("ratingType").value; 37 | 38 | const date = new Date(); 39 | const year = date.getFullYear(); 40 | const month = date.getMonth(); 41 | const monthsPassed = (2019 - year) * 12 + month; 42 | 43 | if (contestType == 1) { 44 | url += longCode[month] + year.toString().substring(2); 45 | } else if (contestType == 2) { 46 | url += "COOK" + (monthsPassed + 102); 47 | } else { 48 | url += "LTIME" + (monthsPassed + 68); 49 | } 50 | 51 | if (contestDivision == 1) { 52 | url += "A"; 53 | } 54 | else { 55 | url += "B"; 56 | } 57 | 58 | if (ratingType == 1) { 59 | url += "/all"; 60 | } else { 61 | url += "/" + ratingTypes[contestType]; 62 | } 63 | 64 | return url; 65 | } 66 | 67 | document.getElementById("contestType").selectedIndex = getCookie("contestType"); 68 | document.getElementById("contestDivision").selectedIndex = getCookie("contestDivision"); 69 | document.getElementById("ratingType").selectedIndex = getCookie("ratingType"); 70 | document.getElementById("predictButton").setAttribute("href", getUrl()); 71 | 72 | function predictButtonClick() { 73 | createCookie("contestType", document.getElementById("contestType").selectedIndex, 30) 74 | createCookie("contestDivision", document.getElementById("contestDivision").selectedIndex, 30) 75 | createCookie("ratingType", document.getElementById("ratingType").selectedIndex, 30) 76 | document.getElementById("predictButton").setAttribute("href", getUrl()) 77 | } 78 | 79 | document.getElementById("predictButton").onclick = function () { 80 | predictButtonClick() 81 | } 82 | 83 | //for middle button click 84 | document.getElementById("predictButton").addEventListener('auxclick', function (event) { 85 | predictButtonClick() 86 | }) 87 | 88 | document.getElementById("fetchButton").onclick = function () { 89 | saveUserAndRefresh() 90 | document.getElementById("form").submit() 91 | } 92 | 93 | function saveUserAndRefresh() { 94 | createCookie("user", document.getElementById("user").value, 365) 95 | window.location.reload() 96 | } -------------------------------------------------------------------------------- /public/js/rating.js: -------------------------------------------------------------------------------- 1 | function createCookie(name, value, days) { 2 | var expires = ""; 3 | if (days) { 4 | var date = new Date(); 5 | date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); 6 | expires = "; expires=" + date.toUTCString(); 7 | } 8 | document.cookie = name + "=" + (value || "") + expires + "; path=/"; 9 | } 10 | 11 | 12 | function getCookie(c_name) { 13 | if (document.cookie.length > 0) { 14 | c_start = document.cookie.indexOf(c_name + "="); 15 | if (c_start != -1) { 16 | c_start = c_start + c_name.length + 1; 17 | c_end = document.cookie.indexOf(";", c_start); 18 | if (c_end == -1) { 19 | c_end = document.cookie.length; 20 | } 21 | return unescape(document.cookie.substring(c_start, c_end)); 22 | } 23 | } 24 | return dark; 25 | } 26 | 27 | const dark = "/css/rating.css"; 28 | const light = "https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"; 29 | 30 | var link = document.getElementById("theme"); 31 | document.getElementById("themeButton").onclick = function () { 32 | var newTheme; 33 | if (link.getAttribute("href") == dark) { 34 | newTheme = light; 35 | } else { 36 | newTheme = dark; 37 | } 38 | link.setAttribute("href", newTheme); 39 | createCookie("theme", newTheme, 365); 40 | } -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var http = require("http"); 2 | var https = require("https"); 3 | var util = require("util"); 4 | var cheerio = require("cheerio"); 5 | var express = require("express"); 6 | var app = express(); 7 | var cookieParser = require('cookie-parser'); 8 | var cronJob = require('cron').CronJob; 9 | 10 | var isWorking = false; 11 | new cronJob('*/10 * * * *', function () { 12 | if (isWorking) { 13 | return; 14 | } 15 | isWorking = true; 16 | var status = require("./status.js"); 17 | var generator = require("./generate.js"); 18 | status(function () { 19 | generator(function () { 20 | isWorking = false; 21 | }) 22 | }); 23 | }, null, true); 24 | 25 | app.set('view engine', 'ejs'); 26 | app.use(express.static(__dirname + '/public')); 27 | app.use(cookieParser()); 28 | 29 | //from snipplr 30 | function elapsedTime(createdAt) { 31 | var ageInSeconds = (new Date().getTime() - new Date(createdAt).getTime()) / 1000; 32 | var s = function (n) { return n == 1 ? '' : 's' }; 33 | if (ageInSeconds < 0) { 34 | return 'just now'; 35 | } 36 | if (ageInSeconds < 60) { 37 | var n = ageInSeconds; 38 | return n + ' second' + s(n) + ' ago'; 39 | } 40 | if (ageInSeconds < 60 * 60) { 41 | var n = Math.floor(ageInSeconds / 60); 42 | return n + ' minute' + s(n) + ' ago'; 43 | } 44 | if (ageInSeconds < 60 * 60 * 24) { 45 | var n = Math.floor(ageInSeconds / 60 / 60); 46 | return n + ' hour' + s(n) + ' ago'; 47 | } 48 | if (ageInSeconds < 60 * 60 * 24 * 7) { 49 | var n = Math.floor(ageInSeconds / 60 / 60 / 24); 50 | return n + ' day' + s(n) + ' ago'; 51 | } 52 | if (ageInSeconds < 60 * 60 * 24 * 31) { 53 | var n = Math.floor(ageInSeconds / 60 / 60 / 24 / 7); 54 | return n + ' week' + s(n) + ' ago'; 55 | } 56 | if (ageInSeconds < 60 * 60 * 24 * 365) { 57 | var n = Math.floor(ageInSeconds / 60 / 60 / 24 / 31); 58 | return n + ' month' + s(n) + ' ago'; 59 | } 60 | var n = Math.floor(ageInSeconds / 60 / 60 / 24 / 365); 61 | return n + ' year' + s(n) + ' ago'; 62 | } 63 | 64 | //https://stackoverflow.com/a/40890687/5258585 65 | function filterIt(arr, searchKey) { 66 | var matches = arr.filter(obj => Object.keys(obj).some(key => obj[key].toString().includes(searchKey))) 67 | if (matches.length == 0) { 68 | return "" 69 | } else { 70 | return elapsedTime(matches[0].date); 71 | } 72 | } 73 | 74 | require("./helper.js")(); 75 | 76 | MongoClient.connect(mongourl, function (err, db) { 77 | if (err) { 78 | throw err; 79 | } 80 | 81 | var datacollection = db.collection("data"); 82 | var lastupdatecollection = db.collection("lastupdate"); 83 | var checklist = db.collection("checklist"); 84 | 85 | app.get('/', function (req, res) { 86 | var user = (req.cookies.user ? req.cookies.user : ""); 87 | 88 | var result = []; 89 | datacollection.find({ user: user }).sort({ contest: 1, type: 1 }).toArray().then(predictions => { 90 | result = predictions; 91 | }).then(() => { 92 | return Promise.resolve(lastupdatecollection.find().toArray()); 93 | }).then(lastUpdated => { 94 | result.forEach(element => { 95 | element.elapsed = filterIt(lastUpdated, element.contest) 96 | }); 97 | }).then(() => { 98 | res.render('landing_down', { result: result, user: user }); 99 | }).catch(e => { 100 | console.error(e); 101 | }); 102 | }) 103 | 104 | app.get('/add/:contest', function (req, res) { 105 | res.render("error", { message: "Sorry, Adding new contest is disabled." }) 106 | /*var cid = req.params.contest; 107 | checklist.findOneAndReplace( 108 | { 109 | contest: cid, 110 | parse: ['all'] 111 | }, 112 | { 113 | contest: cid, 114 | parse: ['all'] 115 | }, 116 | { 117 | upsert: true 118 | }, 119 | function (err, result) { 120 | if (result) { 121 | res.redirect('/contest/' + cid + '/all'); 122 | } else { 123 | res.render("error", { message: "Couldnot add to checklist" }); 124 | } 125 | })*/ 126 | }) 127 | 128 | app.get('/contest/:contestid/:type', function (req, res) { 129 | checklist.findOne({ contest: req.params.contestid }, function (err, obj) { 130 | if (err) { 131 | return; 132 | } 133 | if (obj) { 134 | lastupdatecollection.findOne({ contest: req.params.contestid }, function (err, dateobj) { 135 | if (err) 136 | throw err; 137 | 138 | if (dateobj) { 139 | var page = (req.query.page ? req.query.page : 1); 140 | if (page < 1) { 141 | page = 1; 142 | } 143 | var perPage = (req.query.perPage ? req.query.perPage : 25); 144 | if (perPage <= 10) { 145 | perPage = 10; 146 | } 147 | else if (perPage <= 25) { 148 | perPage = 25; 149 | } 150 | else if (perPage <= 50) { 151 | perPage = 50; 152 | } 153 | else if (perPage <= 100) { 154 | perPage = 100; 155 | } 156 | else { 157 | perPage = 1000; 158 | } 159 | datacollection.find({ contest: req.params.contestid, type: req.params.type }).limit(perPage).skip((page - 1) * perPage).sort({ rank: 1 }).toArray((err, result) => { 160 | if (err) 161 | throw err; 162 | 163 | for (var i in result) { 164 | result[i].change = result[i].rating - result[i].previous; 165 | } 166 | 167 | var typename = req.params.type[0].toUpperCase() + req.params.type.slice(1); 168 | var theme = (req.cookies.theme ? req.cookies.theme : "/css/rating.css"); 169 | datacollection.count({ contest: req.params.contestid, type: req.params.type }).then((count) => { 170 | res.render('rating', { 171 | elapsed: elapsedTime(dateobj.date), 172 | contest: req.params.contestid, 173 | type: req.params.type, 174 | typename: typename, 175 | result: result, 176 | theme: theme, 177 | pageCount: parseInt(count / perPage + 1), 178 | selectedPage: parseInt(page), 179 | perPage: parseInt(perPage) 180 | }); 181 | }); 182 | }); 183 | } 184 | else { 185 | res.status(404); 186 | res.render("error", { message: "We are currently calculating ratings for this contest!" }); 187 | } 188 | }); 189 | } 190 | else { 191 | res.status(404); 192 | var link = req.protocol + "://" + req.get('host') + "/add/" + req.params.contestid 193 | res.render("error", { 194 | message: "No contest predictions found for such contest! Please enter correct contest code.", 195 | link: link 196 | }); 197 | } 198 | }); 199 | }); 200 | 201 | app.use(function (req, res) { 202 | res.status(500); 203 | res.render("error", { message: "Invalid link!" }); 204 | }); 205 | 206 | app.listen(process.env.PORT || 8080); 207 | 208 | console.log('Listening on http://127.0.0.1:8080'); 209 | }); 210 | -------------------------------------------------------------------------------- /status.js: -------------------------------------------------------------------------------- 1 | var util = require("util"); 2 | var async = require("async"); 3 | var authenticate = require("./authenticate.js"); 4 | 5 | var contestid; 6 | 7 | var usercollection; 8 | var collection; 9 | 10 | function parseStatusPage(contestid, pageno, callback) { 11 | 12 | var url = util.format('https://api.codechef.com/submissions?contestCode=%s&offset=%s&limit=20&fields=username', contestid, pageno * 20); 13 | 14 | authenticate.getBearer((err, result) => { 15 | if (err) { 16 | console.log(err) 17 | } else { 18 | var accessToken = result["result"]["data"]["access_token"] 19 | 20 | execHttps(url, function (source) { 21 | if (source.indexOf("API request limit exhausted") != -1) { 22 | console.log("API request limit exhausted :(") 23 | // try after 5 minutes 24 | setTimeout(function () { 25 | parseStatusPage(contestid, pageno, callback) 26 | }, 5 * 60 * 1000) 27 | return 28 | } 29 | 30 | if (source.indexOf("no submissions found for this search") != -1) { 31 | // all submissions parsed 32 | callback() 33 | return 34 | } 35 | 36 | var submissions = JSON.parse(source).result.data.content; 37 | submissions.forEach(submission => { 38 | try { 39 | usercollection.update( 40 | { contestid: contestid, user: submission.username }, 41 | { contestid: contestid, user: submission.username }, 42 | { upsert: true }); 43 | } 44 | catch (ex) { 45 | } 46 | }) 47 | 48 | collection.update({ contestid: contestid }, { $set: { pagedone: pageno } }, { upsert: true }); 49 | 50 | parseStatusPage(contestid, pageno + 1, callback); 51 | }, 4, accessToken); 52 | } 53 | }) 54 | } 55 | 56 | require("./helper.js")(); 57 | 58 | module.exports = function (nextcall) { 59 | 60 | var contestIDS = []; 61 | 62 | MongoClient.connect(mongourl, function (err, db) { 63 | if (err) { 64 | throw err; 65 | } 66 | 67 | collection = db.collection("status"); 68 | usercollection = db.collection("user"); 69 | checklistcollection = db.collection("checklist"); 70 | 71 | usercollection.createIndex({ contestid: 1, user: 1 }, { unique: true }); 72 | 73 | /* 74 | bad way 75 | if (process.argv.length >= 4 && process.argv[3] == 'delete') 76 | { 77 | //reset before use 78 | usercollection.deleteMany({}); 79 | collection.deleteMany({}); 80 | } 81 | */ 82 | 83 | var processContests = function () { 84 | async.eachSeries(contestIDS, function (ciid, callback) { 85 | contestid = ciid; 86 | 87 | collection.findOne({ contestid: contestid }, function (err, obj) { 88 | var lastpage = (obj !== null ? obj.pagedone : 0); 89 | parseStatusPage(contestid, lastpage, callback); 90 | }); 91 | }, 92 | function (err) { 93 | if (err) { 94 | console.log("Error", err); 95 | throw err; 96 | } 97 | 98 | db.close(); 99 | console.log("Completed ALL"); 100 | 101 | setImmediate(nextcall); 102 | }); 103 | }; 104 | 105 | db.collection("checklist").find({}).toArray(function (err, cdatas) { 106 | if (cdatas) { 107 | cdatas.forEach(function (x) { 108 | contestIDS.push(x.contest); 109 | }); 110 | } 111 | 112 | processContests(); 113 | }); 114 | }); 115 | 116 | }; 117 | -------------------------------------------------------------------------------- /verify.js: -------------------------------------------------------------------------------- 1 | var http = require("http"); 2 | var https = require("https"); 3 | var util = require("util"); 4 | var cheerio = require("cheerio"); 5 | var async = require("async"); 6 | var path = require("path"); 7 | var fs = require("fs"); 8 | 9 | var dataList = {}; 10 | var originalList = {}; 11 | 12 | var cacheDir = "cache"; 13 | 14 | require("./helper.js")(); 15 | 16 | var type = "all"; 17 | 18 | function grabPage(pageno, callback) { 19 | var url = "https://www.codechef.com/api/ratings/" + type + "?sortBy=global_rank&order=asc&itemsPerPage=40&page=" + pageno; 20 | var filepath = path.resolve(path.join(cacheDir, pageno + ".json")); 21 | 22 | execHttps(url, function (source) { 23 | if (source.indexOf("availablePages") == -1) { 24 | grabPage(pageno, callback); 25 | return; 26 | } 27 | 28 | fs.writeFile(filepath, source, 'utf8'); 29 | setImmediate(callback); 30 | }, 4); 31 | } 32 | 33 | function readPage(pageno, callback) { 34 | var filepath = path.resolve(path.join(cacheDir, pageno + ".json")); 35 | var source = fs.readFileSync(filepath).toString(); 36 | 37 | var obj = JSON.parse(source); 38 | 39 | obj.list.forEach(function (data) { 40 | originalList[data.username] = parseInt(data.all_rating); 41 | }); 42 | 43 | setImmediate(callback); 44 | } 45 | 46 | function savePages(start, end, callback) { 47 | var arr = []; 48 | for (var i = start; i <= end; i++) { 49 | arr.push(i); 50 | } 51 | 52 | async.eachLimit(arr, 5, grabPage, function (err) { 53 | setImmediate(callback); 54 | }); 55 | } 56 | 57 | function getMatching(url, start, end, callback) { 58 | var url = util.format(url); 59 | execHttps(url, function (source) { 60 | var $ = cheerio.load(source); 61 | var lastpage = parseInt($('.pageinfo').text().split(' ')[2]) - 1; 62 | 63 | $('tbody>tr').each(function (i, data) { 64 | var username = $($(data).children()[1]).text(); 65 | var predicted = parseInt($($(data).children()[3]).text()); 66 | dataList[username] = { user: username, predicted: predicted }; 67 | }); 68 | 69 | var arr = []; 70 | for (var i = start; i <= end; i++) { 71 | arr.push(i); 72 | } 73 | 74 | async.eachLimit(arr, 5, readPage, function (err) { 75 | var limit = 5000; 76 | var countError = new Array(limit).fill(0); 77 | 78 | Object.keys(dataList).forEach(function (username) { 79 | var diff = Math.abs(dataList[username].predicted - originalList[username]); 80 | if (diff != undefined) { 81 | countError[diff]++; 82 | } 83 | console.log(username, dataList[username].predicted, originalList[username], diff); 84 | }); 85 | 86 | for (var i = 0; i < limit; i++) { 87 | if (countError[i]) { 88 | console.log(i, countError[i]); 89 | } 90 | } 91 | }); 92 | }, 4); 93 | } 94 | 95 | //savePages(1, 1003); 96 | getMatching("http://127.0.0.1:8080/contest/JUNE17/all", 1, 1003); 97 | -------------------------------------------------------------------------------- /views/error.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Error 7 | 9 | 10 | 11 | 12 | 19 | 20 | 21 | 22 |
23 |
24 | Error: <%= message %> 25 |
26 |
27 | 30 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /views/landing.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Rating Predictor 7 | 9 | 10 | 11 | 12 | 13 | 20 | 21 | 22 | 23 |
24 |
25 | 26 |
27 |
28 |
29 |
30 |

Codechef Rating Predictor

31 |
32 |
33 | Github 35 |
36 |
37 |
38 |
39 | 40 |
41 |
42 |
43 |
44 |
45 | 47 |
48 |
49 | Fetch 50 |
51 |
52 |
53 | 54 |
55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | <% result.forEach((user) => { %> 71 | <% var change = user.rating - user.previous %> 72 | 0) { %> class="bg-success" <% } %> <% if(change < 0) { %> 73 | class="bg-danger" <% } %> <% if(change == 0) { %> class="bg-primary" <% } %>> 74 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | <% }) %> 86 | 87 |
ContestTypeSeed#OldNewDeltaLast Updated
<%= user.contest %> 76 | <%= user.type %><%= user.erank %><%= user.rank %><%= user.previous %><%= user.rating %><%= change %><%= user.elapsed %>
88 |
89 |
90 |
91 | 92 |
93 |
94 | 95 |
96 |
97 |

Select Contest

98 |
99 | 104 |
105 | 106 |
107 |
108 | 113 |
114 |
115 | 119 |
120 |
121 | 125 |
126 |
127 | 128 |
129 |
130 |
131 |
132 | 135 | 136 | 137 | 138 | -------------------------------------------------------------------------------- /views/landing_down.ejs: -------------------------------------------------------------------------------- 1 | 2 | Predictor Down 3 | 28 | 29 |
30 |

Predictor SHUTDOWN:(

31 |
32 |

Predictor is shudown for indefinite amount of time. Sorry for the inconvenience caused.

33 |
34 |
-------------------------------------------------------------------------------- /views/rating.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Predictions - <%= contest %> 7 | 9 | 10 | 11 | 12 | 13 | 20 | 21 | 22 | 23 |
24 |
25 |
26 |

Predictions for <%= contest %> - 27 | <%= typename %>

28 |
29 |
30 | Codechef Rating Predictor - Github
32 | Last updated: <%= elapsed %> 33 |
34 |
35 | Switch Theme 37 |
38 |
39 | 40 |
41 |
42 |
43 |
44 |
45 |

Entries Per Page

46 |
47 |
48 |
    49 |
  • 10 51 |
  • 52 |
  • 25 54 |
  • 55 |
  • 50 57 |
  • 58 |
  • 100 61 |
  • 62 |
  • 1000 65 |
  • 66 |
67 |
68 |
69 |
70 |
71 |
72 | 73 |
74 |
75 |
76 |
77 |
78 |

Page Number

79 |
80 |
81 |
    82 | <% var pages = 0; %> 83 | <% for(var i=Math.max(1,selectedPage-3);i<=Math.min(selectedPage+3,pageCount); i++, pages++) { %> 84 |
  • <%= i %> 87 |
  • 88 | <% } %> 89 | <% if(pages < 7) %> 90 | <% for(var i=selectedPage+4;i<=pageCount && pages<7; i++, pages++) { %> 91 |
  • <%= i %> 94 |
  • 95 | <% } %> 96 |
97 |
98 |
99 |
100 |
101 |
102 | 103 |
104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | <% result.forEach((user) => { %> 118 | 0) { %> class="bg-success" <% } %> <% if(user.change < 0) { %> 119 | class="bg-danger" <% } %> <% if(user.change == 0) { %> class="bg-primary" <% } %>> 120 | 121 | 122 | 124 | 125 | 126 | 127 | 128 | <% }) %> 129 | 130 |
Seed#UsernamePrevious RatingNew RatingRating Change
<%= user.erank %><%= user.rank %><%= user.user %><%= user.previous %><%= user.rating %><%= user.change %>
131 |
132 |
133 | 134 | 137 | 140 | 141 | 142 | --------------------------------------------------------------------------------