@rasifix/orienteering-utils 2.0.0 → 2.0.2
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/lib/format.d.ts +6 -0
- package/lib/format.js +2 -0
- package/{src/formats/index.ts → lib/formats/index.d.ts} +1 -6
- package/lib/formats/index.js +9 -0
- package/lib/formats/kraemer.d.ts +14 -0
- package/lib/formats/kraemer.js +131 -0
- package/lib/formats/oware.d.ts +7 -0
- package/lib/formats/oware.js +129 -0
- package/lib/formats/solv.d.ts +14 -0
- package/lib/formats/solv.js +151 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +5 -0
- package/{src/model/category.ts → lib/model/category.d.ts} +1 -2
- package/lib/model/category.js +2 -0
- package/{src/model/competition.ts → lib/model/competition.d.ts} +1 -2
- package/lib/model/competition.js +2 -0
- package/lib/model/ranking.d.ts +27 -0
- package/{src/model/ranking.ts → lib/model/ranking.js} +6 -37
- package/lib/model/runner.d.ts +21 -0
- package/lib/model/runner.js +8 -0
- package/{src/model/split.ts → lib/model/split.d.ts} +2 -2
- package/lib/model/split.js +2 -0
- package/lib/time.d.ts +10 -0
- package/{src/time.ts → lib/time.js} +22 -18
- package/lib/utils/anonymizer.d.ts +2 -0
- package/{src/utils/anonymizer.ts → lib/utils/anonymizer.js} +22 -28
- package/lib/utils/ranking.d.ts +71 -0
- package/lib/utils/ranking.js +383 -0
- package/package.json +9 -5
- package/lib/analyzis.js +0 -25
- package/src/format.ts +0 -11
- package/src/formats/kraemer.ts +0 -145
- package/src/formats/oware.ts +0 -143
- package/src/formats/solv.ts +0 -169
- package/src/index.ts +0 -5
- package/src/model/runner.ts +0 -23
- package/src/utils/ranking.ts +0 -535
- package/src/utils/reorganize.ts +0 -212
- package/test/butterfly-oware.csv +0 -120
- package/test/kraemer-test.ts +0 -78
- package/test/kraemer.csv +0 -130
- package/test/oware-test.ts +0 -79
- package/test/oware.csv +0 -101
- package/test/ranking-test.ts +0 -55
- package/test/solv-test.ts +0 -65
- package/test/solv.csv +0 -1833
- package/test/test.js +0 -14
- package/tsconfig.json +0 -11
package/src/utils/ranking.ts
DELETED
|
@@ -1,535 +0,0 @@
|
|
|
1
|
-
import { Runner } from "../model/runner";
|
|
2
|
-
import { Split } from "../model/split";
|
|
3
|
-
import { parseTime, formatTime } from "../time";
|
|
4
|
-
|
|
5
|
-
function invalidTime(time:number|undefined) {
|
|
6
|
-
return time === undefined || time < 0;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
function validTime(time:number|undefined) {
|
|
10
|
-
return !invalidTime(time);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function sum(a1: number, a2: number) {
|
|
14
|
-
return a1 + a2;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface Course {
|
|
18
|
-
code: string;
|
|
19
|
-
runners: number[]
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface RunnerLegs {
|
|
23
|
-
[key: string]: RunnerLeg;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface RunnerLeg {
|
|
27
|
-
runners: RunnerLegEntry[];
|
|
28
|
-
code: string;
|
|
29
|
-
idealSplit?: number;
|
|
30
|
-
fastestSplit?: number;
|
|
31
|
-
spread: [number, number];
|
|
32
|
-
weight?: number;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export interface RunnerLegEntry {
|
|
36
|
-
id: number;
|
|
37
|
-
fullName: string;
|
|
38
|
-
time: number;
|
|
39
|
-
split: number;
|
|
40
|
-
splitBehind?: number;
|
|
41
|
-
splitRank: number;
|
|
42
|
-
performanceIndex?: number;
|
|
43
|
-
overallRank?: number;
|
|
44
|
-
overallBehind?: string;
|
|
45
|
-
idealBehind?: string;
|
|
46
|
-
leg?: string;
|
|
47
|
-
position?: number;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Assigns the following property to every valid leg of a runner.
|
|
53
|
-
*
|
|
54
|
-
* - splitBehind: how much time behind the faster runner
|
|
55
|
-
* - splitRank: rank on corresponding leg
|
|
56
|
-
* - leg: code of leg
|
|
57
|
-
*
|
|
58
|
-
* @param {RankingRunner[]} runners
|
|
59
|
-
* @param {RunnerLegs} legs
|
|
60
|
-
*/
|
|
61
|
-
function assignLegInfoToSplits(runners: RankingRunner[], legs: RunnerLegs): void {
|
|
62
|
-
runners.forEach((runner) => {
|
|
63
|
-
runner.splits
|
|
64
|
-
.filter((s) => validTime(s.splitTime))
|
|
65
|
-
.forEach((split) => {
|
|
66
|
-
let leg = legs[split.legCode];
|
|
67
|
-
|
|
68
|
-
if (!leg) {
|
|
69
|
-
throw "leg with code " + split.leg + " not defined!";
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
split.legCode = leg.code;
|
|
73
|
-
|
|
74
|
-
let legRunner = leg.runners.find((r) => r.id === runner.id);
|
|
75
|
-
|
|
76
|
-
if (legRunner) {
|
|
77
|
-
split.leg.idealBehind = leg.idealSplit && legRunner.split - leg.idealSplit;
|
|
78
|
-
split.leg.behind = legRunner.splitBehind;
|
|
79
|
-
split.leg.rank = legRunner.splitRank;
|
|
80
|
-
split.performanceIndex = legRunner.performanceIndex;
|
|
81
|
-
}
|
|
82
|
-
split.weight = leg.weight;
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function rank(runners: RankingRunner[]) {
|
|
88
|
-
runners.forEach((runner, idx) => {
|
|
89
|
-
if (idx === 0) {
|
|
90
|
-
runner.rank = 1;
|
|
91
|
-
} else {
|
|
92
|
-
let prev = runners[idx - 1];
|
|
93
|
-
if (prev.time === runner.time) {
|
|
94
|
-
runner.rank = prev.rank;
|
|
95
|
-
} else if (parseTime(runner.time)) {
|
|
96
|
-
runner.rank = idx + 1;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
interface RankingRunner {
|
|
103
|
-
// the unique identifier of this runner
|
|
104
|
-
id: number;
|
|
105
|
-
|
|
106
|
-
// the overall rank of this runner
|
|
107
|
-
rank?: number;
|
|
108
|
-
|
|
109
|
-
// the full name of this runner
|
|
110
|
-
fullName: string;
|
|
111
|
-
|
|
112
|
-
// the overall time of this runner
|
|
113
|
-
time?: string;
|
|
114
|
-
|
|
115
|
-
// the year of birth of this runner
|
|
116
|
-
yearOfBirth?: string;
|
|
117
|
-
|
|
118
|
-
// the city of this runner
|
|
119
|
-
city?: string;
|
|
120
|
-
|
|
121
|
-
// the club of this runner
|
|
122
|
-
club?: string;
|
|
123
|
-
|
|
124
|
-
// the splits of this runner
|
|
125
|
-
splits: RankingSplit[];
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
interface RankingSplit {
|
|
129
|
-
|
|
130
|
-
// the control code of this leg
|
|
131
|
-
code: string;
|
|
132
|
-
|
|
133
|
-
// the leg identifier for this leg (from-to)
|
|
134
|
-
legCode: string;
|
|
135
|
-
|
|
136
|
-
// the punch time from the start of the race
|
|
137
|
-
time?: number;
|
|
138
|
-
|
|
139
|
-
// the split time for this leg
|
|
140
|
-
splitTime?: number;
|
|
141
|
-
|
|
142
|
-
overall: RankingInfo;
|
|
143
|
-
|
|
144
|
-
leg: SplitInfo;
|
|
145
|
-
|
|
146
|
-
// the performance index for this runner on this leg
|
|
147
|
-
performanceIndex: number | undefined;
|
|
148
|
-
|
|
149
|
-
// defines the start position of this leg as a percentage
|
|
150
|
-
position: number;
|
|
151
|
-
|
|
152
|
-
// defines the percentage of the overall time this leg represents
|
|
153
|
-
weight: number | undefined;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
interface RankingInfo {
|
|
157
|
-
rank?: number;
|
|
158
|
-
behind?: number;
|
|
159
|
-
idealBehind?: number;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
interface SplitInfo extends RankingInfo {
|
|
163
|
-
performanceIndex?: number;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
export function parseRanking(runners: Runner[]) {
|
|
167
|
-
const courses = defineCourses(runners);
|
|
168
|
-
|
|
169
|
-
// prepare the result by defining the runners and their splits
|
|
170
|
-
const rankingRunners = defineRunners(runners);
|
|
171
|
-
|
|
172
|
-
// prepare auxiliary data about the legs needed to calculate ideal times, weights, ...
|
|
173
|
-
const legs = defineLegs(rankingRunners);
|
|
174
|
-
|
|
175
|
-
// calculate the ideal time [s]
|
|
176
|
-
const idealTime = Object.keys(legs)
|
|
177
|
-
.map((code) => legs[code].idealSplit)
|
|
178
|
-
.filter((time) => time !== undefined)
|
|
179
|
-
.reduce(sum);
|
|
180
|
-
|
|
181
|
-
console.log("ideal time: ", idealTime);
|
|
182
|
-
|
|
183
|
-
// each leg's weight is calculated regarding as a ratio of the ideal split time to the ideal time
|
|
184
|
-
Object.keys(legs).forEach((code) => {
|
|
185
|
-
let leg = legs[code];
|
|
186
|
-
if (leg.idealSplit && idealTime > 0) {
|
|
187
|
-
leg.weight = leg.idealSplit / idealTime;
|
|
188
|
-
}
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
// now assing the leg information (such as idealTime, weight, ...) to the individual splits of the runners
|
|
192
|
-
assignLegInfoToSplits(rankingRunners, legs);
|
|
193
|
-
|
|
194
|
-
rankingRunners.forEach((runner) => {
|
|
195
|
-
let behind = 0;
|
|
196
|
-
let weightSum = 0;
|
|
197
|
-
runner.splits.forEach((split) => {
|
|
198
|
-
behind += split.leg.idealBehind!;
|
|
199
|
-
split.overall = {
|
|
200
|
-
behind: behind
|
|
201
|
-
};
|
|
202
|
-
split.position = weightSum + split.weight!;
|
|
203
|
-
|
|
204
|
-
if (split.weight) {
|
|
205
|
-
weightSum += split.weight;
|
|
206
|
-
}
|
|
207
|
-
});
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
// function of function calculating the time at an arbitrary position for a given runner
|
|
211
|
-
let timeFn =
|
|
212
|
-
(runner: RankingRunner) =>
|
|
213
|
-
(pos: number): number | undefined => {
|
|
214
|
-
if (pos === 0) {
|
|
215
|
-
return 0;
|
|
216
|
-
} else if (isNaN(pos)) {
|
|
217
|
-
return undefined;
|
|
218
|
-
} else if (pos >= 1) {
|
|
219
|
-
if (!parseTime(runner.time)) {
|
|
220
|
-
return undefined;
|
|
221
|
-
}
|
|
222
|
-
return parseTime(runner.time)!;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
let idx = 0;
|
|
226
|
-
let weightSum = 0;
|
|
227
|
-
let prevTime = 0;
|
|
228
|
-
for (idx = 0; idx < runner.splits.length; idx++) {
|
|
229
|
-
let split = runner.splits[idx];
|
|
230
|
-
if (weightSum + split.weight! >= pos) {
|
|
231
|
-
break;
|
|
232
|
-
}
|
|
233
|
-
weightSum += split.weight!;
|
|
234
|
-
prevTime = split.time!
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
let prev = idx === 0 ? { position: 0, time: 0 } : runner.splits[idx - 1];
|
|
238
|
-
let next = runner.splits[idx];
|
|
239
|
-
|
|
240
|
-
if (
|
|
241
|
-
prev === undefined ||
|
|
242
|
-
next === undefined ||
|
|
243
|
-
invalidTime(prev.time) ||
|
|
244
|
-
invalidTime(next.splitTime)
|
|
245
|
-
) {
|
|
246
|
-
return undefined;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
return prevTime! + ((pos - prev.position) / next.weight!) * next.splitTime!;
|
|
250
|
-
};
|
|
251
|
-
|
|
252
|
-
// function returning the times at a given position for all runners
|
|
253
|
-
const memo = new Map<number, { id: number, time: number | undefined}[]>();
|
|
254
|
-
const timesAtPosition = (pos: number): { id: number, time: number | undefined}[] => {
|
|
255
|
-
if (memo.has(pos)) {
|
|
256
|
-
return memo.get(pos)!;
|
|
257
|
-
}
|
|
258
|
-
const result = rankingRunners.map((runner) => {
|
|
259
|
-
return { id: runner.id, time: timeFn(runner)(pos) };
|
|
260
|
-
});
|
|
261
|
-
memo.set(pos, result);
|
|
262
|
-
return result;
|
|
263
|
-
};
|
|
264
|
-
|
|
265
|
-
rankingRunners.forEach((runner) => {
|
|
266
|
-
runner.splits.forEach((split) => {
|
|
267
|
-
const times = timesAtPosition(split.position)
|
|
268
|
-
.filter((entry) => entry.time && entry.time > 0)
|
|
269
|
-
.map((entry) => {
|
|
270
|
-
return { id: entry.id, time: entry.time! };
|
|
271
|
-
});
|
|
272
|
-
times.sort((t1, t2) => t1.time - t2.time);
|
|
273
|
-
|
|
274
|
-
if (!split.position || isNaN(split.position)) {
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
if (!times || times.length === 0) {
|
|
279
|
-
console.log(
|
|
280
|
-
"no times at position ",
|
|
281
|
-
split.position,
|
|
282
|
-
split.weight,
|
|
283
|
-
" for runner ",
|
|
284
|
-
runner.fullName,
|
|
285
|
-
runner.time,
|
|
286
|
-
times.length
|
|
287
|
-
);
|
|
288
|
-
return;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
let rank = 1;
|
|
292
|
-
|
|
293
|
-
const lastTime = times[0].time;
|
|
294
|
-
for (let idx = 0; idx < times.length; idx++) {
|
|
295
|
-
const entry = times[idx];
|
|
296
|
-
|
|
297
|
-
if (lastTime < entry.time) {
|
|
298
|
-
rank++;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
if (runner.id === entry.id) {
|
|
302
|
-
const idealSplitTime = times.slice(0, 5).reduce((sum, t) => sum + t.time!, 0) / Math.min(5, times.length);
|
|
303
|
-
const fastestTime = times[0].time;
|
|
304
|
-
split.overall.rank = rank;
|
|
305
|
-
split.overall.behind = entry.time - fastestTime;
|
|
306
|
-
split.overall.idealBehind = Math.round(entry.time - idealSplitTime);
|
|
307
|
-
break;
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
});
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
// calculate the overall rank
|
|
314
|
-
rank(rankingRunners);
|
|
315
|
-
|
|
316
|
-
console.log(rankingRunners[0].fullName, rankingRunners[0].splits[rankingRunners[0].splits.length - 1]);
|
|
317
|
-
|
|
318
|
-
Object.values(legs).forEach((leg) => {
|
|
319
|
-
const ideal = leg.idealSplit!;
|
|
320
|
-
const min = leg.runners.filter((runner) => !isNaN(runner.split) && runner.split !== undefined).map((runner) => runner.split).reduce((min, split) => Math.min(min, split - ideal), Number.MAX_VALUE);
|
|
321
|
-
const max = leg.runners.filter((runner) => !isNaN(runner.split) && runner.split !== undefined).map((runner) => runner.split).reduce((max, split) => Math.max(max, split - ideal), Number.MIN_VALUE);
|
|
322
|
-
leg.spread = [min, max];
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
console.log("leg runner", legs[Object.keys(legs)[1]].runners[1]);
|
|
326
|
-
|
|
327
|
-
return {
|
|
328
|
-
courses,
|
|
329
|
-
runners: rankingRunners,
|
|
330
|
-
legs: Object.values(legs).map((leg) => ({
|
|
331
|
-
code: leg.code,
|
|
332
|
-
spread: leg.spread,
|
|
333
|
-
idealSplit: leg.idealSplit,
|
|
334
|
-
fastestSplit: leg.fastestSplit,
|
|
335
|
-
weight: leg.weight,
|
|
336
|
-
})),
|
|
337
|
-
};
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
/**
|
|
341
|
-
* Define the sources ran by the given runners. If all runner ran the same
|
|
342
|
-
* course, then only one course will be defined.
|
|
343
|
-
*
|
|
344
|
-
* @param runners the list of runners
|
|
345
|
-
* @returns the defined courses
|
|
346
|
-
*/
|
|
347
|
-
function defineCourses(runners: Runner[]): Course[] {
|
|
348
|
-
let courses:{[key:string]: Course} = {};
|
|
349
|
-
runners.filter((runner) => validTime(parseTime(runner.time))).forEach((runner) => {
|
|
350
|
-
let course = "St," + runner.splits.map((split) => split.code).join(",");
|
|
351
|
-
if (!courses[course]) {
|
|
352
|
-
courses[course] = {
|
|
353
|
-
code: course,
|
|
354
|
-
runners: [runner.id],
|
|
355
|
-
};
|
|
356
|
-
} else {
|
|
357
|
-
courses[course].runners.push(runner.id);
|
|
358
|
-
}
|
|
359
|
-
});
|
|
360
|
-
return Object.keys(courses).map((key) => courses[key]);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
function defineRunners(runners:Runner[]): RankingRunner[] {
|
|
364
|
-
return runners.filter((runner) => runner.splits.length > 0 && runner.splits.every((split) => split.code)).map((runner) => {
|
|
365
|
-
const lastSplit = defineRunnerLegSplit({ code: "Zi", time: runner.time }, runner.splits.length, runner);
|
|
366
|
-
return {
|
|
367
|
-
id: runner.id,
|
|
368
|
-
rank: undefined,
|
|
369
|
-
fullName: runner.fullName,
|
|
370
|
-
time: runner.time,
|
|
371
|
-
yearOfBirth: runner.yearOfBirth,
|
|
372
|
-
city: runner.city,
|
|
373
|
-
club: runner.club,
|
|
374
|
-
splits: runner.splits.map((split, idx) => defineRunnerLegSplit(split, idx, runner)).concat([lastSplit]),
|
|
375
|
-
};
|
|
376
|
-
});
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
function defineRunnerLegSplit(split: {
|
|
380
|
-
code: string;
|
|
381
|
-
time: string | undefined;
|
|
382
|
-
}, idx: number, runner: Runner): RankingSplit {
|
|
383
|
-
var splitTime: number | undefined = undefined;
|
|
384
|
-
if (split.time === "-") {
|
|
385
|
-
splitTime = undefined;
|
|
386
|
-
} else if (idx === 0) {
|
|
387
|
-
splitTime = parseTime(split.time) ? parseTime(split.time)! : undefined;
|
|
388
|
-
} else {
|
|
389
|
-
let current = parseTime(split.time);
|
|
390
|
-
let previous = parseTime(runner.splits[idx - 1].time);
|
|
391
|
-
if (!current || !previous) {
|
|
392
|
-
splitTime = undefined;
|
|
393
|
-
} else {
|
|
394
|
-
splitTime = current - previous;
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
return {
|
|
399
|
-
code: split.code,
|
|
400
|
-
legCode: legCode(runner.splits, idx),
|
|
401
|
-
time: parseTime(split.time),
|
|
402
|
-
splitTime: splitTime,
|
|
403
|
-
leg: {
|
|
404
|
-
rank: undefined,
|
|
405
|
-
behind: 0,
|
|
406
|
-
idealBehind: undefined
|
|
407
|
-
},
|
|
408
|
-
overall: {
|
|
409
|
-
rank: undefined,
|
|
410
|
-
behind: 0,
|
|
411
|
-
idealBehind: undefined
|
|
412
|
-
},
|
|
413
|
-
performanceIndex: undefined,
|
|
414
|
-
position: 0,
|
|
415
|
-
weight: undefined
|
|
416
|
-
};
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
function legCode(splits: Split[], idx: number) {
|
|
420
|
-
if (idx === 0) {
|
|
421
|
-
return "St-" + splits[0].code;
|
|
422
|
-
} else if (idx === splits.length) {
|
|
423
|
-
return splits[idx - 1].code + "-Zi";
|
|
424
|
-
} else {
|
|
425
|
-
return splits[idx - 1].code + "-" + splits[idx].code;
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
function defineLegs(runners: RankingRunner[]) {
|
|
430
|
-
const legs = Array.from(runners
|
|
431
|
-
.map((runner) =>
|
|
432
|
-
runner.splits.map((split, idx) => {
|
|
433
|
-
let from = idx === 0 ? "St" : runner.splits[idx - 1].code;
|
|
434
|
-
let to = split.code;
|
|
435
|
-
let code = from + "-" + to;
|
|
436
|
-
return code;
|
|
437
|
-
})
|
|
438
|
-
).reduce((a, b) => a.concat(b), [])
|
|
439
|
-
.reduce((a, b) => a.add(b), new Set<string>())).reduce((obj, code) => {
|
|
440
|
-
obj[code] = {
|
|
441
|
-
code: code,
|
|
442
|
-
runners: [],
|
|
443
|
-
spread: [0, 0],
|
|
444
|
-
};
|
|
445
|
-
return obj;
|
|
446
|
-
}, {} as RunnerLegs);
|
|
447
|
-
|
|
448
|
-
runners.forEach((runner) => {
|
|
449
|
-
runner.splits
|
|
450
|
-
//.filter((s) => validTime(s.time))
|
|
451
|
-
.forEach((split, idx) => {
|
|
452
|
-
const from = idx === 0 ? "St" : runner.splits[idx - 1].code;
|
|
453
|
-
const to = split.code;
|
|
454
|
-
const code = from + "-" + to;
|
|
455
|
-
|
|
456
|
-
const current = legs[code];
|
|
457
|
-
if (validTime(split.time)) {
|
|
458
|
-
current.runners.push({
|
|
459
|
-
id: runner.id,
|
|
460
|
-
fullName: runner.fullName,
|
|
461
|
-
splitRank: 0,
|
|
462
|
-
time: split.time!,
|
|
463
|
-
split: idx === 0
|
|
464
|
-
? split.time!
|
|
465
|
-
: split.time! - runner.splits[idx - 1].time!,
|
|
466
|
-
});
|
|
467
|
-
}
|
|
468
|
-
});
|
|
469
|
-
});
|
|
470
|
-
|
|
471
|
-
defineLegProperties(legs);
|
|
472
|
-
|
|
473
|
-
return legs;
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
/**
|
|
477
|
-
* Define properties of each leg. After this method the leg structure will be enhanced as follows:
|
|
478
|
-
*
|
|
479
|
-
* - runners are sorted per split time per leg
|
|
480
|
-
* - each leg has a property 'idealSplit' (ideal split time of this leg)
|
|
481
|
-
* - each leg has a property 'fastestSplit'
|
|
482
|
-
* - each runner entry of a leg is enhanced with 'splitBehind' and 'splitRank'
|
|
483
|
-
*
|
|
484
|
-
* @param {*} legs leg data structre (only split is relevant)
|
|
485
|
-
*/
|
|
486
|
-
function defineLegProperties(legs: RunnerLegs) {
|
|
487
|
-
Object.keys(legs).forEach((code) => {
|
|
488
|
-
let leg = legs[code];
|
|
489
|
-
leg.runners.sort(function (r1, r2) {
|
|
490
|
-
return r1.split - r2.split;
|
|
491
|
-
});
|
|
492
|
-
|
|
493
|
-
// calculate the ideal time: take up to 5 fastest on that leg
|
|
494
|
-
let selected = leg.runners
|
|
495
|
-
.slice(0, Math.min(leg.runners.length, 5))
|
|
496
|
-
.map((runner) => runner.split);
|
|
497
|
-
|
|
498
|
-
// only if there are valid splits for this leg
|
|
499
|
-
if (selected.length > 0) {
|
|
500
|
-
leg.idealSplit = Math.round(selected.reduce(sum) / selected.length);
|
|
501
|
-
|
|
502
|
-
if (leg.idealSplit < 0) {
|
|
503
|
-
throw new Error("invalid ideal split calculated for leg " + code);
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
// only if there are valid splits for this leg
|
|
508
|
-
if (leg.runners.length > 0) {
|
|
509
|
-
const fastestSplit = leg.runners[0].split;
|
|
510
|
-
leg.fastestSplit = fastestSplit;
|
|
511
|
-
leg.runners[0].splitBehind = 0;
|
|
512
|
-
leg.runners.slice(1).forEach((runner) => {
|
|
513
|
-
runner.splitBehind = runner.split - fastestSplit;
|
|
514
|
-
});
|
|
515
|
-
|
|
516
|
-
leg.runners[0].splitRank = 1;
|
|
517
|
-
|
|
518
|
-
leg.runners.forEach((runner, idx, arr) => {
|
|
519
|
-
if (idx > 0) {
|
|
520
|
-
if (runner.split === arr[idx - 1].split) {
|
|
521
|
-
runner.splitRank = arr[idx - 1].splitRank;
|
|
522
|
-
} else {
|
|
523
|
-
runner.splitRank = idx + 1;
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
if (leg.idealSplit) {
|
|
528
|
-
runner.performanceIndex = Math.round(((1.0 * leg.idealSplit) / runner.split) * 100);
|
|
529
|
-
} else {
|
|
530
|
-
console.log("cannot calculate performance index for runner ", runner.fullName, " on leg ", code);
|
|
531
|
-
}
|
|
532
|
-
});
|
|
533
|
-
}
|
|
534
|
-
});
|
|
535
|
-
}
|