@rasifix/orienteering-utils 1.0.6 → 2.0.1

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/ranking.js DELETED
@@ -1,422 +0,0 @@
1
- const { parseTime, formatTime } = require('./time');
2
- const { reorganize } = require('./reorganize');
3
-
4
-
5
- function invalidTime(time) {
6
- return !time || time < 0;
7
- }
8
-
9
- function validTime(time) {
10
- return !invalidTime(time);
11
- }
12
-
13
- function sum(a1, a2) {
14
- return a1 + a2;
15
- }
16
-
17
- function defineCourses(runners) {
18
- let courses = { };
19
- runners.forEach(runner => {
20
- let course = defineCourse(runner);
21
- if (!courses[course]) {
22
- courses[course] = {
23
- code: course,
24
- runners: [ runner.id ]
25
- };
26
- } else {
27
- courses[course].runners.push(runner.id);
28
- }
29
- });
30
- return Object.keys(courses).map(key => courses[key]);
31
- }
32
-
33
- function defineCourse(runner) {
34
- return 'St,' + runner.splits.map(split => split.code).join(',');
35
- }
36
-
37
- function defineLegs(runners) {
38
- let result = { };
39
-
40
- runners.filter(runner => runner.splits.length > 0).forEach(runner => {
41
- runner.splits.forEach((split, idx) => {
42
- if (validTime(split.split)) {
43
- let from = idx === 0 ? 'St' : runner.splits[idx - 1].code;
44
- let code = from + '-' + split.code;
45
- addRunnerToLeg(result, code, runner, split.split);
46
- }
47
- });
48
- });
49
-
50
- return result;
51
- }
52
-
53
- function addRunnerToLeg(legs, code, runner, split) {
54
- let leg = legs[code];
55
- if (!leg) {
56
- leg = legs[code] = createLeg(code);
57
- }
58
-
59
- leg.runners.push({
60
- id: runner.id,
61
- fullName: runner.fullName,
62
- split: split
63
- });
64
- }
65
-
66
- function createLeg(code) {
67
- return {
68
- code: code,
69
- runners: []
70
- };
71
- }
72
-
73
- function defineRunners(runners) {
74
- return runners.map(function(runner) {
75
- return {
76
- id: runner.id,
77
- fullName: runner.fullName,
78
- time: runner.time,
79
- yearOfBirth: runner.yearOfBirth,
80
- city: runner.city,
81
- club: runner.club,
82
- splits: runner.splits.map(function(split, idx) {
83
- var splitTime = null;
84
- if (split.time === '-') {
85
- splitTime = '-';
86
- } else if (idx === 0) {
87
- splitTime = parseTime(split.time);
88
- } else {
89
- if (parseTime(split.time) === null || parseTime(runner.splits[idx - 1].time) === null) {
90
- splitTime = '-';
91
- } else {
92
- splitTime = parseTime(split.time) - parseTime(runner.splits[idx - 1].time);
93
- }
94
- }
95
-
96
- return {
97
- code: split.code,
98
- time: split.time,
99
- split: splitTime
100
- };
101
- })
102
- };
103
- });
104
- }
105
-
106
- /**
107
- * Define properties of each leg. After this method the leg structure will be enhanced as follows:
108
- *
109
- * - runners are sorted per split time per leg
110
- * - each leg has a property 'idealSplit' (ideal split time of this leg)
111
- * - each leg has a property 'fastestSplit'
112
- * - each runner entry of a leg is enhanced with 'splitBehind' and 'splitRank'
113
- *
114
- * @param {*} legs leg data structre (only split is relevant)
115
- */
116
- function defineLegProperties(legs) {
117
- Object.keys(legs).forEach(code => {
118
- let leg = legs[code];
119
- leg.runners.sort(function(r1, r2) {
120
- return r1.split - r2.split;
121
- });
122
-
123
- // calculate the ideal time: take up to 5 fastest on that leg
124
- let selected = leg.runners.slice(0, Math.min(leg.runners.length, 5)).map(runner => runner.split);
125
-
126
- // only if there are valid splits for this leg
127
- if (selected.length > 0) {
128
- leg.idealSplit = Math.round(selected.reduce(sum) / selected.length);
129
- }
130
-
131
- // only if there are valid splits for this leg
132
- if (leg.runners.length > 0) {
133
- leg.fastestSplit = leg.runners[0].split;
134
- leg.runners[0].splitBehind = '0:00';
135
- leg.runners.slice(1).forEach(runner => {
136
- runner.splitBehind = runner.split - leg.fastestSplit;
137
- });
138
-
139
- leg.runners[0].splitRank = 1;
140
- leg.runners.forEach((runner, idx, arr) => {
141
- if (idx > 0) {
142
- if (runner.split === arr[idx - 1].split) {
143
- runner.splitRank = arr[idx - 1].splitRank;
144
- } else {
145
- runner.splitRank = idx + 1;
146
- }
147
- runner.performanceIndex = Math.round(1.0 * leg.idealSplit / runner.split * 100);
148
- }
149
- });
150
- }
151
- });
152
- }
153
-
154
- /**
155
- * Calculates the weight of each leg regarding the passed in idealTime.
156
- *
157
- * @param {*} legs
158
- * @param {*} idealTime
159
- */
160
- function defineLegWeight(legs, idealTime) {
161
- Object.keys(legs).forEach((code) => {
162
- let leg = legs[code];
163
- leg.weight = leg.idealSplit / idealTime;
164
- });
165
- }
166
-
167
- function legCode(splits, idx) {
168
- if (idx === 0) {
169
- return 'St-' + splits[0].code;
170
- } else {
171
- return splits[idx - 1].code + '-' + splits[idx].code;
172
- }
173
- }
174
-
175
- /**
176
- * Assigns the following property to every valid leg of a runner.
177
- *
178
- * - splitBehind: how much time behind the faster runner
179
- * - splitRank: rank on corresponding leg
180
- * - leg: code of leg
181
- *
182
- * @param {*} runners
183
- * @param {*} legs
184
- */
185
- function assignLegInfoToSplits(runners, legs) {
186
- runners.forEach(runner => {
187
- runner.splits.filter(s => validTime(s.split)).forEach((split, idx) => {
188
- let code = legCode(runner.splits, idx);
189
- let leg = legs[code];
190
-
191
- if (!leg) {
192
- throw 'leg with code ' + code + ' not defined!';
193
- }
194
-
195
- split.leg = leg.code;
196
-
197
- let legRunner = leg.runners.find(r => r.id === runner.id);
198
-
199
- split.idealBehind = legRunner ? legRunner.split - leg.idealSplit : '-';
200
- split.supermanBehind = legRunner ? legRunner.split - leg.fastestSplit : '-';
201
- split.splitBehind = legRunner ? legRunner.splitBehind : '-';
202
- split.splitRank = legRunner ? legRunner.splitRank : null;
203
- split.performanceIndex = legRunner ? legRunner.performanceIndex : null;
204
- split.weight = leg.weight;
205
- });
206
- });
207
- }
208
-
209
- function rank(runners) {
210
- runners.forEach((runner, idx) => {
211
- if (idx === 0) {
212
- runner.rank = 1;
213
- } else {
214
- let prev = runners[idx - 1];
215
- if (prev.time === runner.time) {
216
- runner.rank = prev.rank;
217
- } else if (parseTime(runner.time)) {
218
- runner.rank = idx + 1;
219
- }
220
- }
221
- });
222
- }
223
-
224
- class RankingBuilder {
225
-
226
- constructor(runners) {
227
- runners.forEach((runner, idx) => {
228
- for (let i = 0; i < runner.splits.length; i++) {
229
- let split = runner.splits[i];
230
- if (i === 0 && validTime(split.time)) {
231
- split.split = parseTime(split.time);
232
- } else if (i > 0) {
233
- let previousSplit = runner.splits[i - 1];
234
- if (validTime(split.time) && validTime(previousSplit.time)) {
235
- split.split = parseTime(split.time) - parseTime(previousSplit.time);
236
- }
237
- }
238
- }
239
- });
240
-
241
- this.courses = defineCourses(runners);
242
- this.legs = defineLegs(runners);
243
- this.runners = defineRunners(runners);
244
-
245
- defineLegProperties(this.legs);
246
-
247
- // calculate the ideal time [s]
248
- this.idealTime = Object.keys(this.legs).map(code => this.legs[code].idealSplit).reduce(sum);
249
-
250
- // each leg's weight is calculated regarding as a ratio of the ideal split time to the ideal time
251
- defineLegWeight(this.legs, this.idealTime);
252
-
253
- // now assing the leg information (such as idealTime, weight, ...) to the individual splits of the runners
254
- assignLegInfoToSplits(this.runners, this.legs);
255
-
256
-
257
- this.runners.forEach(runner => {
258
- let behind = 0;
259
- let supermanBehind = 0;
260
- let weightSum = 0;
261
- runner.splits.forEach(split => {
262
- behind += split.idealBehind;
263
- supermanBehind += split.supermanBehind;
264
- split.overallIdealBehind = behind;
265
- split.overallSupermanBehind = supermanBehind;
266
- split.position = weightSum;
267
-
268
- weightSum += split.weight;
269
- });
270
- });
271
-
272
- // function of function calculating the time at an arbitrary position for a given runner
273
- let timeFn = (runner) => (pos) => {
274
- if (pos === 0) {
275
- return 0;
276
- } else if (pos >= 1) {
277
- return parseTime(runner.time);
278
- }
279
-
280
- let idx = 0;
281
- let weightSum = 0;
282
- for (idx = 0; idx < runner.splits.length; idx++) {
283
- let split = runner.splits[idx];
284
- if (weightSum + split.weight > pos) {
285
- break;
286
- }
287
- weightSum += split.weight;
288
- }
289
-
290
- let prev = runner.splits[idx - 1];
291
- let next = runner.splits[idx];
292
-
293
- let prevTime = parseTime(prev.time);
294
- let nextTime = parseTime(next.time);
295
- return prevTime + (pos - prev.position) / (next.position - prev.position) * (nextTime - prevTime);
296
- };
297
-
298
- let timesAtPosition = (pos) => {
299
- return this.runners.map(runner => { return { id: runner.id, time: timeFn(runner)(pos) }; });
300
- };
301
-
302
- this.runners.forEach(runner => {
303
- runner.splits.forEach(split => {
304
- let times = timesAtPosition(split.position + split.weight).filter(entry => entry.time !== null && entry.time > 0);
305
- times.sort((t1, t2) => t1.time - t2.time);
306
-
307
- let rank = 1;
308
- let fastestTime = times[0].time;
309
-
310
- let lastTime = parseTime(times[0].time);
311
- for (let idx = 0; idx < times.length; idx++) {
312
- let entry = times[idx];
313
-
314
- if (lastTime < parseTime(entry.time)) {
315
- rank = idx;
316
- }
317
-
318
- if (runner.id === entry.id) {
319
- split.overallRank = rank;
320
- split.overallBehind = formatTime(entry.time - fastestTime);
321
- break;
322
- }
323
- }
324
- });
325
- });
326
-
327
- console.log(this.runners[1]);
328
-
329
- // calculate the overall rank
330
- rank(this.runners);
331
- }
332
-
333
- getCourses() {
334
- return this.courses;
335
- }
336
-
337
- getLeg(code) {
338
- return this.legs[code];
339
- }
340
-
341
- getLegs() {
342
- return Object.keys(this.legs).map(code => this.legs[code]);
343
- }
344
-
345
- getRanking() {
346
- let runners = this.runners.map(runner => { return { ...runner } });
347
-
348
-
349
- /*
350
-
351
- for each runner we need the following function
352
-
353
- f(pos) => timeBehindIdeal
354
-
355
- using this function it will be possible to calculate the following properties
356
-
357
- f(runner, split) => {
358
- overallRank // the rank the current position
359
- idealBehind // time behind ideal
360
- }
361
-
362
- the first function
363
-
364
- */
365
- }
366
- }
367
-
368
- module.exports.RankingBuilder = RankingBuilder;
369
-
370
-
371
- /*
372
- /* depends on ordered courses!
373
- Object.keys(results.legs).forEach((code, idx) => {
374
- let leg = result.legs[code];
375
- if (idx === 0) {
376
- leg.fastestTime = formatTime(leg.fastestSplit);
377
- leg.idealTime = formatTime(leg.idealSplit);
378
- } else {
379
- leg.fastestTime = formatTime(parseTime(result.legs[idx - 1].fastestTime) + leg.fastestSplit);
380
- leg.idealTime = formatTime(parseTime(result.legs[idx - 1].idealTime) + leg.idealSplit);
381
- }
382
- });
383
-
384
- result.runners.forEach(function(runner) {
385
- // calculate overall time behind leader
386
- runner.splits.forEach(function(split, splitIdx) {
387
- if (!invalidTime(split.time)) {
388
- let leader = result.runners.map(function(r) {
389
- return {
390
- time: r.splits[splitIdx].time,
391
- rank: r.splits[splitIdx].overallRank
392
- };
393
- }).find(function(split) {
394
- return split.rank === 1;
395
- });
396
-
397
- // no leader for this leg?!
398
- if (leader) {
399
- let leaderTime = leader.time;
400
- if (parseTime(split.time) !== null) {
401
- split.overallBehind = formatTime(parseTime(split.time) - parseTime(leaderTime));
402
- split.fastestBehind = formatTime(parseTime(split.time) - parseTime(result.legs[splitIdx].fastestTime));
403
- split.idealBehind = formatTime(parseTime(split.time) - parseTime(result.legs[splitIdx].idealTime));
404
- } else {
405
- split.overallBehind = '-';
406
- split.fastestBehind = '-';
407
- split.idealBehind = '-';
408
- }
409
- }
410
- }
411
- });
412
-
413
- // performance index for split
414
- runner.splits.filter(split => split.split != '-' && split.split != 's' && split.split != '0:00').forEach(split => {
415
- let leg = findLeg(legs, split.leg);
416
- split.perfidx = Math.round(1.0 * leg.idealSplit / parseTime(split.split) * 100);
417
- });
418
- });
419
-
420
- return result;
421
- }
422
- */
package/lib/reorganize.js DELETED
@@ -1,212 +0,0 @@
1
- const { parseTime, formatTime } = require('./time');
2
- const assert = require('assert');
3
-
4
- module.exports.reorganize = function(runners, course) {
5
- return runners.filter(r => parseTime(r.time) !== null).map(r => {
6
- let runner = { ...r };
7
- let runnerCourse = 'St,' + runner.splits.map(s => s.code).join(',');
8
- console.log(runner.fullName, runnerCourse);
9
- if (runnerCourse === course.join(',')) {
10
- return runner;
11
- }
12
-
13
- let time = 0;
14
- let splits = [];
15
- let lastValidTime = 0;
16
- let lastValidIdx = -1;
17
-
18
- for (let idx = 0; idx < course.length - 1; idx++) {
19
- let from = course[idx];
20
- let to = course[idx + 1];
21
- let split = getSplit(runner, from, to);
22
- if (!split) {
23
- console.log('missing split for runner ' + runner.fullName + " at index " + idx, from, to);
24
- }
25
- console.log('split', from + '-' + to, formatTime(lastValidTime), split.diff);
26
-
27
- if (!split) {
28
- console.log('split not found for runner', from, to, runner.fullName);
29
- return;
30
- }
31
-
32
- if (split.split !== 's') {
33
- time += split.diff;
34
- lastValidTime = time;
35
- lastValidIdx = idx;
36
- splits.push({
37
- ...split,
38
- time: formatTime(time)
39
- });
40
- console.log('result', idx, split.code, 'time', formatTime(time));
41
- } else {
42
- splits.push({
43
- ...split,
44
- time: '-'
45
- });
46
- }
47
- }
48
- runner.splits = splits;
49
- return runner;
50
- });
51
- }
52
-
53
- function getSplit(runner, from, to) {
54
- for (let idx = 0; idx < runner.splits.length; idx++) {
55
- if (from === 'St') {
56
- // assumption: all runners have same start
57
- const split = runner.splits[0];
58
- return {
59
- ...split,
60
- diff: parseTime(split.time)
61
- };
62
- } else if (to === 'Zi') {
63
- throw "to Zi not supported";
64
- }
65
-
66
- for (let j = 1; j < runner.splits.length; j++) {
67
- if (from === runner.splits[j - 1].code && to === runner.splits[j].code) {
68
- const prevSplit = runner.splits[j - 1];
69
- const split = runner.splits[j];
70
- return {
71
- ...split,
72
- diff: parseTime(split.time) - parseTime(prevSplit.time)
73
- };
74
- }
75
- }
76
- }
77
-
78
- console.log('busted!', from, to, runner);
79
- return null;
80
- }
81
-
82
- function getLegTime(runner, from, to) {
83
- // from: [41,38]
84
- // to: [42]
85
- for (let idx = 0; idx < runner.splits.length; idx++) {
86
- if (from[0] === 'St') {
87
- // assumption: all runners have same start
88
- return parseTime(runner.splits[0].time);
89
- } else if (to[0] === 'Zi') {
90
- return parseTime(runner.totalTime) - parseTime(runner.splits[runner.splits.length - 1].time);
91
- }
92
-
93
- for (let j = 1; j < runner.splits.length; j++) {
94
- if (from.includes(runner.splits[j - 1].code) && to.includes(runner.splits[j].code)) {
95
- let currentTime = parseTime(runner.splits[j].time);
96
- let previousTime = parseTime(runner.splits[j - 1].time);
97
- if (currentTime === null) {
98
- return null;
99
- }
100
-
101
- // find last valid previous time or 0
102
- let k = j - 2;
103
- while (previousTime === null) {
104
- if (k === -1) {
105
- previousTime = 0;
106
- } else {
107
- previousTime = parseTime(runner.splits[k].time);
108
- k--;
109
- }
110
- }
111
-
112
- let legTime = currentTime - previousTime;
113
- if (legTime === 0) {
114
- console.log('*', runner.splits[j], currentTime, runner.splits[j - 1], previousTime);
115
- } else if (legTime < 0) {
116
- console.log('*', runner.splits[j], currentTime, runner.splits[j - 1], previousTime);
117
- }
118
- return legTime;
119
- }
120
- }
121
- }
122
-
123
- console.log('busted!', from, to);
124
- return 0;
125
- }
126
-
127
- /**
128
- * Reorganize a single runner to the given course. The course is specified as an array
129
- * of arrays. The elements in the inner array represent the controls that shall be
130
- * combined.
131
- *
132
- * @param {*} runner
133
- * @param {*} course
134
- */
135
- module.exports.reorganizeBbn = function(runner, course) {
136
- assert(runner.splits.length === course.length, 'number of splits must match', runner.splits.length, course.length);
137
-
138
- // course = [[41,38],[42],[43], ...]
139
- let time = 0;
140
- let splits = [];
141
- for (let idx = 0; idx < course.length; idx++) {
142
- let from = idx === 0 ? ['St'] : course[idx - 1];
143
- let to = course[idx];
144
- let legTime = getLegTime(runner, from, to);
145
- if (legTime !== null) {
146
- time += legTime;
147
- }
148
- splits.push({ code: to.join('|'), time: legTime === null ? '-' : formatTime(time) });
149
- }
150
-
151
- assert(runner.splits.length === splits.length, 'number of splits must not change', runner.splits.length, splits.length, course.length);
152
-
153
- runner.splits = splits;
154
- };
155
-
156
-
157
- module.exports.defineSubRanking = function(input, from, to) {
158
- let runners;
159
- if (typeof input.categories !== 'undefined') {
160
- runners = input.categories.flatMap(function(category) { return category.runners; })
161
- } else {
162
- runners = input.runners;
163
- }
164
-
165
- return runners.map(function(runner) {
166
- var fromTime, toTime;
167
- const course = [];
168
-
169
- if (from === 'St') {
170
- fromTime = 0;
171
- course.push(from);
172
- }
173
-
174
- if (to === 'Zi') {
175
- toTime = parseTime(runner.time);
176
- }
177
-
178
- for (var i = 0; i < runner.splits.length; i++) {
179
- var split = runner.splits[i];
180
- if (parseTime(split.time) === null) {
181
- continue;
182
- }
183
-
184
- if (split.code === from && typeof fromTime === 'undefined') {
185
- fromTime = parseTime(split.time);
186
- course.push(split.code);
187
-
188
- } else if (split.code === to) {
189
- toTime = parseTime(split.time);
190
- course.push(split.code);
191
- break;
192
-
193
- } else if (typeof fromTime !== 'undefined') {
194
- course.push(split.code);
195
- }
196
- }
197
-
198
- if (typeof fromTime !== 'undefined' && typeof toTime !== 'undefined' && toTime !== null) {
199
- const time = formatTime(toTime - fromTime);
200
- return {
201
- fullName: runner.fullName,
202
- time: time,
203
- course: course.join('-')
204
- };
205
- }
206
-
207
- return {
208
- fullName: runner.fullName
209
- };
210
- }).filter(runner => typeof runner.time !== 'undefined')
211
- .sort((e1, e2) => parseTime(e1.time) - parseTime(e2.time));
212
- }