@rasifix/orienteering-utils 1.0.6 → 2.0.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.
@@ -1,9 +1,12 @@
1
+ import { Competition } from "../model/competition";
2
+ import { Sex } from "../model/runner";
3
+
1
4
  const names = [
2
5
  'Gustavson', 'Hendrikson', 'Meyer', 'Marlovic', 'Torres', 'Huber',
3
6
  'Wüst', 'Zürcher', 'Berner', 'Rufer', 'Gutmann', 'Hübscher',
4
7
  'Schneller', 'Widemar', 'Rohrer', 'Kunz', 'Kinzle', 'Steinle',
5
8
  'Allemann', 'Röhrig', 'Meyer', 'Uhlmann', 'Garaio', 'Regazoni',
6
- 'Maudet', 'Zoja'
9
+ 'Maudet', 'Zoja', 'Scheller', 'Beckenbauer', 'Würsten'
7
10
  ];
8
11
 
9
12
  const firstNames = {
@@ -16,17 +19,17 @@ const firstNames = {
16
19
  ],
17
20
  m: [
18
21
  'Albert', 'Bruno', 'Chris', 'Dirk', 'David', 'Erwin', 'Francesco',
19
- 'Fritz', 'Gianni', 'Gustav', 'Hans', 'Ian', 'Jan', 'Karl', 'Lars',
22
+ 'Fritz', 'Gianni', 'Gustav', 'Hans', 'Henrik', 'Ian', 'Jan', 'Karl', 'Lars',
20
23
  'Martin', 'Marco', 'Markus', 'Nico', 'Nino', 'Otto', 'Olav',
21
24
  'Patric', 'Pablo', 'Quentin', 'Ralf', 'Rudolf', 'Simon', 'Steve',
22
- 'Thomas', 'Tim', 'Urs', 'Udo'
25
+ 'Thomas', 'Tim', 'Urs', 'Udo', 'Zan'
23
26
  ]
24
27
  };
25
28
 
26
29
  const cities = [
27
- 'Aarberg', 'Bern', 'Colombier', 'Diemerswil', 'Elm', 'Fribourg',
28
- 'Goldiwil', 'Heimiswil', 'Illiswil', 'Konolfingen', 'Lausanne',
29
- 'Lugano', 'Martigny', 'Neuchatel', 'Orbe', 'Zürich'
30
+ 'Aarberg', 'Bern', 'Burgdorf', 'Colombier', 'Diemerswil', 'Domdidier', 'Elm', 'Flawil', 'Fribourg',
31
+ 'Goldiwil', 'Heimiswil', 'Hergiswil', 'Illiswil', 'Konolfingen', 'Lausanne', 'Locarno',
32
+ 'Lugano', 'Martigny', 'Neuchatel', 'Orbe', 'Uzwil', 'Zürich'
30
33
  ];
31
34
 
