@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.
Files changed (48) hide show
  1. package/lib/format.d.ts +6 -0
  2. package/lib/format.js +2 -0
  3. package/{src/formats/index.ts → lib/formats/index.d.ts} +1 -6
  4. package/lib/formats/index.js +9 -0
  5. package/lib/formats/kraemer.d.ts +14 -0
  6. package/lib/formats/kraemer.js +131 -0
  7. package/lib/formats/oware.d.ts +7 -0
  8. package/lib/formats/oware.js +129 -0
  9. package/lib/formats/solv.d.ts +14 -0
  10. package/lib/formats/solv.js +151 -0
  11. package/lib/index.d.ts +2 -0
  12. package/lib/index.js +5 -0
  13. package/{src/model/category.ts → lib/model/category.d.ts} +1 -2
  14. package/lib/model/category.js +2 -0
  15. package/{src/model/competition.ts → lib/model/competition.d.ts} +1 -2
  16. package/lib/model/competition.js +2 -0
  17. package/lib/model/ranking.d.ts +27 -0
  18. package/{src/model/ranking.ts → lib/model/ranking.js} +6 -37
  19. package/lib/model/runner.d.ts +21 -0
  20. package/lib/model/runner.js +8 -0
  21. package/{src/model/split.ts → lib/model/split.d.ts} +2 -2
  22. package/lib/model/split.js +2 -0
  23. package/lib/time.d.ts +10 -0
  24. package/{src/time.ts → lib/time.js} +22 -18
  25. package/lib/utils/anonymizer.d.ts +2 -0
  26. package/{src/utils/anonymizer.ts → lib/utils/anonymizer.js} +22 -28
  27. package/lib/utils/ranking.d.ts +71 -0
  28. package/lib/utils/ranking.js +383 -0
  29. package/package.json +9 -5
  30. package/lib/analyzis.js +0 -25
  31. package/src/format.ts +0 -11
  32. package/src/formats/kraemer.ts +0 -145
  33. package/src/formats/oware.ts +0 -143
  34. package/src/formats/solv.ts +0 -169
  35. package/src/index.ts +0 -5
  36. package/src/model/runner.ts +0 -23
  37. package/src/utils/ranking.ts +0 -535
  38. package/src/utils/reorganize.ts +0 -212
  39. package/test/butterfly-oware.csv +0 -120
  40. package/test/kraemer-test.ts +0 -78
  41. package/test/kraemer.csv +0 -130
  42. package/test/oware-test.ts +0 -79
  43. package/test/oware.csv +0 -101
  44. package/test/ranking-test.ts +0 -55
  45. package/test/solv-test.ts +0 -65
  46. package/test/solv.csv +0 -1833
  47. package/test/test.js +0 -14
  48. package/tsconfig.json +0 -11
@@ -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
- }