diff --git a/src/calculateRank.js b/src/calculateRank.js index 836779e0d12a9..fae22b59756b0 100644 --- a/src/calculateRank.js +++ b/src/calculateRank.js @@ -1,12 +1,91 @@ -function exponential_cdf(x) { - return 1 - 2 ** -x; -} +function score(x, quantiles) { + const i = quantiles.findIndex((q) => x < q); + + if (i == 0) { + return 0.0; + } else if (i == -1) { + return 1.0; + } -function log_normal_cdf(x) { - // approximation - return x / (1 + x); + const a = quantiles[i - 1]; + const b = quantiles[i]; + + return ((x - a) / (b - a) + i - 1) / (quantiles.length - 1); } +const QUANTILES = { + commits: [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 3, 3, + 4, 4, 5, 6, 7, 7, 8, 9, 10, 11, 12, 13, 15, 16, 17, 19, 20, 22, 23, 25, 27, + 29, 31, 33, 35, 38, 40, 43, 45, 48, 51, 54, 57, 60, 64, 67, 71, 76, 80, 85, + 89, 94, 99, 105, 111, 118, 125, 132, 140, 147, 155, 164, 173, 184, 195, 207, + 220, 233, 249, 265, 284, 304, 326, 353, 380, 411, 451, 495, 545, 611, 691, + 794, 933, 1195, 1704, 9722, + ], + all_commits: [ + 0, 0, 0, 0, 2, 4, 8, 11, 15, 19, 23, 27, 32, 36, 41, 45, 50, 55, 60, 65, 71, + 76, 82, 87, 93, 99, 105, 111, 117, 124, 131, 137, 145, 151, 159, 166, 174, + 182, 190, 198, 207, 215, 225, 234, 244, 253, 264, 274, 285, 296, 306, 318, + 330, 342, 355, 368, 382, 396, 409, 424, 440, 457, 475, 493, 512, 531, 551, + 570, 593, 618, 643, 667, 695, 723, 752, 784, 815, 857, 893, 934, 984, 1037, + 1094, 1152, 1217, 1289, 1379, 1475, 1576, 1696, 1851, 2023, 2232, 2480, + 2835, 3242, 3885, 4868, 6614, 11801, 792319, + ], + prs: [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, + 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, + 8, 8, 9, 10, 10, 11, 11, 12, 13, 14, 14, 15, 16, 17, 18, 19, 20, 21, 23, 24, + 25, 27, 28, 30, 32, 34, 36, 38, 40, 43, 46, 50, 53, 57, 61, 65, 70, 76, 83, + 90, 99, 110, 123, 139, 159, 185, 219, 273, 360, 562, 2291, + ], + issues: [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, + 4, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 8, 8, 9, 10, 10, 11, 12, 12, 13, 14, 15, + 16, 17, 19, 20, 21, 23, 24, 26, 28, 30, 32, 35, 38, 41, 45, 49, 54, 59, 66, + 73, 82, 92, 106, 123, 150, 186, 255, 409, 1590, + ], + reviews: [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 2, 2, 3, 4, 5, 6, 8, 11, 15, 23, 36, 61, + 129, 764, + ], + repos: [ + 0, 0, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 6, 7, 7, 8, 8, 8, 9, 9, 10, 10, 11, + 11, 12, 12, 12, 13, 13, 14, 14, 15, 15, 16, 16, 17, 17, 18, 18, 19, 19, 20, + 20, 21, 21, 22, 22, 23, 24, 24, 25, 25, 26, 27, 28, 28, 29, 30, 30, 31, 32, + 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 45, 46, 47, 49, 50, 52, 54, 56, + 58, 60, 62, 65, 68, 70, 74, 77, 82, 86, 92, 98, 105, 115, 127, 144, 170, + 211, 316, 2002, + ], + stars: [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 6, 6, + 7, 7, 8, 8, 9, 9, 10, 11, 11, 12, 13, 14, 15, 16, 17, 18, 19, 21, 22, 24, + 26, 28, 30, 31, 33, 35, 38, 41, 44, 48, 52, 56, 61, 67, 74, 83, 93, 104, + 117, 134, 154, 181, 215, 257, 321, 417, 565, 818, 1298, 2599, 18304, + ], + followers: [ + 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, + 5, 5, 5, 5, 6, 6, 6, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, 10, 10, 11, 11, 12, 12, + 13, 13, 14, 14, 15, 15, 16, 16, 17, 18, 18, 19, 20, 20, 21, 22, 23, 24, 25, + 26, 27, 28, 29, 30, 32, 33, 35, 36, 38, 40, 42, 44, 46, 49, 51, 54, 57, 61, + 65, 70, 75, 81, 88, 97, 108, 121, 139, 161, 193, 240, 334, 569, 3583, + ], +}; + +const WEIGHT = { + commits: 2.0, + prs: 3.0, + issues: 1.0, + reviews: 0.5, + repos: 0.0, + stars: 4.0, + followers: 1.0, +}; + /** * Calculates the users rank. * @@ -27,48 +106,36 @@ function calculateRank({ prs, issues, reviews, - // eslint-disable-next-line no-unused-vars - repos, // unused + repos, stars, followers, }) { - const COMMITS_MEDIAN = all_commits ? 1000 : 250, - COMMITS_WEIGHT = 2; - const PRS_MEDIAN = 50, - PRS_WEIGHT = 3; - const ISSUES_MEDIAN = 25, - ISSUES_WEIGHT = 1; - const REVIEWS_MEDIAN = 2, - REVIEWS_WEIGHT = 1; - const STARS_MEDIAN = 50, - STARS_WEIGHT = 4; - const FOLLOWERS_MEDIAN = 10, - FOLLOWERS_WEIGHT = 1; - - const TOTAL_WEIGHT = - COMMITS_WEIGHT + - PRS_WEIGHT + - ISSUES_WEIGHT + - REVIEWS_WEIGHT + - STARS_WEIGHT + - FOLLOWERS_WEIGHT; - const THRESHOLDS = [1, 12.5, 25, 37.5, 50, 62.5, 75, 87.5, 100]; const LEVELS = ["S", "A+", "A", "A-", "B+", "B", "B-", "C+", "C"]; - const rank = - 1 - - (COMMITS_WEIGHT * exponential_cdf(commits / COMMITS_MEDIAN) + - PRS_WEIGHT * exponential_cdf(prs / PRS_MEDIAN) + - ISSUES_WEIGHT * exponential_cdf(issues / ISSUES_MEDIAN) + - REVIEWS_WEIGHT * exponential_cdf(reviews / REVIEWS_MEDIAN) + - STARS_WEIGHT * log_normal_cdf(stars / STARS_MEDIAN) + - FOLLOWERS_WEIGHT * log_normal_cdf(followers / FOLLOWERS_MEDIAN)) / - TOTAL_WEIGHT; + const total_weight = + WEIGHT.commits + + WEIGHT.prs + + WEIGHT.issues + + WEIGHT.reviews + + WEIGHT.repos + + WEIGHT.stars + + WEIGHT.followers; + + const total_score = + WEIGHT.commits * + score(commits, all_commits ? QUANTILES.all_commits : QUANTILES.commits) + + WEIGHT.prs * score(prs, QUANTILES.prs) + + WEIGHT.issues * score(issues, QUANTILES.issues) + + WEIGHT.reviews * score(reviews, QUANTILES.reviews) + + WEIGHT.repos * score(repos, QUANTILES.repos) + + WEIGHT.stars * score(stars, QUANTILES.stars) + + WEIGHT.followers * score(followers, QUANTILES.followers); - const level = LEVELS[THRESHOLDS.findIndex((t) => rank * 100 <= t)]; + const percentile = 100 * (1 - total_score / total_weight); + const level = LEVELS[THRESHOLDS.findIndex((t) => percentile <= t)]; - return { level, percentile: rank * 100 }; + return { level, percentile }; } export { calculateRank }; diff --git a/tests/calculateRank.test.js b/tests/calculateRank.test.js index 65f60df3cad97..d081164122c42 100644 --- a/tests/calculateRank.test.js +++ b/tests/calculateRank.test.js @@ -3,7 +3,7 @@ import { calculateRank } from "../src/calculateRank.js"; import { expect, it, describe } from "@jest/globals"; describe("Test calculateRank", () => { - it("new user gets C rank", () => { + it("new user gets C+ rank", () => { expect( calculateRank({ all_commits: false, @@ -15,82 +15,67 @@ describe("Test calculateRank", () => { stars: 0, followers: 0, }), - ).toStrictEqual({ level: "C", percentile: 100 }); + ).toStrictEqual({ level: "C+", percentile: 78.26086956521738 }); }); - it("beginner user gets B- rank", () => { + it("beginner user gets B rank", () => { expect( calculateRank({ all_commits: false, - commits: 125, - prs: 25, - issues: 10, - reviews: 5, - repos: 0, - stars: 25, + commits: 50, + prs: 5, + issues: 5, + reviews: 0, + repos: 5, + stars: 5, followers: 5, }), - ).toStrictEqual({ level: "B-", percentile: 65.02918514848255 }); + ).toStrictEqual({ level: "B", percentile: 51.97101449275363 }); }); - it("median user gets B+ rank", () => { + it("advanced user gets A- rank", () => { expect( calculateRank({ all_commits: false, commits: 250, - prs: 50, + prs: 25, issues: 25, - reviews: 10, - repos: 0, - stars: 50, - followers: 10, + reviews: 0, + repos: 25, + stars: 25, + followers: 25, }), - ).toStrictEqual({ level: "B+", percentile: 46.09375 }); + ).toStrictEqual({ level: "A-", percentile: 27.07608695652174 }); }); - it("average user gets B+ rank (include_all_commits)", () => { + it("advanced user gets A- rank (include_all_commits)", () => { expect( calculateRank({ all_commits: true, commits: 1000, - prs: 50, + prs: 25, issues: 25, - reviews: 10, - repos: 0, - stars: 50, - followers: 10, + reviews: 0, + repos: 25, + stars: 25, + followers: 25, }), - ).toStrictEqual({ level: "B+", percentile: 46.09375 }); + ).toStrictEqual({ level: "A-", percentile: 27.55619360131255 }); }); - it("advanced user gets A rank", () => { + it("expert user gets A+ rank", () => { expect( calculateRank({ all_commits: false, commits: 500, prs: 100, - issues: 50, - reviews: 20, - repos: 0, - stars: 200, - followers: 40, - }), - ).toStrictEqual({ level: "A", percentile: 20.841471354166664 }); - }); - - it("expert user gets A+ rank", () => { - expect( - calculateRank({ - all_commits: false, - commits: 1000, - prs: 200, issues: 100, - reviews: 40, - repos: 0, - stars: 800, - followers: 160, + reviews: 0, + repos: 100, + stars: 100, + followers: 100, }), - ).toStrictEqual({ level: "A+", percentile: 5.575988339442828 }); + ).toStrictEqual({ level: "A+", percentile: 10.794579333709752 }); }); it("sindresorhus gets S rank", () => { @@ -105,6 +90,6 @@ describe("Test calculateRank", () => { stars: 600000, followers: 50000, }), - ).toStrictEqual({ level: "S", percentile: 0.4578556547153667 }); + ).toStrictEqual({ level: "S", percentile: 0.4312953010223719 }); }); });