32
35
  const clubs = {
@@ -34,39 +37,35 @@ const clubs = {
34
37
  names: ['Bernstein', 'Erdmannlistein', 'Blanc', 'Piz Balü', 'Bartli und Most', 'Aare', 'Reuss' ]
35
38
  };
36
39
 
37
- function random(arr) {
38
- if (arr.lenght === 0) {
39
- return null;
40
- }
40
+ function random(arr: string[]):string {
41
41
  const idx = Math.floor(Math.random() * (arr.length - 1));
42
42
  return arr[idx];
43
43
  }
44
44
 
45
- function randInt(from, to) {
45
+ function randInt(from: number, to: number) {
46
46
  return from + Math.round(Math.random() * (to - from));
47
47
  }
48
48
 
49
- module.exports.anonymize = function(event) {
50
- event.categories.forEach(category => {
49
+ export function anonymize(competition: Competition) {
50
+ competition.categories.forEach(category => {
51
51
  category.runners.forEach(runner => {
52
52
  runner.name = random(names);
53
53
 
54
- if (runner.sex === 'M' || runner.sex === 'm') {
55
- runner.firstName = random(firstNames.m);
56
- runner.sex = 'M';
57
- } else {
54
+ if (!runner.sex || runner.sex === 'f') {
58
55
  runner.firstName = random(firstNames.f);
59
- runner.sex = 'F';
56
+ runner.sex = Sex.female;
57
+ } else {
58
+ runner.firstName = random(firstNames.m);
59
+ runner.sex = Sex.male;
60
60
  }
61
61
 
62
62
  runner.fullName = runner.firstName + ' ' + runner.name;
63
63
 
64
64
  runner.city = random(cities);
65
65
  runner.club = random(clubs.prefixes) + ' ' + random(clubs.names);
66
- runner.yearOfBirth = randInt(1925, 2010);
66
+ runner.yearOfBirth = '' + randInt(1925, 2010);
67
67
  runner.nation = 'SUI';
68
- runner.ecard = Math.round(Math.random() * 1000000);
69
68
  });
70
69
  });
71
- return event;
70
+ return competition;
72
71
  };
@@ -0,0 +1,535 @@
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
+ }
@@ -0,0 +1,78 @@
1
+ import { readFileSync } from "fs";
2
+ import * as path from "path";
3
+ import * as chai from "chai";
4
+ import { KraemerFormater } from "../src/formats/kraemer";
5
+ import { Split } from "../src/model/split";
6
+
7
+ chai.should();
8
+
9
+ const csvPath = path.join(__dirname, "kraemer.csv");
10
+ const words = readFileSync(csvPath, "utf-8");
11
+
12
+ describe("kraemer", function () {
13
+ let event: any;
14
+
15
+ before(function () {
16
+ event = new KraemerFormater().parse(words);
17
+ });
18
+
19
+ describe("#parse()", function () {
20
+ it("should return parsed event", function () {
21
+ event.should.be.an("object");
22
+ });
23
+
24
+ it("should be named Anonymous Event", function () {
25
+ event.name.should.be.an("string");
26
+ event.name.should.equal("Anonymous Event");
27
+ });
28
+
29
+ it("should have a map name", function () {
30
+ event.map.should.be.an("string");
31
+ event.map.should.equal("Unknown Map");
32
+ });
33
+
34
+ it("should have an event date", function () {
35
+ event.date.should.equal("");
36
+ });
37
+
38
+ it("should have a start time", function () {
39
+ event.startTime.should.equal("");
40
+ });
41
+
42
+ it("should have 3 categories", function () {
43
+ event.categories.length.should.equal(3);
44
+ });
45
+
46
+ describe("category K", function () {
47
+ let category: any;
48
+
49
+ before(function () {
50
+ category = event.categories.find((cat: any) => cat.name === "K");
51
+ });
52
+
53
+ it("should have a category K with 58 runners", function () {
54
+ category.should.be.an("object");
55
+ category.name.should.equal("K");
56
+ category.runners.length.should.equal(38);
57
+ });
58
+
59
+ it("should have a runner named Lena Torres at rank 1", function () {
60
+ const runner = category.runners[0];
61
+ runner.fullName.should.equal("Lena Torres");
62
+ runner.yearOfBirth.should.equal("1933");
63
+ runner.city.should.equal("Flawil");
64
+ runner.club.should.equal("OL Piz Balü");
65
+ });
66
+
67
+ it("Lena Torres should have a 14 splits", function () {
68
+ const runner = category.runners[0];
69
+ runner.splits.length.should.equal(14);
70
+ });
71
+
72
+ it("Lena Torres should have a 14 splits with the given split times", function () {
73
+ const runner = category.runners[0];
74
+ runner.splits.map((s: any) => s.time).join(",").should.equal("3:30,5:55,7:05,8:07,9:15,11:30,13:39,15:08,17:21,19:17,20:58,25:43,27:31,29:26");
75
+ });
76
+ });
77
+ });
78
+ });