@xcpcio/core 0.45.1 → 0.46.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +299 -0
- package/dist/index.d.ts +77 -2
- package/dist/index.mjs +294 -1
- package/package.json +2 -2
- package/src/contest.ts +1 -0
- package/src/index.ts +2 -0
- package/src/person.ts +42 -0
- package/src/rank.ts +2 -0
- package/src/rating/index.ts +4 -0
- package/src/rating/rating-calculator.ts +100 -0
- package/src/rating/rating-history.ts +81 -0
- package/src/rating/rating-user.ts +98 -0
- package/src/rating/rating.ts +137 -0
package/dist/index.cjs
CHANGED
|
@@ -548,6 +548,36 @@ class ICPCStandingsCsvConverter {
|
|
|
548
548
|
}
|
|
549
549
|
}
|
|
550
550
|
|
|
551
|
+
class Person {
|
|
552
|
+
constructor(name = "") {
|
|
553
|
+
this.name = name;
|
|
554
|
+
}
|
|
555
|
+
toJSON() {
|
|
556
|
+
return {
|
|
557
|
+
name: this.name
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
static fromJSON(iPerson) {
|
|
561
|
+
if (typeof iPerson === "string") {
|
|
562
|
+
iPerson = JSON.parse(iPerson);
|
|
563
|
+
}
|
|
564
|
+
const person = new Person();
|
|
565
|
+
person.name = iPerson.name;
|
|
566
|
+
return person;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
function createPersons(iPersons) {
|
|
570
|
+
if (typeof iPersons === "string") {
|
|
571
|
+
for (const c of " ,\u3001|") {
|
|
572
|
+
if (iPersons.includes(c)) {
|
|
573
|
+
return iPersons.split(c).map((name) => new Person(name));
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
return [new Person(iPersons)];
|
|
577
|
+
}
|
|
578
|
+
return iPersons.map((name) => new Person(name));
|
|
579
|
+
}
|
|
580
|
+
|
|
551
581
|
function calcDirt(attemptedNum, solvedNum) {
|
|
552
582
|
if (solvedNum === 0) {
|
|
553
583
|
return 0;
|
|
@@ -576,6 +606,268 @@ function getWhiteOrBlackColor(background) {
|
|
|
576
606
|
return brightness <= threshold ? "#fff" : "#000";
|
|
577
607
|
}
|
|
578
608
|
|
|
609
|
+
class RatingHistory {
|
|
610
|
+
constructor() {
|
|
611
|
+
this.rank = 0;
|
|
612
|
+
this.rating = 0;
|
|
613
|
+
this.teamName = "";
|
|
614
|
+
this.organization = "";
|
|
615
|
+
this.members = [];
|
|
616
|
+
this.coaches = [];
|
|
617
|
+
this.contestID = "";
|
|
618
|
+
this.contestName = "";
|
|
619
|
+
this.contestLink = "";
|
|
620
|
+
this.contestTime = createDayJS();
|
|
621
|
+
}
|
|
622
|
+
toJSON() {
|
|
623
|
+
return {
|
|
624
|
+
rank: this.rank,
|
|
625
|
+
rating: this.rating,
|
|
626
|
+
teamName: this.teamName,
|
|
627
|
+
organization: this.organization,
|
|
628
|
+
members: this.members.map((member) => member.toJSON()),
|
|
629
|
+
coaches: this.coaches.map((coach) => coach.toJSON()),
|
|
630
|
+
contestID: this.contestID,
|
|
631
|
+
contestName: this.contestName,
|
|
632
|
+
contestLink: this.contestLink,
|
|
633
|
+
contestTime: this.contestTime.toDate()
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
static fromJSON(iRatingHistory) {
|
|
637
|
+
if (typeof iRatingHistory === "string") {
|
|
638
|
+
iRatingHistory = JSON.parse(iRatingHistory);
|
|
639
|
+
}
|
|
640
|
+
const ratingHistory = new RatingHistory();
|
|
641
|
+
ratingHistory.rank = iRatingHistory.rank;
|
|
642
|
+
ratingHistory.rating = iRatingHistory.rating;
|
|
643
|
+
ratingHistory.teamName = iRatingHistory.teamName;
|
|
644
|
+
ratingHistory.organization = iRatingHistory.organization;
|
|
645
|
+
ratingHistory.members = iRatingHistory.members.map((iMember) => Person.fromJSON(iMember));
|
|
646
|
+
ratingHistory.coaches = iRatingHistory.coaches.map((iCoach) => Person.fromJSON(iCoach));
|
|
647
|
+
ratingHistory.contestID = iRatingHistory.contestID;
|
|
648
|
+
ratingHistory.contestName = iRatingHistory.contestName;
|
|
649
|
+
ratingHistory.contestLink = iRatingHistory.contestLink;
|
|
650
|
+
ratingHistory.contestTime = createDayJS(iRatingHistory.contestTime);
|
|
651
|
+
return ratingHistory;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
class RatingUser {
|
|
656
|
+
constructor() {
|
|
657
|
+
this.id = "";
|
|
658
|
+
this.name = "";
|
|
659
|
+
this.organization = "";
|
|
660
|
+
this.members = [];
|
|
661
|
+
this.coaches = [];
|
|
662
|
+
this.rating = 0;
|
|
663
|
+
this.minRating = 1061109567;
|
|
664
|
+
this.maxRating = -1061109567;
|
|
665
|
+
this.rank = 0;
|
|
666
|
+
this.oldRating = 0;
|
|
667
|
+
this.seed = 1;
|
|
668
|
+
this.delta = 0;
|
|
669
|
+
this.ratingHistories = [];
|
|
670
|
+
}
|
|
671
|
+
UpdateRating(rating) {
|
|
672
|
+
this.rating = rating;
|
|
673
|
+
this.minRating = Math.min(this.minRating, rating);
|
|
674
|
+
this.maxRating = Math.max(this.maxRating, rating);
|
|
675
|
+
}
|
|
676
|
+
toJSON() {
|
|
677
|
+
return {
|
|
678
|
+
id: this.id,
|
|
679
|
+
name: this.name,
|
|
680
|
+
organization: this.organization,
|
|
681
|
+
members: this.members.map((member) => member.toJSON()),
|
|
682
|
+
coaches: this.coaches.map((coach) => coach.toJSON()),
|
|
683
|
+
rating: this.rating,
|
|
684
|
+
minRating: this.minRating,
|
|
685
|
+
maxRating: this.maxRating,
|
|
686
|
+
ratingHistories: this.ratingHistories.map((ratingHistory) => ratingHistory.toJSON())
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
static fromJSON(iRatingUser) {
|
|
690
|
+
if (typeof iRatingUser === "string") {
|
|
691
|
+
iRatingUser = JSON.parse(iRatingUser);
|
|
692
|
+
}
|
|
693
|
+
const ratingUser = new RatingUser();
|
|
694
|
+
ratingUser.id = iRatingUser.id;
|
|
695
|
+
ratingUser.name = iRatingUser.name;
|
|
696
|
+
ratingUser.organization = iRatingUser.organization;
|
|
697
|
+
ratingUser.members = iRatingUser.members.map((member) => Person.fromJSON(member));
|
|
698
|
+
ratingUser.coaches = iRatingUser.coaches.map((coach) => Person.fromJSON(coach));
|
|
699
|
+
ratingUser.rating = iRatingUser.rating;
|
|
700
|
+
ratingUser.minRating = iRatingUser.minRating;
|
|
701
|
+
ratingUser.maxRating = iRatingUser.maxRating;
|
|
702
|
+
for (const iRatingHistory of iRatingUser.ratingHistories) {
|
|
703
|
+
ratingUser.ratingHistories.push(RatingHistory.fromJSON(iRatingHistory));
|
|
704
|
+
}
|
|
705
|
+
return ratingUser;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
class RatingCalculator {
|
|
710
|
+
constructor() {
|
|
711
|
+
this.users = [];
|
|
712
|
+
}
|
|
713
|
+
calculate() {
|
|
714
|
+
this.calculateInternal();
|
|
715
|
+
}
|
|
716
|
+
calcP(userA, userB) {
|
|
717
|
+
return 1 / (1 + 10 ** ((userB.oldRating - userA.oldRating) / 400));
|
|
718
|
+
}
|
|
719
|
+
getExSeed(users, rating, ownUser) {
|
|
720
|
+
const exUser = new RatingUser();
|
|
721
|
+
exUser.oldRating = rating;
|
|
722
|
+
let res = 0;
|
|
723
|
+
users.forEach((user) => {
|
|
724
|
+
if (user.id !== ownUser.id) {
|
|
725
|
+
res += this.calcP(user, exUser);
|
|
726
|
+
}
|
|
727
|
+
});
|
|
728
|
+
return res;
|
|
729
|
+
}
|
|
730
|
+
calcRating(users, rank, user) {
|
|
731
|
+
let left = 1;
|
|
732
|
+
let right = 8e3;
|
|
733
|
+
while (right - left > 1) {
|
|
734
|
+
const mid = Math.floor((left + right) / 2);
|
|
735
|
+
if (this.getExSeed(users, mid, user) < rank) {
|
|
736
|
+
right = mid;
|
|
737
|
+
} else {
|
|
738
|
+
left = mid;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
return left;
|
|
742
|
+
}
|
|
743
|
+
calculateInternal() {
|
|
744
|
+
for (let i = 0; i < this.users.length; i++) {
|
|
745
|
+
const u = this.users[i];
|
|
746
|
+
u.seed = 1;
|
|
747
|
+
for (let j = 0; j < this.users.length; j++) {
|
|
748
|
+
if (i !== j) {
|
|
749
|
+
const otherUser = this.users[j];
|
|
750
|
+
u.seed += this.calcP(otherUser, u);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
let sumDelta = 0;
|
|
755
|
+
for (let i = 0; i < this.users.length; i++) {
|
|
756
|
+
const u = this.users[i];
|
|
757
|
+
u.delta = Math.floor(
|
|
758
|
+
(this.calcRating(this.users, Math.sqrt(u.rank * u.seed), u) - u.oldRating) / 2
|
|
759
|
+
);
|
|
760
|
+
sumDelta += u.delta;
|
|
761
|
+
}
|
|
762
|
+
let inc = Math.floor(-sumDelta / this.users.length) - 1;
|
|
763
|
+
for (let i = 0; i < this.users.length; i++) {
|
|
764
|
+
const u = this.users[i];
|
|
765
|
+
u.delta += inc;
|
|
766
|
+
}
|
|
767
|
+
this.users = this.users.sort((a, b) => b.oldRating - a.oldRating);
|
|
768
|
+
const s = Math.min(this.users.length, Math.floor(4 * Math.round(Math.sqrt(this.users.length))));
|
|
769
|
+
let sumS = 0;
|
|
770
|
+
for (let i = 0; i < s; i++) {
|
|
771
|
+
sumS += this.users[i].delta;
|
|
772
|
+
}
|
|
773
|
+
inc = Math.min(Math.max(Math.floor(-sumS / s), -10), 0);
|
|
774
|
+
this.users.forEach((u) => {
|
|
775
|
+
u.delta += inc;
|
|
776
|
+
u.UpdateRating(u.oldRating + u.delta);
|
|
777
|
+
});
|
|
778
|
+
this.users = this.users.sort((a, b) => a.rank - b.rank);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
class Rating {
|
|
783
|
+
constructor() {
|
|
784
|
+
this.id = "";
|
|
785
|
+
this.name = "";
|
|
786
|
+
this.baseRating = 1500;
|
|
787
|
+
this.contestIDs = [];
|
|
788
|
+
this.users = [];
|
|
789
|
+
this.ranks = [];
|
|
790
|
+
this.userMap = /* @__PURE__ */ new Map();
|
|
791
|
+
}
|
|
792
|
+
buildRating() {
|
|
793
|
+
for (const rank of this.ranks) {
|
|
794
|
+
rank.buildRank();
|
|
795
|
+
const ratingCalculator = new RatingCalculator();
|
|
796
|
+
for (const t of rank.teams) {
|
|
797
|
+
const id = this.generateTeamId(t);
|
|
798
|
+
let u = null;
|
|
799
|
+
if (!this.userMap.has(id)) {
|
|
800
|
+
u = new RatingUser();
|
|
801
|
+
u.id = id;
|
|
802
|
+
u.name = t.name;
|
|
803
|
+
u.organization = t.organization;
|
|
804
|
+
u.members = createPersons(t.members ?? []);
|
|
805
|
+
u.coaches = createPersons(t.coach ?? []);
|
|
806
|
+
u.rank = t.rank;
|
|
807
|
+
u.oldRating = this.baseRating;
|
|
808
|
+
u.UpdateRating(this.baseRating);
|
|
809
|
+
this.userMap.set(id, u);
|
|
810
|
+
this.users.push(u);
|
|
811
|
+
ratingCalculator.users.push(u);
|
|
812
|
+
} else {
|
|
813
|
+
u = this.userMap.get(id);
|
|
814
|
+
u.rank = t.rank;
|
|
815
|
+
u.oldRating = u.rating;
|
|
816
|
+
ratingCalculator.users.push(u);
|
|
817
|
+
}
|
|
818
|
+
{
|
|
819
|
+
const h = new RatingHistory();
|
|
820
|
+
h.rank = t.rank;
|
|
821
|
+
h.rating = u.rating;
|
|
822
|
+
h.teamName = t.name;
|
|
823
|
+
h.organization = t.organization;
|
|
824
|
+
h.members = createPersons(t.members ?? []);
|
|
825
|
+
h.coaches = createPersons(t.coach ?? []);
|
|
826
|
+
h.contestID = rank.contest.id;
|
|
827
|
+
h.contestLink = h.contestID;
|
|
828
|
+
h.contestName = rank.contest.name;
|
|
829
|
+
h.contestTime = rank.contest.startTime;
|
|
830
|
+
u.ratingHistories.push(h);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
ratingCalculator.calculate();
|
|
834
|
+
for (const u of ratingCalculator.users) {
|
|
835
|
+
u.ratingHistories.at(-1).rating = u.rating;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
generateTeamId(t) {
|
|
840
|
+
const persons = createPersons(t.members ?? []);
|
|
841
|
+
if (persons.length > 0) {
|
|
842
|
+
return persons.map((person) => person.name).join("|");
|
|
843
|
+
}
|
|
844
|
+
return `${t.organization}-${t.name}`;
|
|
845
|
+
}
|
|
846
|
+
toJSON() {
|
|
847
|
+
return {
|
|
848
|
+
id: this.id,
|
|
849
|
+
name: this.name,
|
|
850
|
+
baseRating: this.baseRating,
|
|
851
|
+
contestIDs: this.contestIDs,
|
|
852
|
+
users: this.users.map((ratingUser) => ratingUser.toJSON())
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
static fromJSON(iRating) {
|
|
856
|
+
if (typeof iRating === "string") {
|
|
857
|
+
iRating = JSON.parse(iRating);
|
|
858
|
+
}
|
|
859
|
+
const rating = new Rating();
|
|
860
|
+
rating.id = iRating.id;
|
|
861
|
+
rating.name = iRating.name;
|
|
862
|
+
rating.baseRating = iRating.baseRating;
|
|
863
|
+
rating.contestIDs = iRating.contestIDs;
|
|
864
|
+
for (const iUser of iRating.users) {
|
|
865
|
+
rating.users.push(RatingUser.fromJSON(iUser));
|
|
866
|
+
}
|
|
867
|
+
return rating;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
579
871
|
class ProblemStatistics {
|
|
580
872
|
constructor() {
|
|
581
873
|
this.acceptedNum = 0;
|
|
@@ -1183,6 +1475,7 @@ function createContestOptions(contestOptionsJSON = {}) {
|
|
|
1183
1475
|
|
|
1184
1476
|
class Contest {
|
|
1185
1477
|
constructor() {
|
|
1478
|
+
this.id = "";
|
|
1186
1479
|
this.name = "";
|
|
1187
1480
|
this.startTime = createDayJS();
|
|
1188
1481
|
this.endTime = createDayJS();
|
|
@@ -2036,12 +2329,17 @@ exports.Giants = Giants;
|
|
|
2036
2329
|
exports.GiantsType = GiantsType;
|
|
2037
2330
|
exports.ICPCStandingsCsvConverter = ICPCStandingsCsvConverter;
|
|
2038
2331
|
exports.MedalType = MedalType;
|
|
2332
|
+
exports.Person = Person;
|
|
2039
2333
|
exports.PlaceChartPointData = PlaceChartPointData;
|
|
2040
2334
|
exports.Problem = Problem;
|
|
2041
2335
|
exports.ProblemStatistics = ProblemStatistics;
|
|
2042
2336
|
exports.Rank = Rank;
|
|
2043
2337
|
exports.RankOptions = RankOptions;
|
|
2044
2338
|
exports.RankStatistics = RankStatistics;
|
|
2339
|
+
exports.Rating = Rating;
|
|
2340
|
+
exports.RatingCalculator = RatingCalculator;
|
|
2341
|
+
exports.RatingHistory = RatingHistory;
|
|
2342
|
+
exports.RatingUser = RatingUser;
|
|
2045
2343
|
exports.Resolver = Resolver;
|
|
2046
2344
|
exports.Submission = Submission;
|
|
2047
2345
|
exports.Team = Team;
|
|
@@ -2051,6 +2349,7 @@ exports.createContest = createContest;
|
|
|
2051
2349
|
exports.createContestIndex = createContestIndex;
|
|
2052
2350
|
exports.createContestIndexList = createContestIndexList;
|
|
2053
2351
|
exports.createDayJS = createDayJS;
|
|
2352
|
+
exports.createPersons = createPersons;
|
|
2054
2353
|
exports.createProblem = createProblem;
|
|
2055
2354
|
exports.createProblems = createProblems;
|
|
2056
2355
|
exports.createProblemsByProblemIds = createProblemsByProblemIds;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TimeUnit, SubmissionStatus, Submission as Submission$1, Submissions as Submissions$1, BalloonColor, Problem as Problem$1, Problems as Problems$1, Lang, CalculationOfPenalty, StatusTimeDisplay, MedalPreset, Image, BannerMode, ContestState, Contest as Contest$1, Team as Team$1, Teams as Teams$1, ContestIndex as ContestIndex$1 } from '@xcpcio/types';
|
|
1
|
+
import { TimeUnit, SubmissionStatus, Submission as Submission$1, Submissions as Submissions$1, BalloonColor, Problem as Problem$1, Problems as Problems$1, Lang, CalculationOfPenalty, StatusTimeDisplay, MedalPreset, Image, BannerMode, ContestState, Contest as Contest$1, Team as Team$1, Teams as Teams$1, IPerson, IRatingHistory, IRatingUser, IRating, ContestIndex as ContestIndex$1 } from '@xcpcio/types';
|
|
2
2
|
import dayjs from 'dayjs';
|
|
3
3
|
export { default as dayjs } from 'dayjs';
|
|
4
4
|
import * as XLSX from 'xlsx-js-style';
|
|
@@ -126,6 +126,7 @@ declare class ContestOptions {
|
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
declare class Contest {
|
|
129
|
+
id: string;
|
|
129
130
|
name: string;
|
|
130
131
|
startTime: dayjs.Dayjs;
|
|
131
132
|
endTime: dayjs.Dayjs;
|
|
@@ -331,6 +332,7 @@ declare class Rank {
|
|
|
331
332
|
buildBalloons(): void;
|
|
332
333
|
setReplayTime(replayStartTimestamp: number): void;
|
|
333
334
|
}
|
|
335
|
+
type Ranks = Array<Rank>;
|
|
334
336
|
|
|
335
337
|
declare class CodeforcesGymGhostDATConverter {
|
|
336
338
|
constructor();
|
|
@@ -352,6 +354,79 @@ declare class ICPCStandingsCsvConverter {
|
|
|
352
354
|
private getMedalCitation;
|
|
353
355
|
}
|
|
354
356
|
|
|
357
|
+
declare class Person {
|
|
358
|
+
name: string;
|
|
359
|
+
constructor(name?: string);
|
|
360
|
+
toJSON(): IPerson;
|
|
361
|
+
static fromJSON(iPerson: IPerson | string): Person;
|
|
362
|
+
}
|
|
363
|
+
type Persons = Array<Person>;
|
|
364
|
+
declare function createPersons(iPersons: string | Array<string>): Persons;
|
|
365
|
+
|
|
366
|
+
declare class RatingHistory {
|
|
367
|
+
rank: number;
|
|
368
|
+
rating: number;
|
|
369
|
+
teamName: string;
|
|
370
|
+
organization: string;
|
|
371
|
+
members: Persons;
|
|
372
|
+
coaches: Persons;
|
|
373
|
+
contestID: string;
|
|
374
|
+
contestName: string;
|
|
375
|
+
contestLink: string;
|
|
376
|
+
contestTime: dayjs.Dayjs;
|
|
377
|
+
constructor();
|
|
378
|
+
toJSON(): IRatingHistory;
|
|
379
|
+
static fromJSON(iRatingHistory: IRatingHistory | string): RatingHistory;
|
|
380
|
+
}
|
|
381
|
+
type RatingHistories = Array<RatingHistory>;
|
|
382
|
+
|
|
383
|
+
declare class RatingUser {
|
|
384
|
+
id: string;
|
|
385
|
+
name: string;
|
|
386
|
+
organization: string;
|
|
387
|
+
members: Persons;
|
|
388
|
+
coaches: Persons;
|
|
389
|
+
rating: number;
|
|
390
|
+
minRating: number;
|
|
391
|
+
maxRating: number;
|
|
392
|
+
rank: number;
|
|
393
|
+
oldRating: number;
|
|
394
|
+
seed: number;
|
|
395
|
+
delta: number;
|
|
396
|
+
ratingHistories: RatingHistories;
|
|
397
|
+
constructor();
|
|
398
|
+
UpdateRating(rating: number): void;
|
|
399
|
+
toJSON(): IRatingUser;
|
|
400
|
+
static fromJSON(iRatingUser: IRatingUser | string): RatingUser;
|
|
401
|
+
}
|
|
402
|
+
type RatingUsers = Array<RatingUser>;
|
|
403
|
+
type RatingUserMap = Map<string, RatingUser>;
|
|
404
|
+
|
|
405
|
+
declare class RatingCalculator {
|
|
406
|
+
users: RatingUsers;
|
|
407
|
+
constructor();
|
|
408
|
+
calculate(): void;
|
|
409
|
+
private calcP;
|
|
410
|
+
private getExSeed;
|
|
411
|
+
private calcRating;
|
|
412
|
+
private calculateInternal;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
declare class Rating {
|
|
416
|
+
id: string;
|
|
417
|
+
name: string;
|
|
418
|
+
baseRating: number;
|
|
419
|
+
contestIDs: string[];
|
|
420
|
+
users: RatingUsers;
|
|
421
|
+
ranks: Ranks;
|
|
422
|
+
userMap: RatingUserMap;
|
|
423
|
+
constructor();
|
|
424
|
+
buildRating(): void;
|
|
425
|
+
generateTeamId(t: Team): string;
|
|
426
|
+
toJSON(): IRating;
|
|
427
|
+
static fromJSON(iRating: IRating | string): Rating;
|
|
428
|
+
}
|
|
429
|
+
|
|
355
430
|
declare class ContestIndexConfig {
|
|
356
431
|
contestName: string;
|
|
357
432
|
startTime: dayjs.Dayjs;
|
|
@@ -398,4 +473,4 @@ declare function isRejected(status: SubmissionStatus): boolean;
|
|
|
398
473
|
declare function isPending(status: SubmissionStatus): boolean;
|
|
399
474
|
declare function isNotCalculatedPenaltyStatus(status: SubmissionStatus): boolean;
|
|
400
475
|
|
|
401
|
-
export { Award, Awards, Balloon, Balloons, BattleOfGiants, CodeforcesGymGhostDATConverter, Contest, ContestIndex, ContestIndexConfig, ContestIndexList, ContestOptions, GeneralExcelConverter, Giants, GiantsType, ICPCStandingsCsvConverter, MedalType, PlaceChartPointData, Problem, ProblemStatistics, Problems, Rank, RankOptions, RankStatistics, Resolver, SelectOptionItem, Submission, Submissions, Team, TeamProblemStatistics, Teams, calcDirt, createContest, createContestIndex, createContestIndexList, createDayJS, createProblem, createProblems, createProblemsByProblemIds, createSubmission, createSubmissions, createTeam, createTeams, getImageSource, getTimeDiff, getTimestamp, getWhiteOrBlackColor, getWhiteOrBlackColorV1, isAccepted, isNotCalculatedPenaltyStatus, isPending, isRejected, isValidMedalType, stringToSubmissionStatus };
|
|
476
|
+
export { Award, Awards, Balloon, Balloons, BattleOfGiants, CodeforcesGymGhostDATConverter, Contest, ContestIndex, ContestIndexConfig, ContestIndexList, ContestOptions, GeneralExcelConverter, Giants, GiantsType, ICPCStandingsCsvConverter, MedalType, Person, Persons, PlaceChartPointData, Problem, ProblemStatistics, Problems, Rank, RankOptions, RankStatistics, Ranks, Rating, RatingCalculator, RatingHistories, RatingHistory, RatingUser, RatingUserMap, RatingUsers, Resolver, SelectOptionItem, Submission, Submissions, Team, TeamProblemStatistics, Teams, calcDirt, createContest, createContestIndex, createContestIndexList, createDayJS, createPersons, createProblem, createProblems, createProblemsByProblemIds, createSubmission, createSubmissions, createTeam, createTeams, getImageSource, getTimeDiff, getTimestamp, getWhiteOrBlackColor, getWhiteOrBlackColorV1, isAccepted, isNotCalculatedPenaltyStatus, isPending, isRejected, isValidMedalType, stringToSubmissionStatus };
|
package/dist/index.mjs
CHANGED
|
@@ -515,6 +515,36 @@ class ICPCStandingsCsvConverter {
|
|
|
515
515
|
}
|
|
516
516
|
}
|
|
517
517
|
|
|
518
|
+
class Person {
|
|
519
|
+
constructor(name = "") {
|
|
520
|
+
this.name = name;
|
|
521
|
+
}
|
|
522
|
+
toJSON() {
|
|
523
|
+
return {
|
|
524
|
+
name: this.name
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
static fromJSON(iPerson) {
|
|
528
|
+
if (typeof iPerson === "string") {
|
|
529
|
+
iPerson = JSON.parse(iPerson);
|
|
530
|
+
}
|
|
531
|
+
const person = new Person();
|
|
532
|
+
person.name = iPerson.name;
|
|
533
|
+
return person;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
function createPersons(iPersons) {
|
|
537
|
+
if (typeof iPersons === "string") {
|
|
538
|
+
for (const c of " ,\u3001|") {
|
|
539
|
+
if (iPersons.includes(c)) {
|
|
540
|
+
return iPersons.split(c).map((name) => new Person(name));
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
return [new Person(iPersons)];
|
|
544
|
+
}
|
|
545
|
+
return iPersons.map((name) => new Person(name));
|
|
546
|
+
}
|
|
547
|
+
|
|
518
548
|
function calcDirt(attemptedNum, solvedNum) {
|
|
519
549
|
if (solvedNum === 0) {
|
|
520
550
|
return 0;
|
|
@@ -543,6 +573,268 @@ function getWhiteOrBlackColor(background) {
|
|
|
543
573
|
return brightness <= threshold ? "#fff" : "#000";
|
|
544
574
|
}
|
|
545
575
|
|
|
576
|
+
class RatingHistory {
|
|
577
|
+
constructor() {
|
|
578
|
+
this.rank = 0;
|
|
579
|
+
this.rating = 0;
|
|
580
|
+
this.teamName = "";
|
|
581
|
+
this.organization = "";
|
|
582
|
+
this.members = [];
|
|
583
|
+
this.coaches = [];
|
|
584
|
+
this.contestID = "";
|
|
585
|
+
this.contestName = "";
|
|
586
|
+
this.contestLink = "";
|
|
587
|
+
this.contestTime = createDayJS();
|
|
588
|
+
}
|
|
589
|
+
toJSON() {
|
|
590
|
+
return {
|
|
591
|
+
rank: this.rank,
|
|
592
|
+
rating: this.rating,
|
|
593
|
+
teamName: this.teamName,
|
|
594
|
+
organization: this.organization,
|
|
595
|
+
members: this.members.map((member) => member.toJSON()),
|
|
596
|
+
coaches: this.coaches.map((coach) => coach.toJSON()),
|
|
597
|
+
contestID: this.contestID,
|
|
598
|
+
contestName: this.contestName,
|
|
599
|
+
contestLink: this.contestLink,
|
|
600
|
+
contestTime: this.contestTime.toDate()
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
static fromJSON(iRatingHistory) {
|
|
604
|
+
if (typeof iRatingHistory === "string") {
|
|
605
|
+
iRatingHistory = JSON.parse(iRatingHistory);
|
|
606
|
+
}
|
|
607
|
+
const ratingHistory = new RatingHistory();
|
|
608
|
+
ratingHistory.rank = iRatingHistory.rank;
|
|
609
|
+
ratingHistory.rating = iRatingHistory.rating;
|
|
610
|
+
ratingHistory.teamName = iRatingHistory.teamName;
|
|
611
|
+
ratingHistory.organization = iRatingHistory.organization;
|
|
612
|
+
ratingHistory.members = iRatingHistory.members.map((iMember) => Person.fromJSON(iMember));
|
|
613
|
+
ratingHistory.coaches = iRatingHistory.coaches.map((iCoach) => Person.fromJSON(iCoach));
|
|
614
|
+
ratingHistory.contestID = iRatingHistory.contestID;
|
|
615
|
+
ratingHistory.contestName = iRatingHistory.contestName;
|
|
616
|
+
ratingHistory.contestLink = iRatingHistory.contestLink;
|
|
617
|
+
ratingHistory.contestTime = createDayJS(iRatingHistory.contestTime);
|
|
618
|
+
return ratingHistory;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
class RatingUser {
|
|
623
|
+
constructor() {
|
|
624
|
+
this.id = "";
|
|
625
|
+
this.name = "";
|
|
626
|
+
this.organization = "";
|
|
627
|
+
this.members = [];
|
|
628
|
+
this.coaches = [];
|
|
629
|
+
this.rating = 0;
|
|
630
|
+
this.minRating = 1061109567;
|
|
631
|
+
this.maxRating = -1061109567;
|
|
632
|
+
this.rank = 0;
|
|
633
|
+
this.oldRating = 0;
|
|
634
|
+
this.seed = 1;
|
|
635
|
+
this.delta = 0;
|
|
636
|
+
this.ratingHistories = [];
|
|
637
|
+
}
|
|
638
|
+
UpdateRating(rating) {
|
|
639
|
+
this.rating = rating;
|
|
640
|
+
this.minRating = Math.min(this.minRating, rating);
|
|
641
|
+
this.maxRating = Math.max(this.maxRating, rating);
|
|
642
|
+
}
|
|
643
|
+
toJSON() {
|
|
644
|
+
return {
|
|
645
|
+
id: this.id,
|
|
646
|
+
name: this.name,
|
|
647
|
+
organization: this.organization,
|
|
648
|
+
members: this.members.map((member) => member.toJSON()),
|
|
649
|
+
coaches: this.coaches.map((coach) => coach.toJSON()),
|
|
650
|
+
rating: this.rating,
|
|
651
|
+
minRating: this.minRating,
|
|
652
|
+
maxRating: this.maxRating,
|
|
653
|
+
ratingHistories: this.ratingHistories.map((ratingHistory) => ratingHistory.toJSON())
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
static fromJSON(iRatingUser) {
|
|
657
|
+
if (typeof iRatingUser === "string") {
|
|
658
|
+
iRatingUser = JSON.parse(iRatingUser);
|
|
659
|
+
}
|
|
660
|
+
const ratingUser = new RatingUser();
|
|
661
|
+
ratingUser.id = iRatingUser.id;
|
|
662
|
+
ratingUser.name = iRatingUser.name;
|
|
663
|
+
ratingUser.organization = iRatingUser.organization;
|
|
664
|
+
ratingUser.members = iRatingUser.members.map((member) => Person.fromJSON(member));
|
|
665
|
+
ratingUser.coaches = iRatingUser.coaches.map((coach) => Person.fromJSON(coach));
|
|
666
|
+
ratingUser.rating = iRatingUser.rating;
|
|
667
|
+
ratingUser.minRating = iRatingUser.minRating;
|
|
668
|
+
ratingUser.maxRating = iRatingUser.maxRating;
|
|
669
|
+
for (const iRatingHistory of iRatingUser.ratingHistories) {
|
|
670
|
+
ratingUser.ratingHistories.push(RatingHistory.fromJSON(iRatingHistory));
|
|
671
|
+
}
|
|
672
|
+
return ratingUser;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
class RatingCalculator {
|
|
677
|
+
constructor() {
|
|
678
|
+
this.users = [];
|
|
679
|
+
}
|
|
680
|
+
calculate() {
|
|
681
|
+
this.calculateInternal();
|
|
682
|
+
}
|
|
683
|
+
calcP(userA, userB) {
|
|
684
|
+
return 1 / (1 + 10 ** ((userB.oldRating - userA.oldRating) / 400));
|
|
685
|
+
}
|
|
686
|
+
getExSeed(users, rating, ownUser) {
|
|
687
|
+
const exUser = new RatingUser();
|
|
688
|
+
exUser.oldRating = rating;
|
|
689
|
+
let res = 0;
|
|
690
|
+
users.forEach((user) => {
|
|
691
|
+
if (user.id !== ownUser.id) {
|
|
692
|
+
res += this.calcP(user, exUser);
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
return res;
|
|
696
|
+
}
|
|
697
|
+
calcRating(users, rank, user) {
|
|
698
|
+
let left = 1;
|
|
699
|
+
let right = 8e3;
|
|
700
|
+
while (right - left > 1) {
|
|
701
|
+
const mid = Math.floor((left + right) / 2);
|
|
702
|
+
if (this.getExSeed(users, mid, user) < rank) {
|
|
703
|
+
right = mid;
|
|
704
|
+
} else {
|
|
705
|
+
left = mid;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
return left;
|
|
709
|
+
}
|
|
710
|
+
calculateInternal() {
|
|
711
|
+
for (let i = 0; i < this.users.length; i++) {
|
|
712
|
+
const u = this.users[i];
|
|
713
|
+
u.seed = 1;
|
|
714
|
+
for (let j = 0; j < this.users.length; j++) {
|
|
715
|
+
if (i !== j) {
|
|
716
|
+
const otherUser = this.users[j];
|
|
717
|
+
u.seed += this.calcP(otherUser, u);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
let sumDelta = 0;
|
|
722
|
+
for (let i = 0; i < this.users.length; i++) {
|
|
723
|
+
const u = this.users[i];
|
|
724
|
+
u.delta = Math.floor(
|
|
725
|
+
(this.calcRating(this.users, Math.sqrt(u.rank * u.seed), u) - u.oldRating) / 2
|
|
726
|
+
);
|
|
727
|
+
sumDelta += u.delta;
|
|
728
|
+
}
|
|
729
|
+
let inc = Math.floor(-sumDelta / this.users.length) - 1;
|
|
730
|
+
for (let i = 0; i < this.users.length; i++) {
|
|
731
|
+
const u = this.users[i];
|
|
732
|
+
u.delta += inc;
|
|
733
|
+
}
|
|
734
|
+
this.users = this.users.sort((a, b) => b.oldRating - a.oldRating);
|
|
735
|
+
const s = Math.min(this.users.length, Math.floor(4 * Math.round(Math.sqrt(this.users.length))));
|
|
736
|
+
let sumS = 0;
|
|
737
|
+
for (let i = 0; i < s; i++) {
|
|
738
|
+
sumS += this.users[i].delta;
|
|
739
|
+
}
|
|
740
|
+
inc = Math.min(Math.max(Math.floor(-sumS / s), -10), 0);
|
|
741
|
+
this.users.forEach((u) => {
|
|
742
|
+
u.delta += inc;
|
|
743
|
+
u.UpdateRating(u.oldRating + u.delta);
|
|
744
|
+
});
|
|
745
|
+
this.users = this.users.sort((a, b) => a.rank - b.rank);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
class Rating {
|
|
750
|
+
constructor() {
|
|
751
|
+
this.id = "";
|
|
752
|
+
this.name = "";
|
|
753
|
+
this.baseRating = 1500;
|
|
754
|
+
this.contestIDs = [];
|
|
755
|
+
this.users = [];
|
|
756
|
+
this.ranks = [];
|
|
757
|
+
this.userMap = /* @__PURE__ */ new Map();
|
|
758
|
+
}
|
|
759
|
+
buildRating() {
|
|
760
|
+
for (const rank of this.ranks) {
|
|
761
|
+
rank.buildRank();
|
|
762
|
+
const ratingCalculator = new RatingCalculator();
|
|
763
|
+
for (const t of rank.teams) {
|
|
764
|
+
const id = this.generateTeamId(t);
|
|
765
|
+
let u = null;
|
|
766
|
+
if (!this.userMap.has(id)) {
|
|
767
|
+
u = new RatingUser();
|
|
768
|
+
u.id = id;
|
|
769
|
+
u.name = t.name;
|
|
770
|
+
u.organization = t.organization;
|
|
771
|
+
u.members = createPersons(t.members ?? []);
|
|
772
|
+
u.coaches = createPersons(t.coach ?? []);
|
|
773
|
+
u.rank = t.rank;
|
|
774
|
+
u.oldRating = this.baseRating;
|
|
775
|
+
u.UpdateRating(this.baseRating);
|
|
776
|
+
this.userMap.set(id, u);
|
|
777
|
+
this.users.push(u);
|
|
778
|
+
ratingCalculator.users.push(u);
|
|
779
|
+
} else {
|
|
780
|
+
u = this.userMap.get(id);
|
|
781
|
+
u.rank = t.rank;
|
|
782
|
+
u.oldRating = u.rating;
|
|
783
|
+
ratingCalculator.users.push(u);
|
|
784
|
+
}
|
|
785
|
+
{
|
|
786
|
+
const h = new RatingHistory();
|
|
787
|
+
h.rank = t.rank;
|
|
788
|
+
h.rating = u.rating;
|
|
789
|
+
h.teamName = t.name;
|
|
790
|
+
h.organization = t.organization;
|
|
791
|
+
h.members = createPersons(t.members ?? []);
|
|
792
|
+
h.coaches = createPersons(t.coach ?? []);
|
|
793
|
+
h.contestID = rank.contest.id;
|
|
794
|
+
h.contestLink = h.contestID;
|
|
795
|
+
h.contestName = rank.contest.name;
|
|
796
|
+
h.contestTime = rank.contest.startTime;
|
|
797
|
+
u.ratingHistories.push(h);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
ratingCalculator.calculate();
|
|
801
|
+
for (const u of ratingCalculator.users) {
|
|
802
|
+
u.ratingHistories.at(-1).rating = u.rating;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
generateTeamId(t) {
|
|
807
|
+
const persons = createPersons(t.members ?? []);
|
|
808
|
+
if (persons.length > 0) {
|
|
809
|
+
return persons.map((person) => person.name).join("|");
|
|
810
|
+
}
|
|
811
|
+
return `${t.organization}-${t.name}`;
|
|
812
|
+
}
|
|
813
|
+
toJSON() {
|
|
814
|
+
return {
|
|
815
|
+
id: this.id,
|
|
816
|
+
name: this.name,
|
|
817
|
+
baseRating: this.baseRating,
|
|
818
|
+
contestIDs: this.contestIDs,
|
|
819
|
+
users: this.users.map((ratingUser) => ratingUser.toJSON())
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
static fromJSON(iRating) {
|
|
823
|
+
if (typeof iRating === "string") {
|
|
824
|
+
iRating = JSON.parse(iRating);
|
|
825
|
+
}
|
|
826
|
+
const rating = new Rating();
|
|
827
|
+
rating.id = iRating.id;
|
|
828
|
+
rating.name = iRating.name;
|
|
829
|
+
rating.baseRating = iRating.baseRating;
|
|
830
|
+
rating.contestIDs = iRating.contestIDs;
|
|
831
|
+
for (const iUser of iRating.users) {
|
|
832
|
+
rating.users.push(RatingUser.fromJSON(iUser));
|
|
833
|
+
}
|
|
834
|
+
return rating;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
546
838
|
class ProblemStatistics {
|
|
547
839
|
constructor() {
|
|
548
840
|
this.acceptedNum = 0;
|
|
@@ -1150,6 +1442,7 @@ function createContestOptions(contestOptionsJSON = {}) {
|
|
|
1150
1442
|
|
|
1151
1443
|
class Contest {
|
|
1152
1444
|
constructor() {
|
|
1445
|
+
this.id = "";
|
|
1153
1446
|
this.name = "";
|
|
1154
1447
|
this.startTime = createDayJS();
|
|
1155
1448
|
this.endTime = createDayJS();
|
|
@@ -1989,4 +2282,4 @@ class Resolver extends Rank {
|
|
|
1989
2282
|
}
|
|
1990
2283
|
}
|
|
1991
2284
|
|
|
1992
|
-
export { Award, Balloon, BattleOfGiants, CodeforcesGymGhostDATConverter, Contest, ContestIndex, ContestIndexConfig, ContestOptions, GeneralExcelConverter, Giants, GiantsType, ICPCStandingsCsvConverter, MedalType, PlaceChartPointData, Problem, ProblemStatistics, Rank, RankOptions, RankStatistics, Resolver, Submission, Team, TeamProblemStatistics, calcDirt, createContest, createContestIndex, createContestIndexList, createDayJS, createProblem, createProblems, createProblemsByProblemIds, createSubmission, createSubmissions, createTeam, createTeams, getImageSource, getTimeDiff, getTimestamp, getWhiteOrBlackColor, getWhiteOrBlackColorV1, isAccepted, isNotCalculatedPenaltyStatus, isPending, isRejected, isValidMedalType, stringToSubmissionStatus };
|
|
2285
|
+
export { Award, Balloon, BattleOfGiants, CodeforcesGymGhostDATConverter, Contest, ContestIndex, ContestIndexConfig, ContestOptions, GeneralExcelConverter, Giants, GiantsType, ICPCStandingsCsvConverter, MedalType, Person, PlaceChartPointData, Problem, ProblemStatistics, Rank, RankOptions, RankStatistics, Rating, RatingCalculator, RatingHistory, RatingUser, Resolver, Submission, Team, TeamProblemStatistics, calcDirt, createContest, createContestIndex, createContestIndexList, createDayJS, createPersons, createProblem, createProblems, createProblemsByProblemIds, createSubmission, createSubmissions, createTeam, createTeams, getImageSource, getTimeDiff, getTimestamp, getWhiteOrBlackColor, getWhiteOrBlackColorV1, isAccepted, isNotCalculatedPenaltyStatus, isPending, isRejected, isValidMedalType, stringToSubmissionStatus };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xcpcio/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.46.1",
|
|
4
4
|
"description": "XCPCIO Core",
|
|
5
5
|
"author": "Dup4 <lyuzhi.pan@gmail.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"papaparse": "^5.4.1",
|
|
50
50
|
"string-width": "^6.1.0",
|
|
51
51
|
"xlsx-js-style": "^1.2.0",
|
|
52
|
-
"@xcpcio/types": "0.
|
|
52
|
+
"@xcpcio/types": "0.46.1"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"@babel/types": "^7.22.4",
|
package/src/contest.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export * from "./export";
|
|
2
|
+
export * from "./rating";
|
|
2
3
|
export * from "./utils";
|
|
3
4
|
|
|
4
5
|
export * from "./award";
|
|
@@ -8,6 +9,7 @@ export * from "./battle-of-giants";
|
|
|
8
9
|
export * from "./contest-index";
|
|
9
10
|
export * from "./contest";
|
|
10
11
|
export * from "./image";
|
|
12
|
+
export * from "./person";
|
|
11
13
|
export * from "./problem";
|
|
12
14
|
export * from "./rank-statistics";
|
|
13
15
|
export * from "./rank";
|
package/src/person.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { IPerson } from "@xcpcio/types";
|
|
2
|
+
|
|
3
|
+
export class Person {
|
|
4
|
+
name: string;
|
|
5
|
+
|
|
6
|
+
constructor(name = "") {
|
|
7
|
+
this.name = name;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
toJSON(): IPerson {
|
|
11
|
+
return {
|
|
12
|
+
name: this.name,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
static fromJSON(iPerson: IPerson | string): Person {
|
|
17
|
+
if (typeof iPerson === "string") {
|
|
18
|
+
iPerson = JSON.parse(iPerson) as IPerson;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const person = new Person();
|
|
22
|
+
person.name = iPerson.name;
|
|
23
|
+
|
|
24
|
+
return person;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type Persons = Array<Person>;
|
|
29
|
+
|
|
30
|
+
export function createPersons(iPersons: string | Array<string>): Persons {
|
|
31
|
+
if (typeof iPersons === "string") {
|
|
32
|
+
for (const c of " ,、|") {
|
|
33
|
+
if (iPersons.includes(c)) {
|
|
34
|
+
return iPersons.split(c).map(name => new Person(name));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return [new Person(iPersons)];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return iPersons.map(name => new Person(name));
|
|
42
|
+
}
|
package/src/rank.ts
CHANGED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { RatingUsers } from "./rating-user";
|
|
2
|
+
import { RatingUser } from "./rating-user";
|
|
3
|
+
|
|
4
|
+
// https://www.wikiwand.com/en/Elo_rating_system
|
|
5
|
+
export class RatingCalculator {
|
|
6
|
+
users: RatingUsers;
|
|
7
|
+
|
|
8
|
+
constructor() {
|
|
9
|
+
this.users = [];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
calculate() {
|
|
13
|
+
this.calculateInternal();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
private calcP(userA: RatingUser, userB: RatingUser) {
|
|
17
|
+
return 1.0 / (1.0 + 10 ** ((userB.oldRating - userA.oldRating) / 400.0));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
private getExSeed(users: RatingUsers, rating: number, ownUser: RatingUser) {
|
|
21
|
+
const exUser = new RatingUser();
|
|
22
|
+
exUser.oldRating = rating;
|
|
23
|
+
|
|
24
|
+
let res = 0;
|
|
25
|
+
|
|
26
|
+
users.forEach((user) => {
|
|
27
|
+
if (user.id !== ownUser.id) {
|
|
28
|
+
res += this.calcP(user, exUser);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return res;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private calcRating(users: RatingUsers, rank: number, user: RatingUser) {
|
|
36
|
+
let left = 1;
|
|
37
|
+
let right = 8000;
|
|
38
|
+
|
|
39
|
+
while (right - left > 1) {
|
|
40
|
+
const mid = Math.floor((left + right) / 2);
|
|
41
|
+
if (this.getExSeed(users, mid, user) < rank) {
|
|
42
|
+
right = mid;
|
|
43
|
+
} else {
|
|
44
|
+
left = mid;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return left;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private calculateInternal() {
|
|
52
|
+
// Calculate seed
|
|
53
|
+
for (let i = 0; i < this.users.length; i++) {
|
|
54
|
+
const u = this.users[i];
|
|
55
|
+
u.seed = 1.0;
|
|
56
|
+
for (let j = 0; j < this.users.length; j++) {
|
|
57
|
+
if (i !== j) {
|
|
58
|
+
const otherUser = this.users[j];
|
|
59
|
+
u.seed += this.calcP(otherUser, u);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Calculate initial delta and sumDelta
|
|
65
|
+
let sumDelta = 0;
|
|
66
|
+
for (let i = 0; i < this.users.length; i++) {
|
|
67
|
+
const u = this.users[i];
|
|
68
|
+
u.delta = Math.floor(
|
|
69
|
+
(this.calcRating(this.users, Math.sqrt(u.rank * u.seed), u)
|
|
70
|
+
- u.oldRating)
|
|
71
|
+
/ 2,
|
|
72
|
+
);
|
|
73
|
+
sumDelta += u.delta;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Calculate first inc
|
|
77
|
+
let inc = Math.floor(-sumDelta / this.users.length) - 1;
|
|
78
|
+
for (let i = 0; i < this.users.length; i++) {
|
|
79
|
+
const u = this.users[i];
|
|
80
|
+
u.delta += inc;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Calculate second inc
|
|
84
|
+
this.users = this.users.sort((a, b) => b.oldRating - a.oldRating);
|
|
85
|
+
const s = Math.min(this.users.length, Math.floor(4 * Math.round(Math.sqrt(this.users.length))));
|
|
86
|
+
let sumS = 0;
|
|
87
|
+
for (let i = 0; i < s; i++) {
|
|
88
|
+
sumS += this.users[i].delta;
|
|
89
|
+
}
|
|
90
|
+
inc = Math.min(Math.max(Math.floor(-sumS / s), -10), 0);
|
|
91
|
+
|
|
92
|
+
// Calculate new rating
|
|
93
|
+
this.users.forEach((u) => {
|
|
94
|
+
u.delta += inc;
|
|
95
|
+
u.UpdateRating(u.oldRating + u.delta);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
this.users = this.users.sort((a, b) => a.rank - b.rank);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { IRatingHistory } from "@xcpcio/types";
|
|
2
|
+
|
|
3
|
+
import type { dayjs } from "../utils";
|
|
4
|
+
import { createDayJS } from "../utils";
|
|
5
|
+
import { Person } from "../person";
|
|
6
|
+
import type { Persons } from "../person";
|
|
7
|
+
|
|
8
|
+
export class RatingHistory {
|
|
9
|
+
rank: number;
|
|
10
|
+
rating: number;
|
|
11
|
+
|
|
12
|
+
teamName: string;
|
|
13
|
+
organization: string;
|
|
14
|
+
|
|
15
|
+
members: Persons;
|
|
16
|
+
coaches: Persons;
|
|
17
|
+
|
|
18
|
+
contestID: string;
|
|
19
|
+
contestName: string;
|
|
20
|
+
contestLink: string;
|
|
21
|
+
contestTime: dayjs.Dayjs;
|
|
22
|
+
|
|
23
|
+
constructor() {
|
|
24
|
+
this.rank = 0;
|
|
25
|
+
this.rating = 0;
|
|
26
|
+
|
|
27
|
+
this.teamName = "";
|
|
28
|
+
this.organization = "";
|
|
29
|
+
|
|
30
|
+
this.members = [];
|
|
31
|
+
this.coaches = [];
|
|
32
|
+
|
|
33
|
+
this.contestID = "";
|
|
34
|
+
this.contestName = "";
|
|
35
|
+
this.contestLink = "";
|
|
36
|
+
this.contestTime = createDayJS();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
toJSON(): IRatingHistory {
|
|
40
|
+
return {
|
|
41
|
+
rank: this.rank,
|
|
42
|
+
rating: this.rating,
|
|
43
|
+
|
|
44
|
+
teamName: this.teamName,
|
|
45
|
+
organization: this.organization,
|
|
46
|
+
|
|
47
|
+
members: this.members.map(member => member.toJSON()),
|
|
48
|
+
coaches: this.coaches.map(coach => coach.toJSON()),
|
|
49
|
+
|
|
50
|
+
contestID: this.contestID,
|
|
51
|
+
contestName: this.contestName,
|
|
52
|
+
contestLink: this.contestLink,
|
|
53
|
+
contestTime: this.contestTime.toDate(),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
static fromJSON(iRatingHistory: IRatingHistory | string): RatingHistory {
|
|
58
|
+
if (typeof iRatingHistory === "string") {
|
|
59
|
+
iRatingHistory = JSON.parse(iRatingHistory) as IRatingHistory;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const ratingHistory = new RatingHistory();
|
|
63
|
+
ratingHistory.rank = iRatingHistory.rank;
|
|
64
|
+
ratingHistory.rating = iRatingHistory.rating;
|
|
65
|
+
|
|
66
|
+
ratingHistory.teamName = iRatingHistory.teamName;
|
|
67
|
+
ratingHistory.organization = iRatingHistory.organization;
|
|
68
|
+
|
|
69
|
+
ratingHistory.members = iRatingHistory.members.map(iMember => Person.fromJSON(iMember));
|
|
70
|
+
ratingHistory.coaches = iRatingHistory.coaches.map(iCoach => Person.fromJSON(iCoach));
|
|
71
|
+
|
|
72
|
+
ratingHistory.contestID = iRatingHistory.contestID;
|
|
73
|
+
ratingHistory.contestName = iRatingHistory.contestName;
|
|
74
|
+
ratingHistory.contestLink = iRatingHistory.contestLink;
|
|
75
|
+
ratingHistory.contestTime = createDayJS(iRatingHistory.contestTime);
|
|
76
|
+
|
|
77
|
+
return ratingHistory;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export type RatingHistories = Array<RatingHistory>;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { IRatingUser } from "@xcpcio/types";
|
|
2
|
+
|
|
3
|
+
import type { Persons } from "../person";
|
|
4
|
+
import { Person } from "../person";
|
|
5
|
+
import { type RatingHistories, RatingHistory } from "./rating-history";
|
|
6
|
+
|
|
7
|
+
export class RatingUser {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
organization: string;
|
|
11
|
+
|
|
12
|
+
members: Persons;
|
|
13
|
+
coaches: Persons;
|
|
14
|
+
|
|
15
|
+
rating: number;
|
|
16
|
+
minRating: number;
|
|
17
|
+
maxRating: number;
|
|
18
|
+
|
|
19
|
+
rank: number;
|
|
20
|
+
oldRating: number;
|
|
21
|
+
|
|
22
|
+
seed: number;
|
|
23
|
+
delta: number;
|
|
24
|
+
|
|
25
|
+
ratingHistories: RatingHistories;
|
|
26
|
+
|
|
27
|
+
constructor() {
|
|
28
|
+
this.id = "";
|
|
29
|
+
this.name = "";
|
|
30
|
+
this.organization = "";
|
|
31
|
+
|
|
32
|
+
this.members = [];
|
|
33
|
+
this.coaches = [];
|
|
34
|
+
|
|
35
|
+
this.rating = 0;
|
|
36
|
+
this.minRating = 0x3F3F3F3F;
|
|
37
|
+
this.maxRating = -0x3F3F3F3F;
|
|
38
|
+
|
|
39
|
+
this.rank = 0;
|
|
40
|
+
this.oldRating = 0;
|
|
41
|
+
|
|
42
|
+
this.seed = 1.0;
|
|
43
|
+
this.delta = 0;
|
|
44
|
+
|
|
45
|
+
this.ratingHistories = [];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
UpdateRating(rating: number) {
|
|
49
|
+
this.rating = rating;
|
|
50
|
+
this.minRating = Math.min(this.minRating, rating);
|
|
51
|
+
this.maxRating = Math.max(this.maxRating, rating);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
toJSON(): IRatingUser {
|
|
55
|
+
return {
|
|
56
|
+
id: this.id,
|
|
57
|
+
name: this.name,
|
|
58
|
+
organization: this.organization,
|
|
59
|
+
|
|
60
|
+
members: this.members.map(member => member.toJSON()),
|
|
61
|
+
coaches: this.coaches.map(coach => coach.toJSON()),
|
|
62
|
+
|
|
63
|
+
rating: this.rating,
|
|
64
|
+
minRating: this.minRating,
|
|
65
|
+
maxRating: this.maxRating,
|
|
66
|
+
|
|
67
|
+
ratingHistories: this.ratingHistories.map(ratingHistory => ratingHistory.toJSON()),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
static fromJSON(iRatingUser: IRatingUser | string): RatingUser {
|
|
72
|
+
if (typeof iRatingUser === "string") {
|
|
73
|
+
iRatingUser = JSON.parse(iRatingUser) as IRatingUser;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const ratingUser = new RatingUser();
|
|
77
|
+
|
|
78
|
+
ratingUser.id = iRatingUser.id;
|
|
79
|
+
ratingUser.name = iRatingUser.name;
|
|
80
|
+
ratingUser.organization = iRatingUser.organization;
|
|
81
|
+
|
|
82
|
+
ratingUser.members = iRatingUser.members.map(member => Person.fromJSON(member));
|
|
83
|
+
ratingUser.coaches = iRatingUser.coaches.map(coach => Person.fromJSON(coach));
|
|
84
|
+
|
|
85
|
+
ratingUser.rating = iRatingUser.rating;
|
|
86
|
+
ratingUser.minRating = iRatingUser.minRating;
|
|
87
|
+
ratingUser.maxRating = iRatingUser.maxRating;
|
|
88
|
+
|
|
89
|
+
for (const iRatingHistory of iRatingUser.ratingHistories) {
|
|
90
|
+
ratingUser.ratingHistories.push(RatingHistory.fromJSON(iRatingHistory));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return ratingUser;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export type RatingUsers = Array<RatingUser>;
|
|
98
|
+
export type RatingUserMap = Map<string, RatingUser>;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import type { IRating } from "@xcpcio/types";
|
|
2
|
+
|
|
3
|
+
import type { Ranks } from "../rank";
|
|
4
|
+
import type { Team } from "../team";
|
|
5
|
+
import { createPersons } from "../person";
|
|
6
|
+
|
|
7
|
+
import { RatingCalculator } from "./rating-calculator";
|
|
8
|
+
import { RatingHistory } from "./rating-history";
|
|
9
|
+
import type { RatingUserMap, RatingUsers } from "./rating-user";
|
|
10
|
+
import { RatingUser } from "./rating-user";
|
|
11
|
+
|
|
12
|
+
export class Rating {
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
baseRating: number;
|
|
16
|
+
|
|
17
|
+
contestIDs: string[];
|
|
18
|
+
users: RatingUsers;
|
|
19
|
+
|
|
20
|
+
ranks: Ranks;
|
|
21
|
+
userMap: RatingUserMap;
|
|
22
|
+
|
|
23
|
+
constructor() {
|
|
24
|
+
this.id = "";
|
|
25
|
+
this.name = "";
|
|
26
|
+
this.baseRating = 1500;
|
|
27
|
+
|
|
28
|
+
this.contestIDs = [];
|
|
29
|
+
this.users = [];
|
|
30
|
+
|
|
31
|
+
this.ranks = [];
|
|
32
|
+
this.userMap = new Map<string, RatingUser>();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
buildRating() {
|
|
36
|
+
for (const rank of this.ranks) {
|
|
37
|
+
rank.buildRank();
|
|
38
|
+
|
|
39
|
+
const ratingCalculator = new RatingCalculator();
|
|
40
|
+
|
|
41
|
+
for (const t of rank.teams) {
|
|
42
|
+
const id = this.generateTeamId(t);
|
|
43
|
+
|
|
44
|
+
let u = null;
|
|
45
|
+
|
|
46
|
+
if (!this.userMap.has(id)) {
|
|
47
|
+
u = new RatingUser();
|
|
48
|
+
u.id = id;
|
|
49
|
+
u.name = t.name;
|
|
50
|
+
u.organization = t.organization;
|
|
51
|
+
|
|
52
|
+
u.members = createPersons(t.members ?? []);
|
|
53
|
+
u.coaches = createPersons(t.coach ?? []);
|
|
54
|
+
|
|
55
|
+
u.rank = t.rank;
|
|
56
|
+
u.oldRating = this.baseRating;
|
|
57
|
+
u.UpdateRating(this.baseRating);
|
|
58
|
+
|
|
59
|
+
this.userMap.set(id, u);
|
|
60
|
+
this.users.push(u);
|
|
61
|
+
|
|
62
|
+
ratingCalculator.users.push(u);
|
|
63
|
+
} else {
|
|
64
|
+
u = this.userMap.get(id)!;
|
|
65
|
+
u.rank = t.rank;
|
|
66
|
+
u.oldRating = u.rating;
|
|
67
|
+
ratingCalculator.users.push(u);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
{
|
|
71
|
+
const h = new RatingHistory();
|
|
72
|
+
h.rank = t.rank;
|
|
73
|
+
h.rating = u.rating;
|
|
74
|
+
|
|
75
|
+
h.teamName = t.name;
|
|
76
|
+
h.organization = t.organization;
|
|
77
|
+
|
|
78
|
+
h.members = createPersons(t.members ?? []);
|
|
79
|
+
h.coaches = createPersons(t.coach ?? []);
|
|
80
|
+
|
|
81
|
+
h.contestID = rank.contest.id;
|
|
82
|
+
h.contestLink = h.contestID;
|
|
83
|
+
h.contestName = rank.contest.name;
|
|
84
|
+
h.contestTime = rank.contest.startTime;
|
|
85
|
+
|
|
86
|
+
u.ratingHistories.push(h);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
ratingCalculator.calculate();
|
|
91
|
+
|
|
92
|
+
for (const u of ratingCalculator.users) {
|
|
93
|
+
u.ratingHistories.at(-1)!.rating = u.rating;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
generateTeamId(t: Team) {
|
|
99
|
+
const persons = createPersons(t.members ?? []);
|
|
100
|
+
if (persons.length > 0) {
|
|
101
|
+
return persons.map(person => person.name).join("|");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return `${t.organization}-${t.name}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
toJSON(): IRating {
|
|
108
|
+
return {
|
|
109
|
+
id: this.id,
|
|
110
|
+
name: this.name,
|
|
111
|
+
baseRating: this.baseRating,
|
|
112
|
+
|
|
113
|
+
contestIDs: this.contestIDs,
|
|
114
|
+
users: this.users.map(ratingUser => ratingUser.toJSON()),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
static fromJSON(iRating: IRating | string): Rating {
|
|
119
|
+
if (typeof iRating === "string") {
|
|
120
|
+
iRating = JSON.parse(iRating) as IRating;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const rating = new Rating();
|
|
124
|
+
|
|
125
|
+
rating.id = iRating.id;
|
|
126
|
+
rating.name = iRating.name;
|
|
127
|
+
rating.baseRating = iRating.baseRating;
|
|
128
|
+
|
|
129
|
+
rating.contestIDs = iRating.contestIDs;
|
|
130
|
+
|
|
131
|
+
for (const iUser of iRating.users) {
|
|
132
|
+
rating.users.push(RatingUser.fromJSON(iUser));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return rating;
|
|
136
|
+
}
|
|
137
|
+
}
|