@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
package/lib/time.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Parses a time string that must satisfy the following regex:
3
+ *
4
+ * /(-)?[0-9]?[0-9]:[0-9][0-9](:[0-9][0-9])?/
5
+ *
6
+ * @param str input string
7
+ * @returns the number of seconds in the given input string or null if input is not parseable
8
+ */
9
+ export declare function parseTime(str: string | undefined): number | undefined;
10
+ export declare function formatTime(seconds: number | undefined): string;
@@ -1,48 +1,52 @@
1
- function pad(value:number) {
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseTime = parseTime;
4
+ exports.formatTime = formatTime;
5
+ function pad(value) {
2
6
  return value < 10 ? '0' + value : value;
3
- };
4
-
7
+ }
8
+ ;
5
9
  var regex = /(-)?[0-9]?[0-9]:[0-9][0-9](:[0-9][0-9])?/;
6
-
7
10
  /**
8
11
  * Parses a time string that must satisfy the following regex:
9
- *
12
+ *
10
13
  * /(-)?[0-9]?[0-9]:[0-9][0-9](:[0-9][0-9])?/
11
- *
14
+ *
12
15
  * @param str input string
13
16
  * @returns the number of seconds in the given input string or null if input is not parseable
14
17
  */
15
- export function parseTime(str:string|undefined):number|undefined {
18
+ function parseTime(str) {
16
19
  if (!str) {
17
20
  return undefined;
18
- } else if (typeof str !== 'string') {
21
+ }
22
+ else if (typeof str !== 'string') {
19
23
  return undefined;
20
- } else if (!regex.test(str)) {
24
+ }
25
+ else if (!regex.test(str)) {
21
26
  return undefined;
22
27
  }
23
-
24
28
  var split = str.split(":");
25
- var result:number = NaN;
29
+ var result = NaN;
26
30
  if (split.length === 2) {
27
31
  var negative = split[0][0] === '-';
28
32
  var minutes = parseInt(split[0], 10);
29
33
  result = (negative ? -1 : 1) * (Math.abs(minutes) * 60 + parseInt(split[1], 10));
30
- } else if (split.length === 3) {
34
+ }
35
+ else if (split.length === 3) {
31
36
  result = parseInt(split[0], 10) * 3600 + parseInt(split[1], 10) * 60 + parseInt(split[2], 10);
32
37
  }
33
-
34
38
  return isNaN(result) ? undefined : result;
35
39
  }
36
-
37
- export function formatTime(seconds:number|undefined) {
40
+ function formatTime(seconds) {
38
41
  if (!seconds) {
39
42
  return undefined;
40
43
  }
41
- const sign = seconds < 0 ? '-' : '';
42
- const value = Math.abs(seconds);
44
+ var sign = seconds < 0 ? '-' : '';
45
+ var value = Math.abs(seconds);
43
46
  if (value >= 3600) {
44
47
  return sign + Math.floor(value / 3600) + ":" + pad(Math.floor(value / 60) % 60) + ":" + pad(value % 60);
45
- } else {
48
+ }
49
+ else {
46
50
  return Math.floor(value / 60) + ":" + pad(value % 60);
47
51
  }
48
52
  }
@@ -0,0 +1,2 @@
1
+ import { Competition } from "../model/competition";
2
+ export declare function anonymize(competition: Competition): Competition;
@@ -1,15 +1,15 @@
1
- import { Competition } from "../model/competition";
2
- import { Sex } from "../model/runner";
3
-
4
- const names = [
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.anonymize = anonymize;
4
+ var runner_1 = require("../model/runner");
5
+ var names = [
5
6
  'Gustavson', 'Hendrikson', 'Meyer', 'Marlovic', 'Torres', 'Huber',
6
7
  'Wüst', 'Zürcher', 'Berner', 'Rufer', 'Gutmann', 'Hübscher',
7
8
  'Schneller', 'Widemar', 'Rohrer', 'Kunz', 'Kinzle', 'Steinle',
8
9
  'Allemann', 'Röhrig', 'Meyer', 'Uhlmann', 'Garaio', 'Regazoni',
9
10
  'Maudet', 'Zoja', 'Scheller', 'Beckenbauer', 'Würsten'
10
11
  ];
11
-
12
- const firstNames = {
12
+ var firstNames = {
13
13
  f: [
14
14
  'Anna', 'Alia', 'Ava', 'Berta', 'Benita', 'Carla', 'Cloe',
15
15
  'Dina', 'Daria', 'Eva', 'Esther', 'Elin', 'Franca', 'Franziska',
@@ -25,42 +25,35 @@ const firstNames = {
25
25
  'Thomas', 'Tim', 'Urs', 'Udo', 'Zan'
26
26
  ]
27
27
  };
28
-
29
- const cities = [
28
+ var cities = [
30
29
  'Aarberg', 'Bern', 'Burgdorf', 'Colombier', 'Diemerswil', 'Domdidier', 'Elm', 'Flawil', 'Fribourg',
31
30
  'Goldiwil', 'Heimiswil', 'Hergiswil', 'Illiswil', 'Konolfingen', 'Lausanne', 'Locarno',
32
31
  'Lugano', 'Martigny', 'Neuchatel', 'Orbe', 'Uzwil', 'Zürich'
33
32
  ];
34
-
35
- const clubs = {
36
- prefixes: [ 'OLG', 'OLV', 'OL', 'CA' ],
37
- names: ['Bernstein', 'Erdmannlistein', 'Blanc', 'Piz Balü', 'Bartli und Most', 'Aare', 'Reuss' ]
33
+ var clubs = {
34
+ prefixes: ['OLG', 'OLV', 'OL', 'CA'],
35
+ names: ['Bernstein', 'Erdmannlistein', 'Blanc', 'Piz Balü', 'Bartli und Most', 'Aare', 'Reuss']
38
36
  };
39
-
40
- function random(arr: string[]):string {
41
- const idx = Math.floor(Math.random() * (arr.length - 1));
37
+ function random(arr) {
38
+ var idx = Math.floor(Math.random() * (arr.length - 1));
42
39
  return arr[idx];
43
40
  }
44
-
45
- function randInt(from: number, to: number) {
41
+ function randInt(from, to) {
46
42
  return from + Math.round(Math.random() * (to - from));
47
43
  }
48
-
49
- export function anonymize(competition: Competition) {
50
- competition.categories.forEach(category => {
51
- category.runners.forEach(runner => {
44
+ function anonymize(competition) {
45
+ competition.categories.forEach(function (category) {
46
+ category.runners.forEach(function (runner) {
52
47
  runner.name = random(names);
53
-
54
48
  if (!runner.sex || runner.sex === 'f') {
55
49
  runner.firstName = random(firstNames.f);
56
- runner.sex = Sex.female;
57
- } else {
50
+ runner.sex = runner_1.Sex.female;
51
+ }
52
+ else {
58
53
  runner.firstName = random(firstNames.m);
59
- runner.sex = Sex.male;
54
+ runner.sex = runner_1.Sex.male;
60
55
  }
61
-
62
56
  runner.fullName = runner.firstName + ' ' + runner.name;
63
-
64
57
  runner.city = random(cities);
65
58
  runner.club = random(clubs.prefixes) + ' ' + random(clubs.names);
66
59
  runner.yearOfBirth = '' + randInt(1925, 2010);
@@ -68,4 +61,5 @@ export function anonymize(competition: Competition) {
68
61
  });
69
62
  });
70
63
  return competition;
71
- };
64
+ }
65
+ ;
@@ -0,0 +1,71 @@
1
+ import { Runner } from "../model/runner";
2
+ export interface Course {
3
+ code: string;
4
+ runners: number[];
5
+ }
6
+ export interface RunnerLegs {
7
+ [key: string]: RunnerLeg;
8
+ }
9
+ export interface RunnerLeg {
10
+ runners: RunnerLegEntry[];
11
+ code: string;
12
+ idealSplit?: number;
13
+ fastestSplit?: number;
14
+ spread: [number, number];
15
+ weight?: number;
16
+ }
17
+ export interface RunnerLegEntry {
18
+ id: number;
19
+ fullName: string;
20
+ time: number;
21
+ split: number;
22
+ splitBehind?: number;
23
+ splitRank: number;
24
+ performanceIndex?: number;
25
+ overallRank?: number;
26
+ overallBehind?: string;
27
+ idealBehind?: string;
28
+ leg?: string;
29
+ position?: number;
30
+ }
31
+ interface RankingRunner {
32
+ id: number;
33
+ rank?: number;
34
+ fullName: string;
35
+ time?: string;
36
+ yearOfBirth?: string;
37
+ city?: string;
38
+ club?: string;
39
+ splits: RankingSplit[];
40
+ }
41
+ interface RankingSplit {
42
+ code: string;
43
+ legCode: string;
44
+ time?: number;
45
+ splitTime?: number;
46
+ overall: RankingInfo;
47
+ leg: SplitInfo;
48
+ performanceIndex: number | undefined;
49
+ position: number;
50
+ weight: number | undefined;
51
+ }
52
+ interface RankingInfo {
53
+ rank?: number;
54
+ behind?: number;
55
+ idealBehind?: number;
56
+ }
57
+ interface SplitInfo extends RankingInfo {
58
+ performanceIndex?: number;
59
+ }
60
+ export declare function parseRanking(runners: Runner[]): {
61
+ courses: Course[];
62
+ runners: RankingRunner[];
63
+ legs: {
64
+ code: string;
65
+ spread: [number, number];
66
+ idealSplit: number;
67
+ fastestSplit: number;
68
+ weight: number;
69
+ }[];
70
+ };
71
+ export {};
@@ -0,0 +1,383 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseRanking = parseRanking;
4
+ var time_1 = require("../time");
5
+ function invalidTime(time) {
6
+ return time === undefined || time < 0;
7
+ }
8
+ function validTime(time) {
9
+ return !invalidTime(time);
10
+ }
11
+ function sum(a1, a2) {
12
+ return a1 + a2;
13
+ }
14
+ /**
15
+ * Assigns the following property to every valid leg of a runner.
16
+ *
17
+ * - splitBehind: how much time behind the faster runner
18
+ * - splitRank: rank on corresponding leg
19
+ * - leg: code of leg
20
+ *
21
+ * @param {RankingRunner[]} runners
22
+ * @param {RunnerLegs} legs
23
+ */
24
+ function assignLegInfoToSplits(runners, legs) {
25
+ runners.forEach(function (runner) {
26
+ runner.splits
27
+ .filter(function (s) { return validTime(s.splitTime); })
28
+ .forEach(function (split) {
29
+ var leg = legs[split.legCode];
30
+ if (!leg) {
31
+ throw "leg with code " + split.leg + " not defined!";
32
+ }
33
+ split.legCode = leg.code;
34
+ var legRunner = leg.runners.find(function (r) { return r.id === runner.id; });
35
+ if (legRunner) {
36
+ split.leg.idealBehind = leg.idealSplit && legRunner.split - leg.idealSplit;
37
+ split.leg.behind = legRunner.splitBehind;
38
+ split.leg.rank = legRunner.splitRank;
39
+ split.performanceIndex = legRunner.performanceIndex;
40
+ }
41
+ split.weight = leg.weight;
42
+ });
43
+ });
44
+ }
45
+ function rank(runners) {
46
+ runners.forEach(function (runner, idx) {
47
+ if (idx === 0) {
48
+ runner.rank = 1;
49
+ }
50
+ else {
51
+ var prev = runners[idx - 1];
52
+ if (prev.time === runner.time) {
53
+ runner.rank = prev.rank;
54
+ }
55
+ else if ((0, time_1.parseTime)(runner.time)) {
56
+ runner.rank = idx + 1;
57
+ }
58
+ }
59
+ });
60
+ }
61
+ function parseRanking(runners) {
62
+ var courses = defineCourses(runners);
63
+ // prepare the result by defining the runners and their splits
64
+ var rankingRunners = defineRunners(runners);
65
+ // prepare auxiliary data about the legs needed to calculate ideal times, weights, ...
66
+ var legs = defineLegs(rankingRunners);
67
+ // calculate the ideal time [s]
68
+ var idealTime = Object.keys(legs)
69
+ .map(function (code) { return legs[code].idealSplit; })
70
+ .filter(function (time) { return time !== undefined; })
71
+ .reduce(sum);
72
+ console.log("ideal time: ", idealTime);
73
+ // each leg's weight is calculated regarding as a ratio of the ideal split time to the ideal time
74
+ Object.keys(legs).forEach(function (code) {
75
+ var leg = legs[code];
76
+ if (leg.idealSplit && idealTime > 0) {
77
+ leg.weight = leg.idealSplit / idealTime;
78
+ }
79
+ });
80
+ // now assing the leg information (such as idealTime, weight, ...) to the individual splits of the runners
81
+ assignLegInfoToSplits(rankingRunners, legs);
82
+ rankingRunners.forEach(function (runner) {
83
+ var behind = 0;
84
+ var weightSum = 0;
85
+ runner.splits.forEach(function (split) {
86
+ behind += split.leg.idealBehind;
87
+ split.overall = {
88
+ behind: behind
89
+ };
90
+ split.position = weightSum + split.weight;
91
+ if (split.weight) {
92
+ weightSum += split.weight;
93
+ }
94
+ });
95
+ });
96
+ // function of function calculating the time at an arbitrary position for a given runner
97
+ var timeFn = function (runner) {
98
+ return function (pos) {
99
+ if (pos === 0) {
100
+ return 0;
101
+ }
102
+ else if (isNaN(pos)) {
103
+ return undefined;
104
+ }
105
+ else if (pos >= 1) {
106
+ if (!(0, time_1.parseTime)(runner.time)) {
107
+ return undefined;
108
+ }
109
+ return (0, time_1.parseTime)(runner.time);
110
+ }
111
+ var idx = 0;
112
+ var weightSum = 0;
113
+ var prevTime = 0;
114
+ for (idx = 0; idx < runner.splits.length; idx++) {
115
+ var split = runner.splits[idx];
116
+ if (weightSum + split.weight >= pos) {
117
+ break;
118
+ }
119
+ weightSum += split.weight;
120
+ prevTime = split.time;
121
+ }
122
+ var prev = idx === 0 ? { position: 0, time: 0 } : runner.splits[idx - 1];
123
+ var next = runner.splits[idx];
124
+ if (prev === undefined ||
125
+ next === undefined ||
126
+ invalidTime(prev.time) ||
127
+ invalidTime(next.splitTime)) {
128
+ return undefined;
129
+ }
130
+ return prevTime + ((pos - prev.position) / next.weight) * next.splitTime;
131
+ };
132
+ };
133
+ // function returning the times at a given position for all runners
134
+ var memo = new Map();
135
+ var timesAtPosition = function (pos) {
136
+ if (memo.has(pos)) {
137
+ return memo.get(pos);
138
+ }
139
+ var result = rankingRunners.map(function (runner) {
140
+ return { id: runner.id, time: timeFn(runner)(pos) };
141
+ });
142
+ memo.set(pos, result);
143
+ return result;
144
+ };
145
+ rankingRunners.forEach(function (runner) {
146
+ runner.splits.forEach(function (split) {
147
+ var times = timesAtPosition(split.position)
148
+ .filter(function (entry) { return entry.time && entry.time > 0; })
149
+ .map(function (entry) {
150
+ return { id: entry.id, time: entry.time };
151
+ });
152
+ times.sort(function (t1, t2) { return t1.time - t2.time; });
153
+ if (!split.position || isNaN(split.position)) {
154
+ return;
155
+ }
156
+ if (!times || times.length === 0) {
157
+ console.log("no times at position ", split.position, split.weight, " for runner ", runner.fullName, runner.time, times.length);
158
+ return;
159
+ }
160
+ var rank = 1;
161
+ var lastTime = times[0].time;
162
+ for (var idx = 0; idx < times.length; idx++) {
163
+ var entry = times[idx];
164
+ if (lastTime < entry.time) {
165
+ rank++;
166
+ }
167
+ if (runner.id === entry.id) {
168
+ var idealSplitTime = times.slice(0, 5).reduce(function (sum, t) { return sum + t.time; }, 0) / Math.min(5, times.length);
169
+ var fastestTime = times[0].time;
170
+ split.overall.rank = rank;
171
+ split.overall.behind = entry.time - fastestTime;
172
+ split.overall.idealBehind = Math.round(entry.time - idealSplitTime);
173
+ break;
174
+ }
175
+ }
176
+ });
177
+ });
178
+ // calculate the overall rank
179
+ rank(rankingRunners);
180
+ console.log(rankingRunners[0].fullName, rankingRunners[0].splits[rankingRunners[0].splits.length - 1]);
181
+ Object.values(legs).forEach(function (leg) {
182
+ var ideal = leg.idealSplit;
183
+ var min = leg.runners.filter(function (runner) { return !isNaN(runner.split) && runner.split !== undefined; }).map(function (runner) { return runner.split; }).reduce(function (min, split) { return Math.min(min, split - ideal); }, Number.MAX_VALUE);
184
+ var max = leg.runners.filter(function (runner) { return !isNaN(runner.split) && runner.split !== undefined; }).map(function (runner) { return runner.split; }).reduce(function (max, split) { return Math.max(max, split - ideal); }, Number.MIN_VALUE);
185
+ leg.spread = [min, max];
186
+ });
187
+ console.log("leg runner", legs[Object.keys(legs)[1]].runners[1]);
188
+ return {
189
+ courses: courses,
190
+ runners: rankingRunners,
191
+ legs: Object.values(legs).map(function (leg) { return ({
192
+ code: leg.code,
193
+ spread: leg.spread,
194
+ idealSplit: leg.idealSplit,
195
+ fastestSplit: leg.fastestSplit,
196
+ weight: leg.weight,
197
+ }); }),
198
+ };
199
+ }
200
+ /**
201
+ * Define the sources ran by the given runners. If all runner ran the same
202
+ * course, then only one course will be defined.
203
+ *
204
+ * @param runners the list of runners
205
+ * @returns the defined courses
206
+ */
207
+ function defineCourses(runners) {
208
+ var courses = {};
209
+ runners.filter(function (runner) { return validTime((0, time_1.parseTime)(runner.time)); }).forEach(function (runner) {
210
+ var course = "St," + runner.splits.map(function (split) { return split.code; }).join(",");
211
+ if (!courses[course]) {
212
+ courses[course] = {
213
+ code: course,
214
+ runners: [runner.id],
215
+ };
216
+ }
217
+ else {
218
+ courses[course].runners.push(runner.id);
219
+ }
220
+ });
221
+ return Object.keys(courses).map(function (key) { return courses[key]; });
222
+ }
223
+ function defineRunners(runners) {
224
+ return runners.filter(function (runner) { return runner.splits.length > 0 && runner.splits.every(function (split) { return split.code; }); }).map(function (runner) {
225
+ var lastSplit = defineRunnerLegSplit({ code: "Zi", time: runner.time }, runner.splits.length, runner);
226
+ return {
227
+ id: runner.id,
228
+ rank: undefined,
229
+ fullName: runner.fullName,
230
+ time: runner.time,
231
+ yearOfBirth: runner.yearOfBirth,
232
+ city: runner.city,
233
+ club: runner.club,
234
+ splits: runner.splits.map(function (split, idx) { return defineRunnerLegSplit(split, idx, runner); }).concat([lastSplit]),
235
+ };
236
+ });
237
+ }
238
+ function defineRunnerLegSplit(split, idx, runner) {
239
+ var splitTime = undefined;
240
+ if (split.time === "-") {
241
+ splitTime = undefined;
242
+ }
243
+ else if (idx === 0) {
244
+ splitTime = (0, time_1.parseTime)(split.time) ? (0, time_1.parseTime)(split.time) : undefined;
245
+ }
246
+ else {
247
+ var current = (0, time_1.parseTime)(split.time);
248
+ var previous = (0, time_1.parseTime)(runner.splits[idx - 1].time);
249
+ if (!current || !previous) {
250
+ splitTime = undefined;
251
+ }
252
+ else {
253
+ splitTime = current - previous;
254
+ }
255
+ }
256
+ return {
257
+ code: split.code,
258
+ legCode: legCode(runner.splits, idx),
259
+ time: (0, time_1.parseTime)(split.time),
260
+ splitTime: splitTime,
261
+ leg: {
262
+ rank: undefined,
263
+ behind: 0,
264
+ idealBehind: undefined
265
+ },
266
+ overall: {
267
+ rank: undefined,
268
+ behind: 0,
269
+ idealBehind: undefined
270
+ },
271
+ performanceIndex: undefined,
272
+ position: 0,
273
+ weight: undefined
274
+ };
275
+ }
276
+ function legCode(splits, idx) {
277
+ if (idx === 0) {
278
+ return "St-" + splits[0].code;
279
+ }
280
+ else if (idx === splits.length) {
281
+ return splits[idx - 1].code + "-Zi";
282
+ }
283
+ else {
284
+ return splits[idx - 1].code + "-" + splits[idx].code;
285
+ }
286
+ }
287
+ function defineLegs(runners) {
288
+ var legs = Array.from(runners
289
+ .map(function (runner) {
290
+ return runner.splits.map(function (split, idx) {
291
+ var from = idx === 0 ? "St" : runner.splits[idx - 1].code;
292
+ var to = split.code;
293
+ var code = from + "-" + to;
294
+ return code;
295
+ });
296
+ }).reduce(function (a, b) { return a.concat(b); }, [])
297
+ .reduce(function (a, b) { return a.add(b); }, new Set())).reduce(function (obj, code) {
298
+ obj[code] = {
299
+ code: code,
300
+ runners: [],
301
+ spread: [0, 0],
302
+ };
303
+ return obj;
304
+ }, {});
305
+ runners.forEach(function (runner) {
306
+ runner.splits
307
+ //.filter((s) => validTime(s.time))
308
+ .forEach(function (split, idx) {
309
+ var from = idx === 0 ? "St" : runner.splits[idx - 1].code;
310
+ var to = split.code;
311
+ var code = from + "-" + to;
312
+ var current = legs[code];
313
+ if (validTime(split.time)) {
314
+ current.runners.push({
315
+ id: runner.id,
316
+ fullName: runner.fullName,
317
+ splitRank: 0,
318
+ time: split.time,
319
+ split: idx === 0
320
+ ? split.time
321
+ : split.time - runner.splits[idx - 1].time,
322
+ });
323
+ }
324
+ });
325
+ });
326
+ defineLegProperties(legs);
327
+ return legs;
328
+ }
329
+ /**
330
+ * Define properties of each leg. After this method the leg structure will be enhanced as follows:
331
+ *
332
+ * - runners are sorted per split time per leg
333
+ * - each leg has a property 'idealSplit' (ideal split time of this leg)
334
+ * - each leg has a property 'fastestSplit'
335
+ * - each runner entry of a leg is enhanced with 'splitBehind' and 'splitRank'
336
+ *
337
+ * @param {*} legs leg data structre (only split is relevant)
338
+ */
339
+ function defineLegProperties(legs) {
340
+ Object.keys(legs).forEach(function (code) {
341
+ var leg = legs[code];
342
+ leg.runners.sort(function (r1, r2) {
343
+ return r1.split - r2.split;
344
+ });
345
+ // calculate the ideal time: take up to 5 fastest on that leg
346
+ var selected = leg.runners
347
+ .slice(0, Math.min(leg.runners.length, 5))
348
+ .map(function (runner) { return runner.split; });
349
+ // only if there are valid splits for this leg
350
+ if (selected.length > 0) {
351
+ leg.idealSplit = Math.round(selected.reduce(sum) / selected.length);
352
+ if (leg.idealSplit < 0) {
353
+ throw new Error("invalid ideal split calculated for leg " + code);
354
+ }
355
+ }
356
+ // only if there are valid splits for this leg
357
+ if (leg.runners.length > 0) {
358
+ var fastestSplit_1 = leg.runners[0].split;
359
+ leg.fastestSplit = fastestSplit_1;
360
+ leg.runners[0].splitBehind = 0;
361
+ leg.runners.slice(1).forEach(function (runner) {
362
+ runner.splitBehind = runner.split - fastestSplit_1;
363
+ });
364
+ leg.runners[0].splitRank = 1;
365
+ leg.runners.forEach(function (runner, idx, arr) {
366
+ if (idx > 0) {
367
+ if (runner.split === arr[idx - 1].split) {
368
+ runner.splitRank = arr[idx - 1].splitRank;
369
+ }
370
+ else {
371
+ runner.splitRank = idx + 1;
372
+ }
373
+ }
374
+ if (leg.idealSplit) {
375
+ runner.performanceIndex = Math.round(((1.0 * leg.idealSplit) / runner.split) * 100);
376
+ }
377
+ else {
378
+ console.log("cannot calculate performance index for runner ", runner.fullName, " on leg ", code);
379
+ }
380
+ });
381
+ }
382
+ });
383
+ }
package/package.json CHANGED
@@ -1,9 +1,13 @@
1
1
  {
2
2
  "name": "@rasifix/orienteering-utils",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
4
4
  "description": "utility functions for orienteering result analyzis",
5
- "main": "index",
6
- "typings": "index",
5
+ "main": "lib/index.js",
6
+ "types": "lib/index.d.ts",
7
+ "files": [
8
+ "lib/**/*.js",
9
+ "lib/**/*.d.ts"
10
+ ],
7
11
  "scripts": {
8
12
  "test": "mocha test/test.js",
9
13
  "build": "tsc"
@@ -24,8 +28,8 @@
24
28
  "devDependencies": {
25
29
  "@types/chai": "^5.2.3",
26
30
  "@types/mocha": "^10.0.10",
27
- "@types/node": "^16.0.2",
31
+ "@types/node": "^22.10.5",
28
32
  "ts-node": "^10.9.2",
29
- "typescript": "^4.5.5"
33
+ "typescript": "^5.7.3"
30
34
  }
31
35
  }
package/lib/analyzis.js DELETED
@@ -1,25 +0,0 @@
1
- const { parseTime, formatTime } = require('./time');
2
-
3
- export function errorTime(runner, options) {
4
- let thresholdRelative = options.thresholdRelative || 1.2;
5
- let thresholdAbsolute = options.thresholdAbsolute || 10;
6
-
7
- let perfindices = runner.splits.filter(perfidx => perfidx).map(split => split.perfidx).sort((s1, s2) => s1 - s2 );
8
- let middle = null;
9
- if (perfindices.length % 2 === 1) {
10
- middle = perfindices[Math.floor(perfindices.length / 2)];
11
- } else {
12
- middle = (perfindices[perfindices.length / 2] + perfindices[perfindices.length / 2 + 1]) / 2;
13
- }
14
-
15
- let errorTime = 0;
16
- runner.splits.filter(split => split.split !== '-' && split.split !== 's').forEach(split => {
17
- let errorFreeTime = Math.round(parseTime(split.split) * (split.perfidx / middle));
18
- if (parseTime(split.split) / errorFreeTime > thresholdRelative && (parseTime(split.split) - errorFreeTime) > thresholdAbsolute) {
19
- split.timeLoss = formatTime(parseTime(split.split) - errorFreeTime);
20
- errorTime += parseTime(split.timeLoss);
21
- }
22
- });
23
-
24
- return formatTime(errorTime);
25
- }