@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/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
|
-
|
|
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
|
-
|
|
18
|
+
function parseTime(str) {
|
|
16
19
|
if (!str) {
|
|
17
20
|
return undefined;
|
|
18
|
-
}
|
|
21
|
+
}
|
|
22
|
+
else if (typeof str !== 'string') {
|
|
19
23
|
return undefined;
|
|
20
|
-
}
|
|
24
|
+
}
|
|
25
|
+
else if (!regex.test(str)) {
|
|
21
26
|
return undefined;
|
|
22
27
|
}
|
|
23
|
-
|
|
24
28
|
var split = str.split(":");
|
|
25
|
-
var result
|
|
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
|
-
}
|
|
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
|
-
|
|
42
|
-
|
|
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
|
-
}
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
46
50
|
return Math.floor(value / 60) + ":" + pad(value % 60);
|
|
47
51
|
}
|
|
48
52
|
}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
|
|
50
|
-
|
|
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
|
-
}
|
|
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.
|
|
3
|
+
"version": "2.0.2",
|
|
4
4
|
"description": "utility functions for orienteering result analyzis",
|
|
5
|
-
"main": "index",
|
|
6
|
-
"
|
|
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": "^
|
|
31
|
+
"@types/node": "^22.10.5",
|
|
28
32
|
"ts-node": "^10.9.2",
|
|
29
|
-
"typescript": "^
|
|
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
|
-
}
|