@algoux/standard-ranklist-utils 0.1.1 → 0.2.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/README.md +8 -0
- package/dist/index.cjs +689 -0
- package/dist/index.d.cts +100 -0
- package/dist/index.d.mts +100 -0
- package/dist/index.mjs +669 -0
- package/package.json +26 -10
- package/dist/constants.d.ts +0 -1
- package/dist/constants.js +0 -1
- package/dist/enums.d.ts +0 -4
- package/dist/enums.js +0 -5
- package/dist/formatters.d.ts +0 -35
- package/dist/formatters.js +0 -124
- package/dist/index.d.ts +0 -6
- package/dist/index.js +0 -6
- package/dist/ranklist.d.ts +0 -9
- package/dist/ranklist.js +0 -492
- package/dist/resolvers.d.ts +0 -25
- package/dist/resolvers.js +0 -98
- package/dist/types.d.ts +0 -26
- package/dist/types.js +0 -1
package/dist/ranklist.js
DELETED
|
@@ -1,492 +0,0 @@
|
|
|
1
|
-
import semver from 'semver';
|
|
2
|
-
import BigNumber from 'bignumber.js';
|
|
3
|
-
import { MIN_REGEN_SUPPORTED_VERSION } from './constants';
|
|
4
|
-
import { formatTimeDuration } from './formatters';
|
|
5
|
-
export function canRegenerateRanklist(ranklist) {
|
|
6
|
-
try {
|
|
7
|
-
if (!semver.gte(ranklist.version, MIN_REGEN_SUPPORTED_VERSION)) {
|
|
8
|
-
return false;
|
|
9
|
-
}
|
|
10
|
-
if (ranklist.sorter?.algorithm !== 'ICPC') {
|
|
11
|
-
return false;
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
catch (e) {
|
|
15
|
-
return false;
|
|
16
|
-
}
|
|
17
|
-
return true;
|
|
18
|
-
}
|
|
19
|
-
export function getSortedCalculatedRawSolutions(rows) {
|
|
20
|
-
const solutions = [];
|
|
21
|
-
for (const row of rows) {
|
|
22
|
-
const { user, statuses } = row;
|
|
23
|
-
const userId = (user.id && `${user.id}`) || `${typeof user.name === 'string' ? user.name : JSON.stringify(user.name)}`;
|
|
24
|
-
statuses.forEach((status, index) => {
|
|
25
|
-
if (Array.isArray(status.solutions) && status.solutions.length) {
|
|
26
|
-
solutions.push(...status.solutions.map((solution) => [userId, index, solution.result, solution.time]));
|
|
27
|
-
}
|
|
28
|
-
else if (status.result && status.time?.[0]) {
|
|
29
|
-
// use status.result as partial solutions
|
|
30
|
-
if (status.result === 'AC' || status.result === 'FB') {
|
|
31
|
-
// push a series of mocked rejected solutions based on tries
|
|
32
|
-
for (let i = 1; i < (status.tries || 0); i++) {
|
|
33
|
-
solutions.push([userId, index, 'RJ', status.time]);
|
|
34
|
-
}
|
|
35
|
-
solutions.push([userId, index, status.result, status.time]);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
return solutions.sort((a, b) => {
|
|
41
|
-
const ta = a[3];
|
|
42
|
-
const tb = b[3];
|
|
43
|
-
// if time duration unit is same, directly compare their value; else convert to minimum unit to compare
|
|
44
|
-
const timeComp = ta[1] === tb[1] ? ta[0] - tb[0] : formatTimeDuration(ta) - formatTimeDuration(tb);
|
|
45
|
-
if (timeComp !== 0) {
|
|
46
|
-
return timeComp;
|
|
47
|
-
}
|
|
48
|
-
const resultValue = {
|
|
49
|
-
FB: 998,
|
|
50
|
-
AC: 999,
|
|
51
|
-
'?': 1000,
|
|
52
|
-
};
|
|
53
|
-
const resultA = resultValue[a[2]] || 0;
|
|
54
|
-
const resultB = resultValue[b[2]] || 0;
|
|
55
|
-
return resultA - resultB;
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
export function filterSolutionsUntil(solutions, time) {
|
|
59
|
-
const timeValue = formatTimeDuration(time);
|
|
60
|
-
const check = (tetrad) => formatTimeDuration(tetrad[3]) <= timeValue;
|
|
61
|
-
let lastIndex = -1;
|
|
62
|
-
let low = 0;
|
|
63
|
-
let high = solutions.length - 1;
|
|
64
|
-
while (low <= high) {
|
|
65
|
-
const mid = (low + high) >> 1;
|
|
66
|
-
if (check(solutions[mid])) {
|
|
67
|
-
lastIndex = mid;
|
|
68
|
-
low = mid + 1;
|
|
69
|
-
}
|
|
70
|
-
else {
|
|
71
|
-
high = mid - 1;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
return solutions.slice(0, lastIndex + 1);
|
|
75
|
-
}
|
|
76
|
-
export function sortRows(rows) {
|
|
77
|
-
rows.sort((a, b) => {
|
|
78
|
-
if (a.score.value !== b.score.value) {
|
|
79
|
-
return b.score.value - a.score.value;
|
|
80
|
-
}
|
|
81
|
-
return formatTimeDuration(a.score.time) - formatTimeDuration(b.score.time);
|
|
82
|
-
});
|
|
83
|
-
return rows;
|
|
84
|
-
}
|
|
85
|
-
export function regenerateRanklistBySolutions(originalRanklist, solutions) {
|
|
86
|
-
if (!canRegenerateRanklist(originalRanklist)) {
|
|
87
|
-
throw new Error('The ranklist is not supported to regenerate');
|
|
88
|
-
}
|
|
89
|
-
const sorterConfig = {
|
|
90
|
-
penalty: [20, 'min'],
|
|
91
|
-
noPenaltyResults: ['FB', 'AC', '?', 'CE', 'UKE', null],
|
|
92
|
-
timeRounding: 'floor',
|
|
93
|
-
...JSON.parse(JSON.stringify(originalRanklist.sorter?.config || {})),
|
|
94
|
-
};
|
|
95
|
-
const ranklist = {
|
|
96
|
-
...originalRanklist,
|
|
97
|
-
rows: [],
|
|
98
|
-
};
|
|
99
|
-
const rows = [];
|
|
100
|
-
const userRowMap = new Map();
|
|
101
|
-
const problemCount = originalRanklist.problems.length;
|
|
102
|
-
originalRanklist.rows.forEach((row) => {
|
|
103
|
-
const userId = (row.user.id && `${row.user.id}`) ||
|
|
104
|
-
`${typeof row.user.name === 'string' ? row.user.name : JSON.stringify(row.user.name)}`;
|
|
105
|
-
userRowMap.set(userId, {
|
|
106
|
-
user: row.user,
|
|
107
|
-
score: {
|
|
108
|
-
value: 0,
|
|
109
|
-
},
|
|
110
|
-
statuses: new Array(problemCount).fill(null).map(() => ({ result: null, solutions: [] })),
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
for (const tetrad of solutions) {
|
|
114
|
-
const [userId, problemIndex, result, time] = tetrad;
|
|
115
|
-
let row = userRowMap.get(userId);
|
|
116
|
-
if (!row) {
|
|
117
|
-
console.error(`Invalid user id ${userId} found when regenerating ranklist`);
|
|
118
|
-
break;
|
|
119
|
-
}
|
|
120
|
-
const status = row.statuses[problemIndex];
|
|
121
|
-
status.solutions.push({ result, time });
|
|
122
|
-
}
|
|
123
|
-
const problemAcceptedCount = new Array(problemCount).fill(0);
|
|
124
|
-
const problemSubmittedCount = new Array(problemCount).fill(0);
|
|
125
|
-
for (const row of userRowMap.values()) {
|
|
126
|
-
const { statuses } = row;
|
|
127
|
-
let scoreValue = 0;
|
|
128
|
-
let totalTimeMs = 0;
|
|
129
|
-
for (let i = 0; i < statuses.length; ++i) {
|
|
130
|
-
const status = statuses[i];
|
|
131
|
-
// calculate for each problem
|
|
132
|
-
const solutions = status.solutions;
|
|
133
|
-
for (const solution of solutions) {
|
|
134
|
-
if (!solution.result) {
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
|
-
if (solution.result === '?') {
|
|
138
|
-
status.result = solution.result;
|
|
139
|
-
status.tries = (status.tries || 0) + 1;
|
|
140
|
-
problemSubmittedCount[i] += 1;
|
|
141
|
-
continue;
|
|
142
|
-
}
|
|
143
|
-
if (solution.result === 'AC' || solution.result === 'FB') {
|
|
144
|
-
status.result = solution.result;
|
|
145
|
-
status.time = solution.time;
|
|
146
|
-
status.tries = (status.tries || 0) + 1;
|
|
147
|
-
problemAcceptedCount[i] += 1;
|
|
148
|
-
problemSubmittedCount[i] += 1;
|
|
149
|
-
break;
|
|
150
|
-
}
|
|
151
|
-
// @ts-ignore
|
|
152
|
-
if ((sorterConfig.noPenaltyResults || []).includes(solution.result)) {
|
|
153
|
-
continue;
|
|
154
|
-
}
|
|
155
|
-
status.result = 'RJ';
|
|
156
|
-
status.tries = (status.tries || 0) + 1;
|
|
157
|
-
problemSubmittedCount[i] += 1;
|
|
158
|
-
}
|
|
159
|
-
if (status.result === 'AC' || status.result === 'FB') {
|
|
160
|
-
const targetTime = [
|
|
161
|
-
formatTimeDuration(status.time, sorterConfig.timePrecision || 'ms', sorterConfig.timeRounding === 'ceil'
|
|
162
|
-
? Math.ceil
|
|
163
|
-
: sorterConfig.timeRounding === 'round'
|
|
164
|
-
? Math.round
|
|
165
|
-
: Math.floor),
|
|
166
|
-
sorterConfig.timePrecision || 'ms',
|
|
167
|
-
];
|
|
168
|
-
scoreValue += 1;
|
|
169
|
-
totalTimeMs +=
|
|
170
|
-
formatTimeDuration(targetTime, 'ms') + (status.tries - 1) * formatTimeDuration(sorterConfig.penalty, 'ms');
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
row.score = {
|
|
174
|
-
value: scoreValue,
|
|
175
|
-
time: [totalTimeMs, 'ms'],
|
|
176
|
-
};
|
|
177
|
-
rows.push(row);
|
|
178
|
-
}
|
|
179
|
-
ranklist.rows = sortRows(rows);
|
|
180
|
-
ranklist.problems.forEach((problem, index) => {
|
|
181
|
-
if (!problem.statistics) {
|
|
182
|
-
problem.statistics = {
|
|
183
|
-
accepted: 0,
|
|
184
|
-
submitted: 0,
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
problem.statistics.accepted = problemAcceptedCount[index];
|
|
188
|
-
problem.statistics.submitted = problemSubmittedCount[index];
|
|
189
|
-
});
|
|
190
|
-
return ranklist;
|
|
191
|
-
}
|
|
192
|
-
export function regenerateRowsByIncrementalSolutions(originalRanklist, solutions) {
|
|
193
|
-
if (!canRegenerateRanklist(originalRanklist)) {
|
|
194
|
-
throw new Error('The ranklist is not supported to regenerate');
|
|
195
|
-
}
|
|
196
|
-
const sorterConfig = {
|
|
197
|
-
penalty: [20, 'min'],
|
|
198
|
-
noPenaltyResults: ['FB', 'AC', '?', 'CE', 'UKE', null],
|
|
199
|
-
timeRounding: 'floor',
|
|
200
|
-
...JSON.parse(JSON.stringify(originalRanklist.sorter?.config || {})),
|
|
201
|
-
};
|
|
202
|
-
const userRowIndexMap = new Map();
|
|
203
|
-
const rows = [...originalRanklist.rows];
|
|
204
|
-
rows.forEach((row, index) => {
|
|
205
|
-
const userId = (row.user.id && `${row.user.id}`) ||
|
|
206
|
-
`${typeof row.user.name === 'string' ? row.user.name : JSON.stringify(row.user.name)}`;
|
|
207
|
-
userRowIndexMap.set(userId, index);
|
|
208
|
-
});
|
|
209
|
-
const clonedRowMap = new Set();
|
|
210
|
-
const clonedRowStatusMap = new Set();
|
|
211
|
-
for (const tetrad of solutions) {
|
|
212
|
-
const [userId, problemIndex, result, time] = tetrad;
|
|
213
|
-
let rowIndex = userRowIndexMap.get(userId);
|
|
214
|
-
if (rowIndex === undefined) {
|
|
215
|
-
console.error(`Invalid user id ${userId} found when regenerating ranklist`);
|
|
216
|
-
break;
|
|
217
|
-
}
|
|
218
|
-
let row = rows[rowIndex];
|
|
219
|
-
if (!clonedRowMap.has(userId)) {
|
|
220
|
-
row = { ...row };
|
|
221
|
-
row.score = { ...row.score };
|
|
222
|
-
row.statuses = [...row.statuses];
|
|
223
|
-
rows[rowIndex] = row;
|
|
224
|
-
clonedRowMap.add(userId);
|
|
225
|
-
}
|
|
226
|
-
if (!clonedRowStatusMap.has(`${userId}_${problemIndex}`)) {
|
|
227
|
-
row.statuses[problemIndex] = { ...row.statuses[problemIndex] };
|
|
228
|
-
row.statuses[problemIndex].solutions = [...row.statuses[problemIndex].solutions];
|
|
229
|
-
clonedRowStatusMap.add(`${userId}_${problemIndex}`);
|
|
230
|
-
}
|
|
231
|
-
const status = row.statuses[problemIndex];
|
|
232
|
-
status.solutions.push({ result, time });
|
|
233
|
-
if (status.result === 'AC' || status.result === 'FB') {
|
|
234
|
-
continue;
|
|
235
|
-
}
|
|
236
|
-
if (result === '?') {
|
|
237
|
-
status.result = result;
|
|
238
|
-
status.tries = (status.tries || 0) + 1;
|
|
239
|
-
continue;
|
|
240
|
-
}
|
|
241
|
-
if (result === 'AC' || result === 'FB') {
|
|
242
|
-
status.result = result;
|
|
243
|
-
status.time = time;
|
|
244
|
-
status.tries = (status.tries || 0) + 1;
|
|
245
|
-
row.score.value += 1;
|
|
246
|
-
const targetTime = [
|
|
247
|
-
formatTimeDuration(status.time, sorterConfig.timePrecision || 'ms', sorterConfig.timeRounding === 'ceil'
|
|
248
|
-
? Math.ceil
|
|
249
|
-
: sorterConfig.timeRounding === 'round'
|
|
250
|
-
? Math.round
|
|
251
|
-
: Math.floor),
|
|
252
|
-
sorterConfig.timePrecision || 'ms',
|
|
253
|
-
];
|
|
254
|
-
const totalTime = formatTimeDuration(row.score.time, 'ms') || 0;
|
|
255
|
-
row.score.time = [
|
|
256
|
-
totalTime +
|
|
257
|
-
formatTimeDuration(targetTime, 'ms') +
|
|
258
|
-
(status.tries - 1) * formatTimeDuration(sorterConfig.penalty, 'ms'),
|
|
259
|
-
'ms',
|
|
260
|
-
];
|
|
261
|
-
continue;
|
|
262
|
-
}
|
|
263
|
-
// @ts-ignore
|
|
264
|
-
if ((sorterConfig.noPenaltyResults || []).includes(result)) {
|
|
265
|
-
continue;
|
|
266
|
-
}
|
|
267
|
-
status.result = 'RJ';
|
|
268
|
-
status.tries = (status.tries || 0) + 1;
|
|
269
|
-
}
|
|
270
|
-
return sortRows(rows);
|
|
271
|
-
}
|
|
272
|
-
function genSeriesCalcFns(series, rows, ranks, officialRanks) {
|
|
273
|
-
const fallbackSeriesCalcFn = () => ({
|
|
274
|
-
rank: null,
|
|
275
|
-
segmentIndex: null,
|
|
276
|
-
});
|
|
277
|
-
const fns = series.map((seriesConfig) => {
|
|
278
|
-
const { rule } = seriesConfig;
|
|
279
|
-
if (!rule) {
|
|
280
|
-
return fallbackSeriesCalcFn;
|
|
281
|
-
}
|
|
282
|
-
const { preset } = rule;
|
|
283
|
-
switch (preset) {
|
|
284
|
-
case 'Normal': {
|
|
285
|
-
const options = rule.options;
|
|
286
|
-
return (row, index) => {
|
|
287
|
-
if (options?.includeOfficialOnly && row.user.official === false) {
|
|
288
|
-
return {
|
|
289
|
-
rank: null,
|
|
290
|
-
segmentIndex: null,
|
|
291
|
-
};
|
|
292
|
-
}
|
|
293
|
-
return {
|
|
294
|
-
rank: options?.includeOfficialOnly ? officialRanks[index] : ranks[index],
|
|
295
|
-
segmentIndex: null,
|
|
296
|
-
};
|
|
297
|
-
};
|
|
298
|
-
}
|
|
299
|
-
case 'UniqByUserField': {
|
|
300
|
-
const options = rule.options;
|
|
301
|
-
const field = options?.field;
|
|
302
|
-
const assignedRanksMap = new Map();
|
|
303
|
-
const valueSet = new Set();
|
|
304
|
-
const stringify = (v) => (typeof v === 'object' ? JSON.stringify(v) : `${v}`);
|
|
305
|
-
let lastOuterRank = 0;
|
|
306
|
-
let lastRank = 0;
|
|
307
|
-
rows.forEach((row, index) => {
|
|
308
|
-
if (options.includeOfficialOnly && row.user.official === false) {
|
|
309
|
-
return;
|
|
310
|
-
}
|
|
311
|
-
const value = stringify(row.user[field]);
|
|
312
|
-
if (value && !valueSet.has(value)) {
|
|
313
|
-
const outerRank = options.includeOfficialOnly ? officialRanks[index] : ranks[index];
|
|
314
|
-
valueSet.add(value);
|
|
315
|
-
if (outerRank !== lastOuterRank) {
|
|
316
|
-
lastOuterRank = outerRank;
|
|
317
|
-
lastRank = assignedRanksMap.size + 1;
|
|
318
|
-
assignedRanksMap.set(index, lastRank);
|
|
319
|
-
}
|
|
320
|
-
assignedRanksMap.set(index, lastRank);
|
|
321
|
-
}
|
|
322
|
-
});
|
|
323
|
-
return (row, index) => {
|
|
324
|
-
return {
|
|
325
|
-
rank: assignedRanksMap.get(index) ?? null,
|
|
326
|
-
segmentIndex: null,
|
|
327
|
-
};
|
|
328
|
-
};
|
|
329
|
-
}
|
|
330
|
-
case 'ICPC': {
|
|
331
|
-
const options = rule.options;
|
|
332
|
-
let filteredRows = rows.filter((row) => row.user.official === undefined || row.user.official === true);
|
|
333
|
-
let filteredOfficialRanks = [...officialRanks];
|
|
334
|
-
if (options.filter) {
|
|
335
|
-
if (Array.isArray(options.filter.byUserFields) && options.filter.byUserFields.length) {
|
|
336
|
-
const currentFilteredRows = [];
|
|
337
|
-
filteredOfficialRanks = filteredOfficialRanks.map(() => null);
|
|
338
|
-
let currentOfficialRank = 0;
|
|
339
|
-
let currentOfficialRankOld = 0;
|
|
340
|
-
rows.forEach((row, index) => {
|
|
341
|
-
const shouldInclude = options.filter.byUserFields.every((filter) => {
|
|
342
|
-
const { field, rule } = filter;
|
|
343
|
-
const value = row.user[field];
|
|
344
|
-
if (value === undefined) {
|
|
345
|
-
return false;
|
|
346
|
-
}
|
|
347
|
-
return new RegExp(rule).test(`${value}`);
|
|
348
|
-
});
|
|
349
|
-
if (shouldInclude) {
|
|
350
|
-
currentFilteredRows.push(row);
|
|
351
|
-
const oldRank = officialRanks[index];
|
|
352
|
-
if (oldRank !== null) {
|
|
353
|
-
if (currentOfficialRankOld !== oldRank) {
|
|
354
|
-
currentOfficialRank++;
|
|
355
|
-
currentOfficialRankOld = oldRank;
|
|
356
|
-
}
|
|
357
|
-
filteredOfficialRanks[index] = currentOfficialRank;
|
|
358
|
-
}
|
|
359
|
-
else {
|
|
360
|
-
filteredOfficialRanks[index] = null;
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
});
|
|
364
|
-
filteredRows = currentFilteredRows;
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
const usingEndpointRules = [];
|
|
368
|
-
let noTied = false;
|
|
369
|
-
if (options.ratio) {
|
|
370
|
-
const { value, rounding = 'ceil', denominator = 'all' } = options.ratio;
|
|
371
|
-
const officialRows = filteredRows;
|
|
372
|
-
let total = denominator === 'submitted'
|
|
373
|
-
? officialRows.filter((row) => !row.statuses.every((s) => s.result === null)).length
|
|
374
|
-
: officialRows.length;
|
|
375
|
-
const accValues = [];
|
|
376
|
-
for (let i = 0; i < value.length; i++) {
|
|
377
|
-
if (i === 0) {
|
|
378
|
-
accValues[i] = new BigNumber(value[i]);
|
|
379
|
-
}
|
|
380
|
-
else {
|
|
381
|
-
accValues[i] = accValues[i - 1].plus(new BigNumber(value[i]));
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
const segmentRawEndpoints = accValues.map((v) => v.times(total).toNumber());
|
|
385
|
-
usingEndpointRules.push(segmentRawEndpoints.map((v) => {
|
|
386
|
-
return rounding === 'floor' ? Math.floor(v) : rounding === 'round' ? Math.round(v) : Math.ceil(v);
|
|
387
|
-
}));
|
|
388
|
-
if (options.ratio.noTied) {
|
|
389
|
-
noTied = true;
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
if (options.count) {
|
|
393
|
-
const { value } = options.count;
|
|
394
|
-
const accValues = [];
|
|
395
|
-
for (let i = 0; i < value.length; i++) {
|
|
396
|
-
accValues[i] = (i > 0 ? accValues[i - 1] : 0) + value[i];
|
|
397
|
-
}
|
|
398
|
-
usingEndpointRules.push(accValues);
|
|
399
|
-
if (options.count.noTied) {
|
|
400
|
-
noTied = true;
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
const officialRanksNoTied = [];
|
|
404
|
-
let currentOfficialRank = 0;
|
|
405
|
-
for (let i = 0; i < filteredOfficialRanks.length; i++) {
|
|
406
|
-
officialRanksNoTied.push(filteredOfficialRanks[i] === null ? null : ++currentOfficialRank);
|
|
407
|
-
}
|
|
408
|
-
return (row, index) => {
|
|
409
|
-
if (row.user.official === false || !filteredRows.find((r) => r.user.id === row.user.id)) {
|
|
410
|
-
return {
|
|
411
|
-
rank: null,
|
|
412
|
-
segmentIndex: null,
|
|
413
|
-
};
|
|
414
|
-
}
|
|
415
|
-
const usingSegmentIndex = (seriesConfig.segments || []).findIndex((_, segIndex) => {
|
|
416
|
-
return usingEndpointRules
|
|
417
|
-
.map((e) => e[segIndex])
|
|
418
|
-
.every((endpoints) => (noTied ? officialRanksNoTied : filteredOfficialRanks)[index] <= endpoints);
|
|
419
|
-
});
|
|
420
|
-
return {
|
|
421
|
-
rank: filteredOfficialRanks[index],
|
|
422
|
-
segmentIndex: usingSegmentIndex > -1 ? usingSegmentIndex : null,
|
|
423
|
-
};
|
|
424
|
-
};
|
|
425
|
-
}
|
|
426
|
-
default:
|
|
427
|
-
console.warn('Unknown series rule preset:', preset);
|
|
428
|
-
return fallbackSeriesCalcFn;
|
|
429
|
-
}
|
|
430
|
-
});
|
|
431
|
-
return fns;
|
|
432
|
-
}
|
|
433
|
-
function genRowRanks(rows) {
|
|
434
|
-
const compareScoreEqual = (a, b) => {
|
|
435
|
-
if (a.value !== b.value) {
|
|
436
|
-
return false;
|
|
437
|
-
}
|
|
438
|
-
const da = a.time ? formatTimeDuration(a.time) : 0;
|
|
439
|
-
const db = b.time ? formatTimeDuration(b.time) : 0;
|
|
440
|
-
return da === db;
|
|
441
|
-
};
|
|
442
|
-
const genRanks = (rows) => {
|
|
443
|
-
let ranks = new Array(rows.length).fill(null);
|
|
444
|
-
for (let i = 0; i < rows.length; ++i) {
|
|
445
|
-
if (i === 0) {
|
|
446
|
-
ranks[i] = 1;
|
|
447
|
-
continue;
|
|
448
|
-
}
|
|
449
|
-
if (compareScoreEqual(rows[i].score, rows[i - 1].score)) {
|
|
450
|
-
ranks[i] = ranks[i - 1];
|
|
451
|
-
}
|
|
452
|
-
else {
|
|
453
|
-
ranks[i] = i + 1;
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
return ranks;
|
|
457
|
-
};
|
|
458
|
-
const ranks = genRanks(rows);
|
|
459
|
-
const officialPartialRows = [];
|
|
460
|
-
const officialIndexBackMap = new Map();
|
|
461
|
-
rows.forEach((row, index) => {
|
|
462
|
-
if (row.user.official !== false) {
|
|
463
|
-
officialIndexBackMap.set(index, officialPartialRows.length);
|
|
464
|
-
officialPartialRows.push(row);
|
|
465
|
-
}
|
|
466
|
-
});
|
|
467
|
-
const officialPartialRanks = genRanks(officialPartialRows);
|
|
468
|
-
const officialRanks = new Array(rows.length)
|
|
469
|
-
.fill(null)
|
|
470
|
-
.map((_, index) => officialIndexBackMap.get(index) === undefined ? null : officialPartialRanks[officialIndexBackMap.get(index)]);
|
|
471
|
-
return {
|
|
472
|
-
ranks,
|
|
473
|
-
officialRanks,
|
|
474
|
-
};
|
|
475
|
-
}
|
|
476
|
-
export function convertToStaticRanklist(ranklist) {
|
|
477
|
-
if (!ranklist) {
|
|
478
|
-
return ranklist;
|
|
479
|
-
}
|
|
480
|
-
const { series, rows } = ranklist;
|
|
481
|
-
const rowRanks = genRowRanks(rows);
|
|
482
|
-
const seriesCalcFns = genSeriesCalcFns(series, rows, rowRanks.ranks, rowRanks.officialRanks);
|
|
483
|
-
return {
|
|
484
|
-
...ranklist,
|
|
485
|
-
rows: rows.map((row, index) => {
|
|
486
|
-
return {
|
|
487
|
-
...row,
|
|
488
|
-
rankValues: seriesCalcFns.map((fn) => fn(row, index)),
|
|
489
|
-
};
|
|
490
|
-
}),
|
|
491
|
-
};
|
|
492
|
-
}
|
package/dist/resolvers.d.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import type * as srk from '@algoux/standard-ranklist';
|
|
2
|
-
import { ThemeColor } from './types';
|
|
3
|
-
export declare function resolveText(text: srk.Text | undefined): string;
|
|
4
|
-
/**
|
|
5
|
-
* Parse contributor string to an object which contains name, email (optional) and url (optional).
|
|
6
|
-
* @param contributor
|
|
7
|
-
* @returns parsed contributor object
|
|
8
|
-
* @example
|
|
9
|
-
* 'name <mail@example.com> (http://example.com)' -> { name: 'name', email: 'mail@example.com', url: 'http://example.com' }
|
|
10
|
-
* 'name' -> { name: 'name' }
|
|
11
|
-
* 'name <mail@example.com>' -> { name: 'name', email: 'mail@example.com' }
|
|
12
|
-
* 'name (http://example.com)' -> { name: 'name', url: 'http://example.com' }
|
|
13
|
-
* 'John Smith (http://example.com)' -> { name: 'John Smith', url: 'http://example.com' }
|
|
14
|
-
*/
|
|
15
|
-
export declare function resolveContributor(contributor: srk.Contributor | undefined): {
|
|
16
|
-
name: string;
|
|
17
|
-
email?: string;
|
|
18
|
-
url?: string;
|
|
19
|
-
} | null;
|
|
20
|
-
export declare function resolveColor(color: srk.Color): string | undefined;
|
|
21
|
-
export declare function resolveThemeColor(themeColor: srk.ThemeColor): ThemeColor;
|
|
22
|
-
export declare function resolveStyle(style: srk.Style): {
|
|
23
|
-
textColor: ThemeColor;
|
|
24
|
-
backgroundColor: ThemeColor;
|
|
25
|
-
};
|
package/dist/resolvers.js
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
// @ts-ignore
|
|
2
|
-
import TEXTColor from 'textcolor';
|
|
3
|
-
import { lookup as langLookup } from 'bcp-47-match';
|
|
4
|
-
import { EnumTheme } from './enums';
|
|
5
|
-
export function resolveText(text) {
|
|
6
|
-
if (text === undefined) {
|
|
7
|
-
return '';
|
|
8
|
-
}
|
|
9
|
-
if (typeof text === 'string') {
|
|
10
|
-
return text;
|
|
11
|
-
}
|
|
12
|
-
else {
|
|
13
|
-
const langs = Object.keys(text)
|
|
14
|
-
.filter((k) => k && k !== 'fallback')
|
|
15
|
-
.sort()
|
|
16
|
-
.reverse();
|
|
17
|
-
const userLangs = (typeof navigator !== 'undefined' && [...navigator.languages]) || [];
|
|
18
|
-
const usingLang = langLookup(userLangs, langs) || 'fallback';
|
|
19
|
-
return text[usingLang] ?? '';
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Parse contributor string to an object which contains name, email (optional) and url (optional).
|
|
24
|
-
* @param contributor
|
|
25
|
-
* @returns parsed contributor object
|
|
26
|
-
* @example
|
|
27
|
-
* 'name <mail@example.com> (http://example.com)' -> { name: 'name', email: 'mail@example.com', url: 'http://example.com' }
|
|
28
|
-
* 'name' -> { name: 'name' }
|
|
29
|
-
* 'name <mail@example.com>' -> { name: 'name', email: 'mail@example.com' }
|
|
30
|
-
* 'name (http://example.com)' -> { name: 'name', url: 'http://example.com' }
|
|
31
|
-
* 'John Smith (http://example.com)' -> { name: 'John Smith', url: 'http://example.com' }
|
|
32
|
-
*/
|
|
33
|
-
export function resolveContributor(contributor) {
|
|
34
|
-
if (!contributor) {
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
let name = '';
|
|
38
|
-
let email;
|
|
39
|
-
let url;
|
|
40
|
-
const words = contributor.split(' ').map((s) => s.trim());
|
|
41
|
-
let index = words.length - 1;
|
|
42
|
-
while (index > 0) {
|
|
43
|
-
const word = words[index];
|
|
44
|
-
if (word.startsWith('<') && word.endsWith('>')) {
|
|
45
|
-
email = word.slice(1, -1);
|
|
46
|
-
index--;
|
|
47
|
-
continue;
|
|
48
|
-
}
|
|
49
|
-
if (word.startsWith('(') && word.endsWith(')')) {
|
|
50
|
-
url = word.slice(1, -1);
|
|
51
|
-
index--;
|
|
52
|
-
continue;
|
|
53
|
-
}
|
|
54
|
-
break;
|
|
55
|
-
}
|
|
56
|
-
name = words.slice(0, index + 1).join(' ');
|
|
57
|
-
return { name, email, url };
|
|
58
|
-
}
|
|
59
|
-
export function resolveColor(color) {
|
|
60
|
-
if (Array.isArray(color)) {
|
|
61
|
-
return `rgba(${color[0]},${color[1]},${color[2]},${color[3]})`;
|
|
62
|
-
}
|
|
63
|
-
else if (color) {
|
|
64
|
-
return color;
|
|
65
|
-
}
|
|
66
|
-
return undefined;
|
|
67
|
-
}
|
|
68
|
-
export function resolveThemeColor(themeColor) {
|
|
69
|
-
let light = resolveColor(typeof themeColor === 'string' ? themeColor : themeColor.light);
|
|
70
|
-
let dark = resolveColor(typeof themeColor === 'string' ? themeColor : themeColor.dark);
|
|
71
|
-
return {
|
|
72
|
-
[EnumTheme.light]: light,
|
|
73
|
-
[EnumTheme.dark]: dark,
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
export function resolveStyle(style) {
|
|
77
|
-
const { textColor, backgroundColor } = style;
|
|
78
|
-
let usingTextColor = textColor;
|
|
79
|
-
// 未指定前景色时,尝试自动适配
|
|
80
|
-
if (backgroundColor && !textColor) {
|
|
81
|
-
if (typeof backgroundColor === 'string') {
|
|
82
|
-
usingTextColor = TEXTColor.findTextColor(backgroundColor);
|
|
83
|
-
}
|
|
84
|
-
else {
|
|
85
|
-
const { light, dark } = backgroundColor;
|
|
86
|
-
usingTextColor = {
|
|
87
|
-
light: light && TEXTColor.findTextColor(light),
|
|
88
|
-
dark: dark && TEXTColor.findTextColor(dark),
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
const textThemeColor = resolveThemeColor(usingTextColor || '');
|
|
93
|
-
const backgroundThemeColor = resolveThemeColor(backgroundColor || '');
|
|
94
|
-
return {
|
|
95
|
-
textColor: textThemeColor,
|
|
96
|
-
backgroundColor: backgroundThemeColor,
|
|
97
|
-
};
|
|
98
|
-
}
|
package/dist/types.d.ts
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import type * as srk from '@algoux/standard-ranklist';
|
|
2
|
-
import { EnumTheme } from './enums';
|
|
3
|
-
export interface RankValue {
|
|
4
|
-
/** Rank value initially. If the user is unofficial and rank value equals null, it will be rendered as unofficial mark such as '*'. */
|
|
5
|
-
rank: number | null;
|
|
6
|
-
/**
|
|
7
|
-
* Series segment index which this rank belongs to initially. `null` means this rank does not belong to any segment. `undefined` means it will be calculated automatically (only if the segment's count property exists).
|
|
8
|
-
* @defaultValue null
|
|
9
|
-
*/
|
|
10
|
-
segmentIndex?: number | null;
|
|
11
|
-
}
|
|
12
|
-
export type StaticRanklist = Omit<srk.Ranklist, 'rows'> & {
|
|
13
|
-
rows: Array<srk.RanklistRow & {
|
|
14
|
-
rankValues: RankValue[];
|
|
15
|
-
}>;
|
|
16
|
-
};
|
|
17
|
-
export type CalculatedSolutionTetrad = [
|
|
18
|
-
/** user id */ string,
|
|
19
|
-
/** problem index */ number,
|
|
20
|
-
/** result */ Exclude<srk.SolutionResultFull, null> | srk.SolutionResultCustom,
|
|
21
|
-
/** solution submitted time */ srk.TimeDuration
|
|
22
|
-
];
|
|
23
|
-
export interface ThemeColor {
|
|
24
|
-
[EnumTheme.light]: string | undefined;
|
|
25
|
-
[EnumTheme.dark]: string | undefined;
|
|
26
|
-
}
|
package/dist/types.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import { EnumTheme } from './enums';
|