@xcpcio/core 0.45.1 → 0.46.0
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 +232 -0
- package/dist/index.d.ts +62 -2
- package/dist/index.mjs +229 -1
- package/package.json +2 -2
- package/src/contest.ts +1 -0
- package/src/index.ts +1 -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 +55 -0
- package/src/rating/rating-user.ts +79 -0
- package/src/rating/rating.ts +114 -0
package/dist/index.cjs
CHANGED
|
@@ -576,6 +576,233 @@ function getWhiteOrBlackColor(background) {
|
|
|
576
576
|
return brightness <= threshold ? "#fff" : "#000";
|
|
577
577
|
}
|
|
578
578
|
|
|
579
|
+
class RatingHistory {
|
|
580
|
+
constructor() {
|
|
581
|
+
this.rank = 0;
|
|
582
|
+
this.rating = 0;
|
|
583
|
+
this.contestID = "";
|
|
584
|
+
this.contestName = "";
|
|
585
|
+
this.contestLink = "";
|
|
586
|
+
this.contestTime = createDayJS();
|
|
587
|
+
}
|
|
588
|
+
toJSON() {
|
|
589
|
+
return {
|
|
590
|
+
rank: this.rank,
|
|
591
|
+
rating: this.rating,
|
|
592
|
+
contestID: this.contestID,
|
|
593
|
+
contestName: this.contestName,
|
|
594
|
+
contestLink: this.contestLink,
|
|
595
|
+
contestTime: this.contestTime.toDate()
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
static fromJSON(iRatingHistory) {
|
|
599
|
+
if (typeof iRatingHistory === "string") {
|
|
600
|
+
iRatingHistory = JSON.parse(iRatingHistory);
|
|
601
|
+
}
|
|
602
|
+
const ratingHistory = new RatingHistory();
|
|
603
|
+
ratingHistory.rank = iRatingHistory.rank;
|
|
604
|
+
ratingHistory.rating = iRatingHistory.rating;
|
|
605
|
+
ratingHistory.contestID = iRatingHistory.contestID;
|
|
606
|
+
ratingHistory.contestName = iRatingHistory.contestName;
|
|
607
|
+
ratingHistory.contestLink = iRatingHistory.contestLink;
|
|
608
|
+
ratingHistory.contestTime = createDayJS(iRatingHistory.contestTime);
|
|
609
|
+
return ratingHistory;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
class RatingUser {
|
|
614
|
+
constructor() {
|
|
615
|
+
this.id = "";
|
|
616
|
+
this.name = "";
|
|
617
|
+
this.rating = 0;
|
|
618
|
+
this.minRating = 1061109567;
|
|
619
|
+
this.maxRating = -1061109567;
|
|
620
|
+
this.rank = 0;
|
|
621
|
+
this.oldRating = 0;
|
|
622
|
+
this.seed = 1;
|
|
623
|
+
this.delta = 0;
|
|
624
|
+
this.ratingHistories = [];
|
|
625
|
+
}
|
|
626
|
+
UpdateRating(rating) {
|
|
627
|
+
this.rating = rating;
|
|
628
|
+
this.minRating = Math.min(this.minRating, rating);
|
|
629
|
+
this.maxRating = Math.max(this.maxRating, rating);
|
|
630
|
+
}
|
|
631
|
+
toJSON() {
|
|
632
|
+
return {
|
|
633
|
+
id: this.id,
|
|
634
|
+
name: this.name,
|
|
635
|
+
rating: this.rating,
|
|
636
|
+
minRating: this.minRating,
|
|
637
|
+
maxRating: this.maxRating,
|
|
638
|
+
ratingHistories: this.ratingHistories.map((ratingHistory) => ratingHistory.toJSON())
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
static fromJSON(iRatingUser) {
|
|
642
|
+
if (typeof iRatingUser === "string") {
|
|
643
|
+
iRatingUser = JSON.parse(iRatingUser);
|
|
644
|
+
}
|
|
645
|
+
const ratingUser = new RatingUser();
|
|
646
|
+
ratingUser.id = iRatingUser.id;
|
|
647
|
+
ratingUser.name = iRatingUser.name;
|
|
648
|
+
ratingUser.rating = iRatingUser.rating;
|
|
649
|
+
ratingUser.minRating = iRatingUser.minRating;
|
|
650
|
+
ratingUser.maxRating = iRatingUser.maxRating;
|
|
651
|
+
for (const iRatingHistory of iRatingUser.ratingHistories) {
|
|
652
|
+
ratingUser.ratingHistories.push(RatingHistory.fromJSON(iRatingHistory));
|
|
653
|
+
}
|
|
654
|
+
return ratingUser;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
class RatingCalculator {
|
|
659
|
+
constructor() {
|
|
660
|
+
this.users = [];
|
|
661
|
+
}
|
|
662
|
+
calculate() {
|
|
663
|
+
this.calculateInternal();
|
|
664
|
+
}
|
|
665
|
+
calcP(userA, userB) {
|
|
666
|
+
return 1 / (1 + 10 ** ((userB.oldRating - userA.oldRating) / 400));
|
|
667
|
+
}
|
|
668
|
+
getExSeed(users, rating, ownUser) {
|
|
669
|
+
const exUser = new RatingUser();
|
|
670
|
+
exUser.oldRating = rating;
|
|
671
|
+
let res = 0;
|
|
672
|
+
users.forEach((user) => {
|
|
673
|
+
if (user.id !== ownUser.id) {
|
|
674
|
+
res += this.calcP(user, exUser);
|
|
675
|
+
}
|
|
676
|
+
});
|
|
677
|
+
return res;
|
|
678
|
+
}
|
|
679
|
+
calcRating(users, rank, user) {
|
|
680
|
+
let left = 1;
|
|
681
|
+
let right = 8e3;
|
|
682
|
+
while (right - left > 1) {
|
|
683
|
+
const mid = Math.floor((left + right) / 2);
|
|
684
|
+
if (this.getExSeed(users, mid, user) < rank) {
|
|
685
|
+
right = mid;
|
|
686
|
+
} else {
|
|
687
|
+
left = mid;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
return left;
|
|
691
|
+
}
|
|
692
|
+
calculateInternal() {
|
|
693
|
+
for (let i = 0; i < this.users.length; i++) {
|
|
694
|
+
const u = this.users[i];
|
|
695
|
+
u.seed = 1;
|
|
696
|
+
for (let j = 0; j < this.users.length; j++) {
|
|
697
|
+
if (i !== j) {
|
|
698
|
+
const otherUser = this.users[j];
|
|
699
|
+
u.seed += this.calcP(otherUser, u);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
let sumDelta = 0;
|
|
704
|
+
for (let i = 0; i < this.users.length; i++) {
|
|
705
|
+
const u = this.users[i];
|
|
706
|
+
u.delta = Math.floor(
|
|
707
|
+
(this.calcRating(this.users, Math.sqrt(u.rank * u.seed), u) - u.oldRating) / 2
|
|
708
|
+
);
|
|
709
|
+
sumDelta += u.delta;
|
|
710
|
+
}
|
|
711
|
+
let inc = Math.floor(-sumDelta / this.users.length) - 1;
|
|
712
|
+
for (let i = 0; i < this.users.length; i++) {
|
|
713
|
+
const u = this.users[i];
|
|
714
|
+
u.delta += inc;
|
|
715
|
+
}
|
|
716
|
+
this.users = this.users.sort((a, b) => b.oldRating - a.oldRating);
|
|
717
|
+
const s = Math.min(this.users.length, Math.floor(4 * Math.round(Math.sqrt(this.users.length))));
|
|
718
|
+
let sumS = 0;
|
|
719
|
+
for (let i = 0; i < s; i++) {
|
|
720
|
+
sumS += this.users[i].delta;
|
|
721
|
+
}
|
|
722
|
+
inc = Math.min(Math.max(Math.floor(-sumS / s), -10), 0);
|
|
723
|
+
this.users.forEach((u) => {
|
|
724
|
+
u.delta += inc;
|
|
725
|
+
u.UpdateRating(u.oldRating + u.delta);
|
|
726
|
+
});
|
|
727
|
+
this.users = this.users.sort((a, b) => a.rank - b.rank);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
class Rating {
|
|
732
|
+
constructor() {
|
|
733
|
+
this.id = "";
|
|
734
|
+
this.name = "";
|
|
735
|
+
this.baseRating = 1500;
|
|
736
|
+
this.contestIDs = [];
|
|
737
|
+
this.users = [];
|
|
738
|
+
this.ranks = [];
|
|
739
|
+
this.userMap = /* @__PURE__ */ new Map();
|
|
740
|
+
}
|
|
741
|
+
buildRating() {
|
|
742
|
+
for (const rank of this.ranks) {
|
|
743
|
+
rank.buildRank();
|
|
744
|
+
for (const u of this.users) {
|
|
745
|
+
u.oldRating = u.rating;
|
|
746
|
+
}
|
|
747
|
+
for (const t of rank.teams) {
|
|
748
|
+
const id = this.generateTeamId(t);
|
|
749
|
+
if (!this.userMap.has(id)) {
|
|
750
|
+
const u = new RatingUser();
|
|
751
|
+
u.id = id;
|
|
752
|
+
u.name = t.name;
|
|
753
|
+
u.rank = t.rank;
|
|
754
|
+
u.oldRating = this.baseRating;
|
|
755
|
+
u.UpdateRating(this.baseRating);
|
|
756
|
+
this.userMap.set(id, u);
|
|
757
|
+
this.users.push(u);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
const ratingCalculator = new RatingCalculator();
|
|
761
|
+
ratingCalculator.users = this.users;
|
|
762
|
+
ratingCalculator.calculate();
|
|
763
|
+
for (const u of this.users) {
|
|
764
|
+
const h = new RatingHistory();
|
|
765
|
+
h.rank = u.rank;
|
|
766
|
+
h.rating = u.rating;
|
|
767
|
+
h.contestID = rank.contest.id;
|
|
768
|
+
h.contestLink = h.contestID;
|
|
769
|
+
h.contestName = rank.contest.name;
|
|
770
|
+
h.contestTime = rank.contest.startTime;
|
|
771
|
+
u.ratingHistories.push(h);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
generateTeamId(t) {
|
|
776
|
+
if (Array.isArray(t.members) && t.members.length > 0) {
|
|
777
|
+
return t.members.join("|");
|
|
778
|
+
}
|
|
779
|
+
return `${t.organization}-${t.name}`;
|
|
780
|
+
}
|
|
781
|
+
toJSON() {
|
|
782
|
+
return {
|
|
783
|
+
id: this.id,
|
|
784
|
+
name: this.name,
|
|
785
|
+
baseRating: this.baseRating,
|
|
786
|
+
contestIDs: this.contestIDs,
|
|
787
|
+
users: this.users.map((ratingUser) => ratingUser.toJSON())
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
static fromJSON(iRating) {
|
|
791
|
+
if (typeof iRating === "string") {
|
|
792
|
+
iRating = JSON.parse(iRating);
|
|
793
|
+
}
|
|
794
|
+
const rating = new Rating();
|
|
795
|
+
rating.id = iRating.id;
|
|
796
|
+
rating.name = iRating.name;
|
|
797
|
+
rating.baseRating = iRating.baseRating;
|
|
798
|
+
rating.contestIDs = iRating.contestIDs;
|
|
799
|
+
for (const iUser of iRating.users) {
|
|
800
|
+
rating.users.push(RatingUser.fromJSON(iUser));
|
|
801
|
+
}
|
|
802
|
+
return rating;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
579
806
|
class ProblemStatistics {
|
|
580
807
|
constructor() {
|
|
581
808
|
this.acceptedNum = 0;
|
|
@@ -1183,6 +1410,7 @@ function createContestOptions(contestOptionsJSON = {}) {
|
|
|
1183
1410
|
|
|
1184
1411
|
class Contest {
|
|
1185
1412
|
constructor() {
|
|
1413
|
+
this.id = "";
|
|
1186
1414
|
this.name = "";
|
|
1187
1415
|
this.startTime = createDayJS();
|
|
1188
1416
|
this.endTime = createDayJS();
|
|
@@ -2042,6 +2270,10 @@ exports.ProblemStatistics = ProblemStatistics;
|
|
|
2042
2270
|
exports.Rank = Rank;
|
|
2043
2271
|
exports.RankOptions = RankOptions;
|
|
2044
2272
|
exports.RankStatistics = RankStatistics;
|
|
2273
|
+
exports.Rating = Rating;
|
|
2274
|
+
exports.RatingCalculator = RatingCalculator;
|
|
2275
|
+
exports.RatingHistory = RatingHistory;
|
|
2276
|
+
exports.RatingUser = RatingUser;
|
|
2045
2277
|
exports.Resolver = Resolver;
|
|
2046
2278
|
exports.Submission = Submission;
|
|
2047
2279
|
exports.Team = Team;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
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, IRatingHistory, 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';
|
|
5
|
+
import { IRatingUser, IRating } from '@xcpcio/types/index';
|
|
5
6
|
|
|
6
7
|
declare class Submission {
|
|
7
8
|
id: string;
|
|
@@ -126,6 +127,7 @@ declare class ContestOptions {
|
|
|
126
127
|
}
|
|
127
128
|
|
|
128
129
|
declare class Contest {
|
|
130
|
+
id: string;
|
|
129
131
|
name: string;
|
|
130
132
|
startTime: dayjs.Dayjs;
|
|
131
133
|
endTime: dayjs.Dayjs;
|
|
@@ -331,6 +333,7 @@ declare class Rank {
|
|
|
331
333
|
buildBalloons(): void;
|
|
332
334
|
setReplayTime(replayStartTimestamp: number): void;
|
|
333
335
|
}
|
|
336
|
+
type Ranks = Array<Rank>;
|
|
334
337
|
|
|
335
338
|
declare class CodeforcesGymGhostDATConverter {
|
|
336
339
|
constructor();
|
|
@@ -352,6 +355,63 @@ declare class ICPCStandingsCsvConverter {
|
|
|
352
355
|
private getMedalCitation;
|
|
353
356
|
}
|
|
354
357
|
|
|
358
|
+
declare class RatingHistory {
|
|
359
|
+
rank: number;
|
|
360
|
+
rating: number;
|
|
361
|
+
contestID: string;
|
|
362
|
+
contestName: string;
|
|
363
|
+
contestLink: string;
|
|
364
|
+
contestTime: dayjs.Dayjs;
|
|
365
|
+
constructor();
|
|
366
|
+
toJSON(): IRatingHistory;
|
|
367
|
+
static fromJSON(iRatingHistory: IRatingHistory | string): RatingHistory;
|
|
368
|
+
}
|
|
369
|
+
type RatingHistories = Array<RatingHistory>;
|
|
370
|
+
|
|
371
|
+
declare class RatingUser {
|
|
372
|
+
id: string;
|
|
373
|
+
name: string;
|
|
374
|
+
rating: number;
|
|
375
|
+
minRating: number;
|
|
376
|
+
maxRating: number;
|
|
377
|
+
rank: number;
|
|
378
|
+
oldRating: number;
|
|
379
|
+
seed: number;
|
|
380
|
+
delta: number;
|
|
381
|
+
ratingHistories: RatingHistories;
|
|
382
|
+
constructor();
|
|
383
|
+
UpdateRating(rating: number): void;
|
|
384
|
+
toJSON(): IRatingUser;
|
|
385
|
+
static fromJSON(iRatingUser: IRatingUser | string): RatingUser;
|
|
386
|
+
}
|
|
387
|
+
type RatingUsers = Array<RatingUser>;
|
|
388
|
+
type RatingUserMap = Map<string, RatingUser>;
|
|
389
|
+
|
|
390
|
+
declare class RatingCalculator {
|
|
391
|
+
users: RatingUsers;
|
|
392
|
+
constructor();
|
|
393
|
+
calculate(): void;
|
|
394
|
+
private calcP;
|
|
395
|
+
private getExSeed;
|
|
396
|
+
private calcRating;
|
|
397
|
+
private calculateInternal;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
declare class Rating {
|
|
401
|
+
id: string;
|
|
402
|
+
name: string;
|
|
403
|
+
baseRating: number;
|
|
404
|
+
contestIDs: string[];
|
|
405
|
+
users: RatingUsers;
|
|
406
|
+
ranks: Ranks;
|
|
407
|
+
userMap: RatingUserMap;
|
|
408
|
+
constructor();
|
|
409
|
+
buildRating(): void;
|
|
410
|
+
generateTeamId(t: Team): string;
|
|
411
|
+
toJSON(): IRating;
|
|
412
|
+
static fromJSON(iRating: IRating | string): Rating;
|
|
413
|
+
}
|
|
414
|
+
|
|
355
415
|
declare class ContestIndexConfig {
|
|
356
416
|
contestName: string;
|
|
357
417
|
startTime: dayjs.Dayjs;
|
|
@@ -398,4 +458,4 @@ declare function isRejected(status: SubmissionStatus): boolean;
|
|
|
398
458
|
declare function isPending(status: SubmissionStatus): boolean;
|
|
399
459
|
declare function isNotCalculatedPenaltyStatus(status: SubmissionStatus): boolean;
|
|
400
460
|
|
|
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 };
|
|
461
|
+
export { Award, Awards, Balloon, Balloons, BattleOfGiants, CodeforcesGymGhostDATConverter, Contest, ContestIndex, ContestIndexConfig, ContestIndexList, ContestOptions, GeneralExcelConverter, Giants, GiantsType, ICPCStandingsCsvConverter, MedalType, 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, createProblem, createProblems, createProblemsByProblemIds, createSubmission, createSubmissions, createTeam, createTeams, getImageSource, getTimeDiff, getTimestamp, getWhiteOrBlackColor, getWhiteOrBlackColorV1, isAccepted, isNotCalculatedPenaltyStatus, isPending, isRejected, isValidMedalType, stringToSubmissionStatus };
|
package/dist/index.mjs
CHANGED
|
@@ -543,6 +543,233 @@ function getWhiteOrBlackColor(background) {
|
|
|
543
543
|
return brightness <= threshold ? "#fff" : "#000";
|
|
544
544
|
}
|
|
545
545
|
|
|
546
|
+
class RatingHistory {
|
|
547
|
+
constructor() {
|
|
548
|
+
this.rank = 0;
|
|
549
|
+
this.rating = 0;
|
|
550
|
+
this.contestID = "";
|
|
551
|
+
this.contestName = "";
|
|
552
|
+
this.contestLink = "";
|
|
553
|
+
this.contestTime = createDayJS();
|
|
554
|
+
}
|
|
555
|
+
toJSON() {
|
|
556
|
+
return {
|
|
557
|
+
rank: this.rank,
|
|
558
|
+
rating: this.rating,
|
|
559
|
+
contestID: this.contestID,
|
|
560
|
+
contestName: this.contestName,
|
|
561
|
+
contestLink: this.contestLink,
|
|
562
|
+
contestTime: this.contestTime.toDate()
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
static fromJSON(iRatingHistory) {
|
|
566
|
+
if (typeof iRatingHistory === "string") {
|
|
567
|
+
iRatingHistory = JSON.parse(iRatingHistory);
|
|
568
|
+
}
|
|
569
|
+
const ratingHistory = new RatingHistory();
|
|
570
|
+
ratingHistory.rank = iRatingHistory.rank;
|
|
571
|
+
ratingHistory.rating = iRatingHistory.rating;
|
|
572
|
+
ratingHistory.contestID = iRatingHistory.contestID;
|
|
573
|
+
ratingHistory.contestName = iRatingHistory.contestName;
|
|
574
|
+
ratingHistory.contestLink = iRatingHistory.contestLink;
|
|
575
|
+
ratingHistory.contestTime = createDayJS(iRatingHistory.contestTime);
|
|
576
|
+
return ratingHistory;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
class RatingUser {
|
|
581
|
+
constructor() {
|
|
582
|
+
this.id = "";
|
|
583
|
+
this.name = "";
|
|
584
|
+
this.rating = 0;
|
|
585
|
+
this.minRating = 1061109567;
|
|
586
|
+
this.maxRating = -1061109567;
|
|
587
|
+
this.rank = 0;
|
|
588
|
+
this.oldRating = 0;
|
|
589
|
+
this.seed = 1;
|
|
590
|
+
this.delta = 0;
|
|
591
|
+
this.ratingHistories = [];
|
|
592
|
+
}
|
|
593
|
+
UpdateRating(rating) {
|
|
594
|
+
this.rating = rating;
|
|
595
|
+
this.minRating = Math.min(this.minRating, rating);
|
|
596
|
+
this.maxRating = Math.max(this.maxRating, rating);
|
|
597
|
+
}
|
|
598
|
+
toJSON() {
|
|
599
|
+
return {
|
|
600
|
+
id: this.id,
|
|
601
|
+
name: this.name,
|
|
602
|
+
rating: this.rating,
|
|
603
|
+
minRating: this.minRating,
|
|
604
|
+
maxRating: this.maxRating,
|
|
605
|
+
ratingHistories: this.ratingHistories.map((ratingHistory) => ratingHistory.toJSON())
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
static fromJSON(iRatingUser) {
|
|
609
|
+
if (typeof iRatingUser === "string") {
|
|
610
|
+
iRatingUser = JSON.parse(iRatingUser);
|
|
611
|
+
}
|
|
612
|
+
const ratingUser = new RatingUser();
|
|
613
|
+
ratingUser.id = iRatingUser.id;
|
|
614
|
+
ratingUser.name = iRatingUser.name;
|
|
615
|
+
ratingUser.rating = iRatingUser.rating;
|
|
616
|
+
ratingUser.minRating = iRatingUser.minRating;
|
|
617
|
+
ratingUser.maxRating = iRatingUser.maxRating;
|
|
618
|
+
for (const iRatingHistory of iRatingUser.ratingHistories) {
|
|
619
|
+
ratingUser.ratingHistories.push(RatingHistory.fromJSON(iRatingHistory));
|
|
620
|
+
}
|
|
621
|
+
return ratingUser;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
class RatingCalculator {
|
|
626
|
+
constructor() {
|
|
627
|
+
this.users = [];
|
|
628
|
+
}
|
|
629
|
+
calculate() {
|
|
630
|
+
this.calculateInternal();
|
|
631
|
+
}
|
|
632
|
+
calcP(userA, userB) {
|
|
633
|
+
return 1 / (1 + 10 ** ((userB.oldRating - userA.oldRating) / 400));
|
|
634
|
+
}
|
|
635
|
+
getExSeed(users, rating, ownUser) {
|
|
636
|
+
const exUser = new RatingUser();
|
|
637
|
+
exUser.oldRating = rating;
|
|
638
|
+
let res = 0;
|
|
639
|
+
users.forEach((user) => {
|
|
640
|
+
if (user.id !== ownUser.id) {
|
|
641
|
+
res += this.calcP(user, exUser);
|
|
642
|
+
}
|
|
643
|
+
});
|
|
644
|
+
return res;
|
|
645
|
+
}
|
|
646
|
+
calcRating(users, rank, user) {
|
|
647
|
+
let left = 1;
|
|
648
|
+
let right = 8e3;
|
|
649
|
+
while (right - left > 1) {
|
|
650
|
+
const mid = Math.floor((left + right) / 2);
|
|
651
|
+
if (this.getExSeed(users, mid, user) < rank) {
|
|
652
|
+
right = mid;
|
|
653
|
+
} else {
|
|
654
|
+
left = mid;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
return left;
|
|
658
|
+
}
|
|
659
|
+
calculateInternal() {
|
|
660
|
+
for (let i = 0; i < this.users.length; i++) {
|
|
661
|
+
const u = this.users[i];
|
|
662
|
+
u.seed = 1;
|
|
663
|
+
for (let j = 0; j < this.users.length; j++) {
|
|
664
|
+
if (i !== j) {
|
|
665
|
+
const otherUser = this.users[j];
|
|
666
|
+
u.seed += this.calcP(otherUser, u);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
let sumDelta = 0;
|
|
671
|
+
for (let i = 0; i < this.users.length; i++) {
|
|
672
|
+
const u = this.users[i];
|
|
673
|
+
u.delta = Math.floor(
|
|
674
|
+
(this.calcRating(this.users, Math.sqrt(u.rank * u.seed), u) - u.oldRating) / 2
|
|
675
|
+
);
|
|
676
|
+
sumDelta += u.delta;
|
|
677
|
+
}
|
|
678
|
+
let inc = Math.floor(-sumDelta / this.users.length) - 1;
|
|
679
|
+
for (let i = 0; i < this.users.length; i++) {
|
|
680
|
+
const u = this.users[i];
|
|
681
|
+
u.delta += inc;
|
|
682
|
+
}
|
|
683
|
+
this.users = this.users.sort((a, b) => b.oldRating - a.oldRating);
|
|
684
|
+
const s = Math.min(this.users.length, Math.floor(4 * Math.round(Math.sqrt(this.users.length))));
|
|
685
|
+
let sumS = 0;
|
|
686
|
+
for (let i = 0; i < s; i++) {
|
|
687
|
+
sumS += this.users[i].delta;
|
|
688
|
+
}
|
|
689
|
+
inc = Math.min(Math.max(Math.floor(-sumS / s), -10), 0);
|
|
690
|
+
this.users.forEach((u) => {
|
|
691
|
+
u.delta += inc;
|
|
692
|
+
u.UpdateRating(u.oldRating + u.delta);
|
|
693
|
+
});
|
|
694
|
+
this.users = this.users.sort((a, b) => a.rank - b.rank);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
class Rating {
|
|
699
|
+
constructor() {
|
|
700
|
+
this.id = "";
|
|
701
|
+
this.name = "";
|
|
702
|
+
this.baseRating = 1500;
|
|
703
|
+
this.contestIDs = [];
|
|
704
|
+
this.users = [];
|
|
705
|
+
this.ranks = [];
|
|
706
|
+
this.userMap = /* @__PURE__ */ new Map();
|
|
707
|
+
}
|
|
708
|
+
buildRating() {
|
|
709
|
+
for (const rank of this.ranks) {
|
|
710
|
+
rank.buildRank();
|
|
711
|
+
for (const u of this.users) {
|
|
712
|
+
u.oldRating = u.rating;
|
|
713
|
+
}
|
|
714
|
+
for (const t of rank.teams) {
|
|
715
|
+
const id = this.generateTeamId(t);
|
|
716
|
+
if (!this.userMap.has(id)) {
|
|
717
|
+
const u = new RatingUser();
|
|
718
|
+
u.id = id;
|
|
719
|
+
u.name = t.name;
|
|
720
|
+
u.rank = t.rank;
|
|
721
|
+
u.oldRating = this.baseRating;
|
|
722
|
+
u.UpdateRating(this.baseRating);
|
|
723
|
+
this.userMap.set(id, u);
|
|
724
|
+
this.users.push(u);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
const ratingCalculator = new RatingCalculator();
|
|
728
|
+
ratingCalculator.users = this.users;
|
|
729
|
+
ratingCalculator.calculate();
|
|
730
|
+
for (const u of this.users) {
|
|
731
|
+
const h = new RatingHistory();
|
|
732
|
+
h.rank = u.rank;
|
|
733
|
+
h.rating = u.rating;
|
|
734
|
+
h.contestID = rank.contest.id;
|
|
735
|
+
h.contestLink = h.contestID;
|
|
736
|
+
h.contestName = rank.contest.name;
|
|
737
|
+
h.contestTime = rank.contest.startTime;
|
|
738
|
+
u.ratingHistories.push(h);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
generateTeamId(t) {
|
|
743
|
+
if (Array.isArray(t.members) && t.members.length > 0) {
|
|
744
|
+
return t.members.join("|");
|
|
745
|
+
}
|
|
746
|
+
return `${t.organization}-${t.name}`;
|
|
747
|
+
}
|
|
748
|
+
toJSON() {
|
|
749
|
+
return {
|
|
750
|
+
id: this.id,
|
|
751
|
+
name: this.name,
|
|
752
|
+
baseRating: this.baseRating,
|
|
753
|
+
contestIDs: this.contestIDs,
|
|
754
|
+
users: this.users.map((ratingUser) => ratingUser.toJSON())
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
static fromJSON(iRating) {
|
|
758
|
+
if (typeof iRating === "string") {
|
|
759
|
+
iRating = JSON.parse(iRating);
|
|
760
|
+
}
|
|
761
|
+
const rating = new Rating();
|
|
762
|
+
rating.id = iRating.id;
|
|
763
|
+
rating.name = iRating.name;
|
|
764
|
+
rating.baseRating = iRating.baseRating;
|
|
765
|
+
rating.contestIDs = iRating.contestIDs;
|
|
766
|
+
for (const iUser of iRating.users) {
|
|
767
|
+
rating.users.push(RatingUser.fromJSON(iUser));
|
|
768
|
+
}
|
|
769
|
+
return rating;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
546
773
|
class ProblemStatistics {
|
|
547
774
|
constructor() {
|
|
548
775
|
this.acceptedNum = 0;
|
|
@@ -1150,6 +1377,7 @@ function createContestOptions(contestOptionsJSON = {}) {
|
|
|
1150
1377
|
|
|
1151
1378
|
class Contest {
|
|
1152
1379
|
constructor() {
|
|
1380
|
+
this.id = "";
|
|
1153
1381
|
this.name = "";
|
|
1154
1382
|
this.startTime = createDayJS();
|
|
1155
1383
|
this.endTime = createDayJS();
|
|
@@ -1989,4 +2217,4 @@ class Resolver extends Rank {
|
|
|
1989
2217
|
}
|
|
1990
2218
|
}
|
|
1991
2219
|
|
|
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 };
|
|
2220
|
+
export { Award, Balloon, BattleOfGiants, CodeforcesGymGhostDATConverter, Contest, ContestIndex, ContestIndexConfig, ContestOptions, GeneralExcelConverter, Giants, GiantsType, ICPCStandingsCsvConverter, MedalType, PlaceChartPointData, Problem, ProblemStatistics, Rank, RankOptions, RankStatistics, Rating, RatingCalculator, RatingHistory, RatingUser, 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 };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xcpcio/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.46.0",
|
|
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.0"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"@babel/types": "^7.22.4",
|
package/src/contest.ts
CHANGED
package/src/index.ts
CHANGED
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,55 @@
|
|
|
1
|
+
import type { IRatingHistory } from "@xcpcio/types";
|
|
2
|
+
|
|
3
|
+
import type { dayjs } from "../utils";
|
|
4
|
+
import { createDayJS } from "../utils";
|
|
5
|
+
|
|
6
|
+
export class RatingHistory {
|
|
7
|
+
rank: number;
|
|
8
|
+
rating: number;
|
|
9
|
+
|
|
10
|
+
contestID: string;
|
|
11
|
+
contestName: string;
|
|
12
|
+
contestLink: string;
|
|
13
|
+
contestTime: dayjs.Dayjs;
|
|
14
|
+
|
|
15
|
+
constructor() {
|
|
16
|
+
this.rank = 0;
|
|
17
|
+
this.rating = 0;
|
|
18
|
+
|
|
19
|
+
this.contestID = "";
|
|
20
|
+
this.contestName = "";
|
|
21
|
+
this.contestLink = "";
|
|
22
|
+
this.contestTime = createDayJS();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
toJSON(): IRatingHistory {
|
|
26
|
+
return {
|
|
27
|
+
rank: this.rank,
|
|
28
|
+
rating: this.rating,
|
|
29
|
+
|
|
30
|
+
contestID: this.contestID,
|
|
31
|
+
contestName: this.contestName,
|
|
32
|
+
contestLink: this.contestLink,
|
|
33
|
+
contestTime: this.contestTime.toDate(),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
static fromJSON(iRatingHistory: IRatingHistory | string): RatingHistory {
|
|
38
|
+
if (typeof iRatingHistory === "string") {
|
|
39
|
+
iRatingHistory = JSON.parse(iRatingHistory) as IRatingHistory;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const ratingHistory = new RatingHistory();
|
|
43
|
+
ratingHistory.rank = iRatingHistory.rank;
|
|
44
|
+
ratingHistory.rating = iRatingHistory.rating;
|
|
45
|
+
|
|
46
|
+
ratingHistory.contestID = iRatingHistory.contestID;
|
|
47
|
+
ratingHistory.contestName = iRatingHistory.contestName;
|
|
48
|
+
ratingHistory.contestLink = iRatingHistory.contestLink;
|
|
49
|
+
ratingHistory.contestTime = createDayJS(iRatingHistory.contestTime);
|
|
50
|
+
|
|
51
|
+
return ratingHistory;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type RatingHistories = Array<RatingHistory>;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { IRatingUser } from "@xcpcio/types/index";
|
|
2
|
+
import { type RatingHistories, RatingHistory } from "./rating-history";
|
|
3
|
+
|
|
4
|
+
export class RatingUser {
|
|
5
|
+
id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
|
|
8
|
+
rating: number;
|
|
9
|
+
minRating: number;
|
|
10
|
+
maxRating: number;
|
|
11
|
+
|
|
12
|
+
rank: number;
|
|
13
|
+
oldRating: number;
|
|
14
|
+
|
|
15
|
+
seed: number;
|
|
16
|
+
delta: number;
|
|
17
|
+
|
|
18
|
+
ratingHistories: RatingHistories;
|
|
19
|
+
|
|
20
|
+
constructor() {
|
|
21
|
+
this.id = "";
|
|
22
|
+
this.name = "";
|
|
23
|
+
|
|
24
|
+
this.rating = 0;
|
|
25
|
+
this.minRating = 0x3F3F3F3F;
|
|
26
|
+
this.maxRating = -0x3F3F3F3F;
|
|
27
|
+
|
|
28
|
+
this.rank = 0;
|
|
29
|
+
this.oldRating = 0;
|
|
30
|
+
|
|
31
|
+
this.seed = 1.0;
|
|
32
|
+
this.delta = 0;
|
|
33
|
+
|
|
34
|
+
this.ratingHistories = [];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
UpdateRating(rating: number) {
|
|
38
|
+
this.rating = rating;
|
|
39
|
+
this.minRating = Math.min(this.minRating, rating);
|
|
40
|
+
this.maxRating = Math.max(this.maxRating, rating);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
toJSON(): IRatingUser {
|
|
44
|
+
return {
|
|
45
|
+
id: this.id,
|
|
46
|
+
name: this.name,
|
|
47
|
+
|
|
48
|
+
rating: this.rating,
|
|
49
|
+
minRating: this.minRating,
|
|
50
|
+
maxRating: this.maxRating,
|
|
51
|
+
|
|
52
|
+
ratingHistories: this.ratingHistories.map(ratingHistory => ratingHistory.toJSON()),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
static fromJSON(iRatingUser: IRatingUser | string): RatingUser {
|
|
57
|
+
if (typeof iRatingUser === "string") {
|
|
58
|
+
iRatingUser = JSON.parse(iRatingUser) as IRatingUser;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const ratingUser = new RatingUser();
|
|
62
|
+
|
|
63
|
+
ratingUser.id = iRatingUser.id;
|
|
64
|
+
ratingUser.name = iRatingUser.name;
|
|
65
|
+
|
|
66
|
+
ratingUser.rating = iRatingUser.rating;
|
|
67
|
+
ratingUser.minRating = iRatingUser.minRating;
|
|
68
|
+
ratingUser.maxRating = iRatingUser.maxRating;
|
|
69
|
+
|
|
70
|
+
for (const iRatingHistory of iRatingUser.ratingHistories) {
|
|
71
|
+
ratingUser.ratingHistories.push(RatingHistory.fromJSON(iRatingHistory));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return ratingUser;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export type RatingUsers = Array<RatingUser>;
|
|
79
|
+
export type RatingUserMap = Map<string, RatingUser>;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { IRating } from "@xcpcio/types/index";
|
|
2
|
+
import type { Ranks } from "../rank";
|
|
3
|
+
import type { Team } from "../team";
|
|
4
|
+
|
|
5
|
+
import { RatingCalculator } from "./rating-calculator";
|
|
6
|
+
import { RatingHistory } from "./rating-history";
|
|
7
|
+
import type { RatingUserMap, RatingUsers } from "./rating-user";
|
|
8
|
+
import { RatingUser } from "./rating-user";
|
|
9
|
+
|
|
10
|
+
export class Rating {
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
baseRating: number;
|
|
14
|
+
|
|
15
|
+
contestIDs: string[];
|
|
16
|
+
users: RatingUsers;
|
|
17
|
+
|
|
18
|
+
ranks: Ranks;
|
|
19
|
+
userMap: RatingUserMap;
|
|
20
|
+
|
|
21
|
+
constructor() {
|
|
22
|
+
this.id = "";
|
|
23
|
+
this.name = "";
|
|
24
|
+
this.baseRating = 1500;
|
|
25
|
+
|
|
26
|
+
this.contestIDs = [];
|
|
27
|
+
this.users = [];
|
|
28
|
+
|
|
29
|
+
this.ranks = [];
|
|
30
|
+
this.userMap = new Map<string, RatingUser>();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
buildRating() {
|
|
34
|
+
for (const rank of this.ranks) {
|
|
35
|
+
rank.buildRank();
|
|
36
|
+
|
|
37
|
+
for (const u of this.users) {
|
|
38
|
+
u.oldRating = u.rating;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
for (const t of rank.teams) {
|
|
42
|
+
const id = this.generateTeamId(t);
|
|
43
|
+
|
|
44
|
+
if (!this.userMap.has(id)) {
|
|
45
|
+
const u = new RatingUser();
|
|
46
|
+
u.id = id;
|
|
47
|
+
u.name = t.name;
|
|
48
|
+
u.rank = t.rank;
|
|
49
|
+
u.oldRating = this.baseRating;
|
|
50
|
+
u.UpdateRating(this.baseRating);
|
|
51
|
+
|
|
52
|
+
this.userMap.set(id, u);
|
|
53
|
+
this.users.push(u);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const ratingCalculator = new RatingCalculator();
|
|
58
|
+
ratingCalculator.users = this.users;
|
|
59
|
+
ratingCalculator.calculate();
|
|
60
|
+
|
|
61
|
+
for (const u of this.users) {
|
|
62
|
+
const h = new RatingHistory();
|
|
63
|
+
h.rank = u.rank;
|
|
64
|
+
h.rating = u.rating;
|
|
65
|
+
|
|
66
|
+
h.contestID = rank.contest.id;
|
|
67
|
+
h.contestLink = h.contestID;
|
|
68
|
+
h.contestName = rank.contest.name;
|
|
69
|
+
h.contestTime = rank.contest.startTime;
|
|
70
|
+
|
|
71
|
+
u.ratingHistories.push(h);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
generateTeamId(t: Team) {
|
|
77
|
+
if (Array.isArray(t.members) && t.members.length > 0) {
|
|
78
|
+
return t.members.join("|");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return `${t.organization}-${t.name}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
toJSON(): IRating {
|
|
85
|
+
return {
|
|
86
|
+
id: this.id,
|
|
87
|
+
name: this.name,
|
|
88
|
+
baseRating: this.baseRating,
|
|
89
|
+
|
|
90
|
+
contestIDs: this.contestIDs,
|
|
91
|
+
users: this.users.map(ratingUser => ratingUser.toJSON()),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
static fromJSON(iRating: IRating | string): Rating {
|
|
96
|
+
if (typeof iRating === "string") {
|
|
97
|
+
iRating = JSON.parse(iRating) as IRating;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const rating = new Rating();
|
|
101
|
+
|
|
102
|
+
rating.id = iRating.id;
|
|
103
|
+
rating.name = iRating.name;
|
|
104
|
+
rating.baseRating = iRating.baseRating;
|
|
105
|
+
|
|
106
|
+
rating.contestIDs = iRating.contestIDs;
|
|
107
|
+
|
|
108
|
+
for (const iUser of iRating.users) {
|
|
109
|
+
rating.users.push(RatingUser.fromJSON(iUser));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return rating;
|
|
113
|
+
}
|
|
114
|
+
}
|