@rasifix/orienteering-utils 2.0.1 → 2.0.3
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/lib/formats/index.d.ts +4 -0
- 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 +3 -0
- package/lib/index.js +7 -0
- package/lib/model/category.d.ts +8 -0
- package/lib/model/category.js +2 -0
- package/lib/model/competition.d.ts +8 -0
- package/lib/model/competition.js +2 -0
- package/lib/model/ranking.d.ts +27 -0
- package/lib/model/ranking.js +52 -0
- package/lib/model/runner.d.ts +21 -0
- package/lib/model/runner.js +8 -0
- package/lib/model/split.d.ts +4 -0
- package/lib/model/split.js +2 -0
- package/lib/time.d.ts +10 -0
- package/lib/time.js +52 -0
- package/lib/utils/anonymizer.d.ts +2 -0
- package/lib/utils/anonymizer.js +65 -0
- package/lib/utils/ranking.d.ts +71 -0
- package/lib/utils/ranking.js +383 -0
- package/package.json +3 -3
- package/lib/analyzis.js +0 -25
package/lib/format.d.ts
ADDED
package/lib/format.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.solv = exports.oware = exports.kraemer = void 0;
|
|
4
|
+
var kraemer = require("./kraemer");
|
|
5
|
+
exports.kraemer = kraemer;
|
|
6
|
+
var oware = require("./oware");
|
|
7
|
+
exports.oware = oware;
|
|
8
|
+
var solv = require("./solv");
|
|
9
|
+
exports.solv = solv;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Format } from "../format";
|
|
2
|
+
import { Category } from "../model/category";
|
|
3
|
+
import { Competition } from "../model/competition";
|
|
4
|
+
export declare class KraemerFormater implements Format {
|
|
5
|
+
parse(text: string, options?: any): {
|
|
6
|
+
name: any;
|
|
7
|
+
map: any;
|
|
8
|
+
date: any;
|
|
9
|
+
startTime: any;
|
|
10
|
+
categories: Category[];
|
|
11
|
+
};
|
|
12
|
+
check(text: string): boolean;
|
|
13
|
+
serialize(competition: Competition): string;
|
|
14
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.KraemerFormater = void 0;
|
|
4
|
+
var time_1 = require("../time");
|
|
5
|
+
function strip(str) {
|
|
6
|
+
if (!str) {
|
|
7
|
+
return "";
|
|
8
|
+
}
|
|
9
|
+
else if (str.length > 0 && str[0] === '"') {
|
|
10
|
+
return str.substring(1, str.length - 1);
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
return str;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
var KraemerFormater = /** @class */ (function () {
|
|
17
|
+
function KraemerFormater() {
|
|
18
|
+
}
|
|
19
|
+
// OE0014;Stnr;XStnr;Chipnr;Datenbank Id;Nachname;Vorname;Jg;G;Block; AK; Start;Ziel; Zeit; Wertung;Gutschrift -;Zuschlag +;Kommentar;Club-Nr.;Abk; Ort; Nat; Sitz;Region;Katnr;Kurz; Lang; MeldeKat. Nr;MeldeKat. (kurz);MeldeKat. (lang);Rang;Ranglistenpunkte;Num1;Num2; Num3; Text1; Text2; Text3; Adr. Nachname;Adr. Vorname;Straße;Zeile2;PLZ; Adr. Ort;Tel; Mobil; Fax; EMail; Gemietet;Startgeld;Bezahlt;Mannschaft;Bahnnummer;Bahn; km; Hm; Bahn Posten;Platz; Startstempel;Zielstempel;Posten1;Stempel1;Posten2;Stempel2;Posten3; Stempel3;Posten4;Stempel4;Posten5;Stempel5;Posten6;Stempel6;Posten7;Stempel7;Posten8;Stempel8;Posten9;Stempel9;Posten10;Stempel10;Posten11;Stempel11;Posten12;Stempel12;Posten13;Stempel13;Posten14;Stempel14;Posten15;Stempel15;Posten16;Stempel16;Posten17;Stempel17;Posten18;Stempel18;Posten19;Stempel19;Posten20;Stempel20;Posten21;Stempel21;Posten22;Stempel22;Posten23;Stempel23;Posten24;Stempel24;Posten25;Stempel25;Posten26;Stempel26;Posten27;Stempel27;Posten28;Stempel28;Posten29;Stempel29;Posten30;Stempel30;Posten31;Stempel31;Posten32;Stempel32;Posten33;Stempel33;Posten34;Stempel34;Posten35;Stempel35;Posten36;Stempel36;Posten37;Stempel37;Posten38;Stempel38;Posten39;Stempel39;Posten40;Stempel40;Posten41;Stempel41;Posten42;Stempel42;Posten43;Stempel43;Posten44;Stempel44;Posten45;Stempel45;Posten46;Stempel46;Posten47;Stempel47;Posten48;Stempel48;Posten49;Stempel49;Posten50;Stempel50;Posten51;Stempel51;Posten52;Stempel52;Posten53;Stempel53;Posten54;Stempel54;Posten55;Stempel55;Posten56;Stempel56;Posten57;Stempel57;Posten58;Stempel58;Posten59;Stempel59;Posten60;Stempel60;Posten61;Stempel61;Posten62;Stempel62;Posten63;Stempel63;Posten64;Stempel64;
|
|
20
|
+
// Stnr ;Chip;Datenbank Id;Nachname;Vorname;Jg;G;Block;AK;Start;Ziel;Zeit; Wertung;Club-Nr.;Abk; Ort; Nat; Katnr; Kurz; Lang;Num1;Num2;Num3;Text1; Text2;Text3;Adr. Name;Straße; Zeile2; PLZ; Ort; Tel; Fax; EMail;Id/Verein;Gemietet;Startgeld;Bezahlt;Bahnnummer; Bahn; km; Hm; Bahn Posten;Pl; Startstempel;Zielstempel;Posten1;Stempel1;Posten2; Stempel2; Posten3;Stempel3; Posten4; Stempel4;Posten5;Stempel5;Posten6; Stempel6;Posten7; Stempel7; Posten8;Stempel8;Posten9;Stempel9;Posten10;Stempel10;(und weitere)...
|
|
21
|
+
KraemerFormater.prototype.parse = function (text, options) {
|
|
22
|
+
if (options === void 0) { options = {}; }
|
|
23
|
+
// split text into lines
|
|
24
|
+
var lines = text.split(/\r?\n|\r|\n/g);
|
|
25
|
+
// extract header
|
|
26
|
+
var header = lines[0].split(";");
|
|
27
|
+
var firstTimeIdx;
|
|
28
|
+
// trying to get hold of correct column indices
|
|
29
|
+
var indices = {};
|
|
30
|
+
indices["Melde Id"] = header.indexOf("Melde Id");
|
|
31
|
+
indices["Nachname"] = header.indexOf("Nachname");
|
|
32
|
+
indices["Vorname"] = header.indexOf("Vorname");
|
|
33
|
+
indices["Jg"] = header.indexOf("Jg");
|
|
34
|
+
indices["G"] = header.indexOf("G");
|
|
35
|
+
indices["Datenbank Id"] = header.indexOf("Datenbank Id");
|
|
36
|
+
indices["Abk"] = header.indexOf("Abk");
|
|
37
|
+
indices["Club"] = header.indexOf("Ort");
|
|
38
|
+
indices["Ort"] = header.indexOf("Adr. Ort");
|
|
39
|
+
indices["Nat"] = header.indexOf("Nat");
|
|
40
|
+
indices["Start"] = header.indexOf("Start");
|
|
41
|
+
indices["Ziel"] = header.indexOf("Ziel");
|
|
42
|
+
indices["Zeit"] = header.indexOf("Zeit");
|
|
43
|
+
indices["Katnr"] = header.indexOf("Kurz");
|
|
44
|
+
indices["Rang"] = header.indexOf("Rang");
|
|
45
|
+
indices["Wertung"] = header.indexOf("Wertung");
|
|
46
|
+
indices["Posten"] = header.indexOf("Bahn Posten");
|
|
47
|
+
indices["km"] = header.indexOf("km");
|
|
48
|
+
indices["hm"] = header.indexOf("Hm");
|
|
49
|
+
firstTimeIdx = header.indexOf("Posten1");
|
|
50
|
+
if (header[0] === "OE0014") {
|
|
51
|
+
indices["Chip"] = header.indexOf("Chipnr");
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
indices["Chip"] = header.indexOf("Chip");
|
|
55
|
+
}
|
|
56
|
+
lines = lines.slice(1);
|
|
57
|
+
// the result object
|
|
58
|
+
var categories = {};
|
|
59
|
+
function objectify(cols) {
|
|
60
|
+
var result = {};
|
|
61
|
+
Object.keys(indices).forEach(function (key) {
|
|
62
|
+
result[key] = cols[indices[key]];
|
|
63
|
+
});
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
lines.forEach(function (line) {
|
|
67
|
+
var cols = line.split(";");
|
|
68
|
+
var lineObj = objectify(cols);
|
|
69
|
+
if (lineObj["Wertung"] !== "0") {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
var categoryName = strip(lineObj["Katnr"]);
|
|
73
|
+
var runnerName = strip(lineObj["Nachname"]);
|
|
74
|
+
var runnerFirstname = strip(lineObj["Vorname"]);
|
|
75
|
+
var runTime = (0, time_1.parseTime)(lineObj["Zeit"]);
|
|
76
|
+
var startTime = (0, time_1.parseTime)(lineObj["Start"]);
|
|
77
|
+
var runner = {
|
|
78
|
+
id: lineObj["Melde Id"],
|
|
79
|
+
category: categoryName,
|
|
80
|
+
firstName: runnerFirstname,
|
|
81
|
+
name: runnerName,
|
|
82
|
+
fullName: [runnerFirstname, runnerName].join(" "),
|
|
83
|
+
yearOfBirth: lineObj["Jg"],
|
|
84
|
+
club: (strip(lineObj["Abk"]) + " " + strip(lineObj["Club"])).trim(),
|
|
85
|
+
city: strip(lineObj["Ort"]),
|
|
86
|
+
nation: strip(lineObj["Nat"]),
|
|
87
|
+
time: (0, time_1.formatTime)(runTime) || "",
|
|
88
|
+
startTime: (0, time_1.formatTime)(startTime) || "",
|
|
89
|
+
splits: [],
|
|
90
|
+
};
|
|
91
|
+
var category = categories[categoryName];
|
|
92
|
+
if (typeof category === "undefined") {
|
|
93
|
+
category = {
|
|
94
|
+
name: strip(lineObj["Katnr"]),
|
|
95
|
+
distance: parseInt(strip(lineObj["km"]), 10) * 1000,
|
|
96
|
+
ascent: parseInt(strip(lineObj["hm"])),
|
|
97
|
+
controls: parseInt(strip(lineObj["Posten"])),
|
|
98
|
+
runners: [],
|
|
99
|
+
};
|
|
100
|
+
categories[category.name] = category;
|
|
101
|
+
}
|
|
102
|
+
var times = cols.slice(firstTimeIdx);
|
|
103
|
+
for (var idx = 0; idx < parseInt(lineObj["Posten"], 10) * 2; idx += 2) {
|
|
104
|
+
if (idx === times.length - 1) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
var parsedTime = (0, time_1.parseTime)(times[idx + 1]);
|
|
108
|
+
runner.splits.push({ code: times[idx], time: (0, time_1.formatTime)(parsedTime) });
|
|
109
|
+
}
|
|
110
|
+
category.runners.push(runner);
|
|
111
|
+
});
|
|
112
|
+
return {
|
|
113
|
+
name: options.event || "Anonymous Event",
|
|
114
|
+
map: options.map || "Unknown Map",
|
|
115
|
+
date: options.date || "",
|
|
116
|
+
startTime: options.startTime || "",
|
|
117
|
+
categories: Object.keys(categories).map(function (category) {
|
|
118
|
+
return categories[category];
|
|
119
|
+
}),
|
|
120
|
+
};
|
|
121
|
+
};
|
|
122
|
+
KraemerFormater.prototype.check = function (text) {
|
|
123
|
+
return (text.substring(0, 6) === "OE0014" ||
|
|
124
|
+
text.substring(0, 23) === "Stnr;Chip;Datenbank Id;");
|
|
125
|
+
};
|
|
126
|
+
KraemerFormater.prototype.serialize = function (competition) {
|
|
127
|
+
throw new Error("format does not implement serialization");
|
|
128
|
+
};
|
|
129
|
+
return KraemerFormater;
|
|
130
|
+
}());
|
|
131
|
+
exports.KraemerFormater = KraemerFormater;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OwareFormat = void 0;
|
|
4
|
+
function parseCategory(row) {
|
|
5
|
+
return {
|
|
6
|
+
name: row[0],
|
|
7
|
+
distance: parseInt(row[1], 10),
|
|
8
|
+
ascent: parseInt(row[2], 10),
|
|
9
|
+
controls: parseInt(row[3], 10),
|
|
10
|
+
runners: [],
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
function parseRunner(row, category, id) {
|
|
14
|
+
var headerLength = 15;
|
|
15
|
+
var i;
|
|
16
|
+
var splits = [];
|
|
17
|
+
for (i = headerLength; i < row.length; i += 2) {
|
|
18
|
+
splits.push({ code: row[i], time: row[i + 1] });
|
|
19
|
+
}
|
|
20
|
+
// split cleanup - detect two following splits with identical time
|
|
21
|
+
// --> control not working properly; set 's' as split time (substitute)
|
|
22
|
+
// going from back to front to catch several not working controls
|
|
23
|
+
for (i = splits.length - 1; i > 0; i--) {
|
|
24
|
+
if (splits[i].time === splits[i - 1].time && splits[i].time !== "-") {
|
|
25
|
+
splits[i].time = "s";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
id: id,
|
|
30
|
+
category: category,
|
|
31
|
+
rank: row[0] ? parseInt(row[0]) : undefined,
|
|
32
|
+
firstName: row[2],
|
|
33
|
+
name: row[1],
|
|
34
|
+
fullName: [row[2], row[1]].join(" "),
|
|
35
|
+
yearOfBirth: row[3],
|
|
36
|
+
club: row[8],
|
|
37
|
+
city: row[7],
|
|
38
|
+
nation: row[9],
|
|
39
|
+
time: row[12],
|
|
40
|
+
startTime: row[13],
|
|
41
|
+
splits: splits,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
var OwareFormat = /** @class */ (function () {
|
|
45
|
+
function OwareFormat() {
|
|
46
|
+
}
|
|
47
|
+
OwareFormat.prototype.parse = function (text) {
|
|
48
|
+
// split text into lines
|
|
49
|
+
var lines = text.trim().split(/[\r\n]+/);
|
|
50
|
+
// throw away first row containing headers
|
|
51
|
+
lines = lines.splice(1);
|
|
52
|
+
// second row contains information about the event
|
|
53
|
+
var header = lines[0].split(";");
|
|
54
|
+
var competition = {
|
|
55
|
+
// row starts with a double slash
|
|
56
|
+
name: header[0].substring(2, header[0].length),
|
|
57
|
+
map: header[1],
|
|
58
|
+
date: header[2],
|
|
59
|
+
startTime: header[3],
|
|
60
|
+
categories: [],
|
|
61
|
+
};
|
|
62
|
+
// throw a way the now parsed header
|
|
63
|
+
lines = lines.splice(1);
|
|
64
|
+
var category;
|
|
65
|
+
var idx = 0;
|
|
66
|
+
lines
|
|
67
|
+
.filter(function (line) { return line.trim().length > 0; })
|
|
68
|
+
.forEach(function (line) {
|
|
69
|
+
var cols = line.split(";");
|
|
70
|
+
if (cols.length === 4) {
|
|
71
|
+
category = parseCategory(cols);
|
|
72
|
+
competition.categories.push(category);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
category.runners.push(parseRunner(cols, category.name, ++idx));
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
return competition;
|
|
79
|
+
};
|
|
80
|
+
OwareFormat.prototype.serialize = function (event) {
|
|
81
|
+
var result = "//Format: Rank;Name;Firstname;YearOfBirth;SexMF;FedNr;Zip;Town;Club;NationIOF;StartNr;eCardNr;RunTime;StartTime;FinishTime;CtrlCode;SplitTime; ...\n";
|
|
82
|
+
result +=
|
|
83
|
+
"//" +
|
|
84
|
+
[event.name, event.map, event.date, event.startTime, ""].join(";") +
|
|
85
|
+
"\n";
|
|
86
|
+
event.categories.forEach(function (category) {
|
|
87
|
+
result +=
|
|
88
|
+
[
|
|
89
|
+
category.name,
|
|
90
|
+
category.distance,
|
|
91
|
+
category.ascent,
|
|
92
|
+
category.controls,
|
|
93
|
+
].join(";") + "\n";
|
|
94
|
+
category.runners.forEach(function (runner) {
|
|
95
|
+
result += [
|
|
96
|
+
runner.rank,
|
|
97
|
+
runner.fullName,
|
|
98
|
+
"",
|
|
99
|
+
runner.yearOfBirth,
|
|
100
|
+
"",
|
|
101
|
+
"",
|
|
102
|
+
"",
|
|
103
|
+
runner.city,
|
|
104
|
+
runner.club,
|
|
105
|
+
runner.nation,
|
|
106
|
+
"",
|
|
107
|
+
"", // ecard
|
|
108
|
+
runner.time,
|
|
109
|
+
runner.startTime,
|
|
110
|
+
"",
|
|
111
|
+
].join(";");
|
|
112
|
+
result +=
|
|
113
|
+
";" +
|
|
114
|
+
runner.splits
|
|
115
|
+
.map(function (split) {
|
|
116
|
+
return split.code + ";" + split.time;
|
|
117
|
+
})
|
|
118
|
+
.join(";") +
|
|
119
|
+
"\n";
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
return result;
|
|
123
|
+
};
|
|
124
|
+
OwareFormat.prototype.check = function (text) {
|
|
125
|
+
return text.substring(0, 8) === "//Format";
|
|
126
|
+
};
|
|
127
|
+
return OwareFormat;
|
|
128
|
+
}());
|
|
129
|
+
exports.OwareFormat = OwareFormat;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Format } from "../format";
|
|
2
|
+
import { Category } from "../model/category";
|
|
3
|
+
import { Competition } from "../model/competition";
|
|
4
|
+
export declare class SolvFormat implements Format {
|
|
5
|
+
parse(text: string, options?: any): {
|
|
6
|
+
name: any;
|
|
7
|
+
map: any;
|
|
8
|
+
date: any;
|
|
9
|
+
startTime: any;
|
|
10
|
+
categories: Category[];
|
|
11
|
+
};
|
|
12
|
+
serialize(competition: Competition): string;
|
|
13
|
+
check(text: string): boolean;
|
|
14
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SolvFormat = void 0;
|
|
4
|
+
var time_1 = require("../time");
|
|
5
|
+
function reformatTime(str) {
|
|
6
|
+
// "special" total times (like wrong or missing control)
|
|
7
|
+
if (str.indexOf(":") === -1) {
|
|
8
|
+
return str;
|
|
9
|
+
}
|
|
10
|
+
var parsed = (0, time_1.parseTime)(str);
|
|
11
|
+
return parsed ? (0, time_1.formatTime)(parsed) : undefined;
|
|
12
|
+
}
|
|
13
|
+
function reformatSplitTime(str) {
|
|
14
|
+
// normalize missing punch time
|
|
15
|
+
if (str === "-" || str === "-----") {
|
|
16
|
+
return "-";
|
|
17
|
+
}
|
|
18
|
+
// normalize not working control
|
|
19
|
+
if (str === "0.00") {
|
|
20
|
+
return "s";
|
|
21
|
+
}
|
|
22
|
+
var parsed = (0, time_1.parseTime)(str);
|
|
23
|
+
return parsed ? (0, time_1.formatTime)(parsed) : undefined;
|
|
24
|
+
}
|
|
25
|
+
// flat csv file format - every row contains full info including category
|
|
26
|
+
// Kategorie;Laenge;Steigung;PoAnz;Rang;Name;Jahrgang;Ort;Club;Zeit;Startzeit;Zielzeit;Zwischenzeiten
|
|
27
|
+
var SolvFormat = /** @class */ (function () {
|
|
28
|
+
function SolvFormat() {
|
|
29
|
+
}
|
|
30
|
+
SolvFormat.prototype.parse = function (text, options) {
|
|
31
|
+
if (options === void 0) { options = {}; }
|
|
32
|
+
var categories = {};
|
|
33
|
+
var lines = text.split("\n");
|
|
34
|
+
// drop header column
|
|
35
|
+
lines.splice(0, 1)[0].split(";");
|
|
36
|
+
lines.forEach(function (line, idx) {
|
|
37
|
+
var tokens = line.split(";");
|
|
38
|
+
if (tokens.length < 11) {
|
|
39
|
+
// invalid input? not enough data for runner
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
var categoryName = tokens[0];
|
|
43
|
+
var category = categories[categoryName];
|
|
44
|
+
if (!category) {
|
|
45
|
+
category = {
|
|
46
|
+
name: categoryName,
|
|
47
|
+
distance: Math.round(parseFloat(tokens[1]) * 1000),
|
|
48
|
+
ascent: parseInt(tokens[2]),
|
|
49
|
+
controls: parseInt(tokens[3]),
|
|
50
|
+
runners: [],
|
|
51
|
+
};
|
|
52
|
+
categories[categoryName] = category;
|
|
53
|
+
}
|
|
54
|
+
var name = tokens[5].split(" ");
|
|
55
|
+
var runner = {
|
|
56
|
+
id: idx,
|
|
57
|
+
category: categoryName,
|
|
58
|
+
rank: tokens[4] ? parseInt(tokens[4]) : undefined,
|
|
59
|
+
firstName: name[0],
|
|
60
|
+
name: name.slice(1).join(" "),
|
|
61
|
+
fullName: tokens[5],
|
|
62
|
+
yearOfBirth: tokens[6],
|
|
63
|
+
city: tokens[7],
|
|
64
|
+
club: tokens[8],
|
|
65
|
+
time: reformatTime(tokens[9]),
|
|
66
|
+
startTime: tokens[10],
|
|
67
|
+
splits: [],
|
|
68
|
+
};
|
|
69
|
+
if (tokens.length - 12 < category.controls * 2) {
|
|
70
|
+
// some crappy SOLV data...
|
|
71
|
+
console.log("fix crappy data from SOLV - not enough tokens on line for runner " +
|
|
72
|
+
runner.fullName);
|
|
73
|
+
for (var i = tokens.length; i < category.controls * 2 + 12; i++) {
|
|
74
|
+
if (i % 2 === 0) {
|
|
75
|
+
tokens[i] =
|
|
76
|
+
category.runners.length === 0
|
|
77
|
+
? "???"
|
|
78
|
+
: category.runners[0].splits[(i - 12) / 2].code;
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
tokens[i] = "-";
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
for (var i_1 = 12; i_1 < tokens.length - 1; i_1 += 2) {
|
|
86
|
+
var time = reformatSplitTime(tokens[i_1 + 1]);
|
|
87
|
+
if (runner.splits.length > 0 && time) {
|
|
88
|
+
var prev = runner.splits[runner.splits.length - 1].time;
|
|
89
|
+
var parsedTime = (0, time_1.parseTime)(tokens[i_1 + 1]);
|
|
90
|
+
if (time === prev ||
|
|
91
|
+
tokens[i_1 + 1] === "0.00" || (parsedTime && parsedTime > 180 * 60)) {
|
|
92
|
+
// normalize valid manual punches
|
|
93
|
+
time = "s";
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
runner.splits.push({ code: tokens[i_1], time: time });
|
|
97
|
+
}
|
|
98
|
+
category.runners.push(runner);
|
|
99
|
+
});
|
|
100
|
+
return {
|
|
101
|
+
name: options.event || "Anonymous Event",
|
|
102
|
+
map: options.map || "Unknown Map",
|
|
103
|
+
date: options.date || "",
|
|
104
|
+
startTime: options.startTime || "",
|
|
105
|
+
categories: Object.keys(categories).map(function (category) {
|
|
106
|
+
return categories[category];
|
|
107
|
+
}),
|
|
108
|
+
};
|
|
109
|
+
};
|
|
110
|
+
SolvFormat.prototype.serialize = function (competition) {
|
|
111
|
+
var result = "Kategorie;Laenge;Steigung;PoAnz;Rang;Name;Jahrgang;Ort;Club;Zeit;Startzeit;Zielzeit;Zwischenzeiten\n";
|
|
112
|
+
competition.categories.forEach(function (category) {
|
|
113
|
+
category.runners.forEach(function (runner) {
|
|
114
|
+
var distance = category.distance
|
|
115
|
+
? category.distance
|
|
116
|
+
: null;
|
|
117
|
+
var runTime = (0, time_1.parseTime)(runner.time);
|
|
118
|
+
var startTime = (0, time_1.parseTime)(runner.startTime);
|
|
119
|
+
var finishTime = runTime && startTime ? (0, time_1.formatTime)(runTime + startTime) : '';
|
|
120
|
+
result += [
|
|
121
|
+
category.name,
|
|
122
|
+
distance,
|
|
123
|
+
category.ascent,
|
|
124
|
+
category.controls,
|
|
125
|
+
runner.rank,
|
|
126
|
+
runner.fullName,
|
|
127
|
+
runner.yearOfBirth,
|
|
128
|
+
runner.city,
|
|
129
|
+
runner.club,
|
|
130
|
+
runner.time,
|
|
131
|
+
runner.startTime,
|
|
132
|
+
finishTime,
|
|
133
|
+
].join(";");
|
|
134
|
+
result +=
|
|
135
|
+
";" +
|
|
136
|
+
runner.splits
|
|
137
|
+
.map(function (split) {
|
|
138
|
+
return split.code + ";" + split.time;
|
|
139
|
+
})
|
|
140
|
+
.join(";") +
|
|
141
|
+
"\n";
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
return result;
|
|
145
|
+
};
|
|
146
|
+
SolvFormat.prototype.check = function (text) {
|
|
147
|
+
return text.indexOf("Kategorie;Laenge;Steigung;PoAnz;Rang;") === 0;
|
|
148
|
+
};
|
|
149
|
+
return SolvFormat;
|
|
150
|
+
}());
|
|
151
|
+
exports.SolvFormat = SolvFormat;
|
package/lib/index.d.ts
ADDED
package/lib/index.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface Ranking {
|
|
2
|
+
runners: {
|
|
3
|
+
[key: string]: RunnerInfo;
|
|
4
|
+
};
|
|
5
|
+
legs: {
|
|
6
|
+
[key: string]: Leg;
|
|
7
|
+
};
|
|
8
|
+
ranking: RankingEntry[];
|
|
9
|
+
}
|
|
10
|
+
export interface Leg {
|
|
11
|
+
from: string;
|
|
12
|
+
to: string;
|
|
13
|
+
ranking: RankingEntry[];
|
|
14
|
+
}
|
|
15
|
+
export interface RankingEntry {
|
|
16
|
+
rank: number;
|
|
17
|
+
time: string;
|
|
18
|
+
runnerRef: string;
|
|
19
|
+
}
|
|
20
|
+
export interface RunnerInfo {
|
|
21
|
+
fullName: string;
|
|
22
|
+
club: string;
|
|
23
|
+
city: string;
|
|
24
|
+
category: string;
|
|
25
|
+
course: string[];
|
|
26
|
+
startTime: string;
|
|
27
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
var sample = {
|
|
4
|
+
runners: {
|
|
5
|
+
"22": {
|
|
6
|
+
fullName: "Hans Dobeli",
|
|
7
|
+
club: "OLG Hopp Hopp",
|
|
8
|
+
city: "Witzwil",
|
|
9
|
+
category: "HB",
|
|
10
|
+
startTime: "01:33:00",
|
|
11
|
+
course: ["St", "31", "32", "33", "Zi"]
|
|
12
|
+
},
|
|
13
|
+
"23": {
|
|
14
|
+
fullName: "Fritz Berger",
|
|
15
|
+
club: "OLV Chapf",
|
|
16
|
+
city: "Langnau",
|
|
17
|
+
category: "HB",
|
|
18
|
+
startTime: "00:55:00",
|
|
19
|
+
course: ["St", "31", "32", "33", "Zi"]
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
legs: {
|
|
23
|
+
"St-31": {
|
|
24
|
+
from: "St",
|
|
25
|
+
to: "31",
|
|
26
|
+
ranking: [
|
|
27
|
+
{
|
|
28
|
+
rank: 1,
|
|
29
|
+
time: "02:33",
|
|
30
|
+
runnerRef: "22"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
rank: 2,
|
|
34
|
+
time: "08:02",
|
|
35
|
+
runnerRef: "23"
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
ranking: [
|
|
41
|
+
{
|
|
42
|
+
rank: 1,
|
|
43
|
+
runnerRef: "22",
|
|
44
|
+
time: "38:22"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
rank: 2,
|
|
48
|
+
runnerRef: "23",
|
|
49
|
+
time: "46:44"
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Split } from "./split";
|
|
2
|
+
export interface Runner {
|
|
3
|
+
id: number;
|
|
4
|
+
category: string;
|
|
5
|
+
rank?: number;
|
|
6
|
+
name: string;
|
|
7
|
+
firstName: string;
|
|
8
|
+
fullName: string;
|
|
9
|
+
yearOfBirth?: string;
|
|
10
|
+
sex?: Sex;
|
|
11
|
+
city?: string;
|
|
12
|
+
nation?: string;
|
|
13
|
+
club?: string;
|
|
14
|
+
time?: string;
|
|
15
|
+
startTime: string;
|
|
16
|
+
splits: Split[];
|
|
17
|
+
}
|
|
18
|
+
export declare enum Sex {
|
|
19
|
+
male = "m",
|
|
20
|
+
female = "f"
|
|
21
|
+
}
|
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;
|
package/lib/time.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseTime = parseTime;
|
|
4
|
+
exports.formatTime = formatTime;
|
|
5
|
+
function pad(value) {
|
|
6
|
+
return value < 10 ? '0' + value : value;
|
|
7
|
+
}
|
|
8
|
+
;
|
|
9
|
+
var regex = /(-)?[0-9]?[0-9]:[0-9][0-9](:[0-9][0-9])?/;
|
|
10
|
+
/**
|
|
11
|
+
* Parses a time string that must satisfy the following regex:
|
|
12
|
+
*
|
|
13
|
+
* /(-)?[0-9]?[0-9]:[0-9][0-9](:[0-9][0-9])?/
|
|
14
|
+
*
|
|
15
|
+
* @param str input string
|
|
16
|
+
* @returns the number of seconds in the given input string or null if input is not parseable
|
|
17
|
+
*/
|
|
18
|
+
function parseTime(str) {
|
|
19
|
+
if (!str) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
else if (typeof str !== 'string') {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
else if (!regex.test(str)) {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
var split = str.split(":");
|
|
29
|
+
var result = NaN;
|
|
30
|
+
if (split.length === 2) {
|
|
31
|
+
var negative = split[0][0] === '-';
|
|
32
|
+
var minutes = parseInt(split[0], 10);
|
|
33
|
+
result = (negative ? -1 : 1) * (Math.abs(minutes) * 60 + parseInt(split[1], 10));
|
|
34
|
+
}
|
|
35
|
+
else if (split.length === 3) {
|
|
36
|
+
result = parseInt(split[0], 10) * 3600 + parseInt(split[1], 10) * 60 + parseInt(split[2], 10);
|
|
37
|
+
}
|
|
38
|
+
return isNaN(result) ? undefined : result;
|
|
39
|
+
}
|
|
40
|
+
function formatTime(seconds) {
|
|
41
|
+
if (!seconds) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
var sign = seconds < 0 ? '-' : '';
|
|
45
|
+
var value = Math.abs(seconds);
|
|
46
|
+
if (value >= 3600) {
|
|
47
|
+
return sign + Math.floor(value / 3600) + ":" + pad(Math.floor(value / 60) % 60) + ":" + pad(value % 60);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
return Math.floor(value / 60) + ":" + pad(value % 60);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.anonymize = anonymize;
|
|
4
|
+
var runner_1 = require("../model/runner");
|
|
5
|
+
var names = [
|
|
6
|
+
'Gustavson', 'Hendrikson', 'Meyer', 'Marlovic', 'Torres', 'Huber',
|
|
7
|
+
'Wüst', 'Zürcher', 'Berner', 'Rufer', 'Gutmann', 'Hübscher',
|
|
8
|
+
'Schneller', 'Widemar', 'Rohrer', 'Kunz', 'Kinzle', 'Steinle',
|
|
9
|
+
'Allemann', 'Röhrig', 'Meyer', 'Uhlmann', 'Garaio', 'Regazoni',
|
|
10
|
+
'Maudet', 'Zoja', 'Scheller', 'Beckenbauer', 'Würsten'
|
|
11
|
+
];
|
|
12
|
+
var firstNames = {
|
|
13
|
+
f: [
|
|
14
|
+
'Anna', 'Alia', 'Ava', 'Berta', 'Benita', 'Carla', 'Cloe',
|
|
15
|
+
'Dina', 'Daria', 'Eva', 'Esther', 'Elin', 'Franca', 'Franziska',
|
|
16
|
+
'Gaby', 'Gerta', 'Gudrun', 'Hanna', 'Isabel', 'Ilda', 'Kim', 'Kathrin',
|
|
17
|
+
'Lia', 'Lisa', 'Lena', 'Liselotte', 'Lara', 'Mia', 'Marla', 'Nele', 'Olga',
|
|
18
|
+
'Pia', 'Rahel', 'Sara', 'Simona', 'Sina', 'Siri', 'Xenia', 'Zoé'
|
|
19
|
+
],
|
|
20
|
+
m: [
|
|
21
|
+
'Albert', 'Bruno', 'Chris', 'Dirk', 'David', 'Erwin', 'Francesco',
|
|
22
|
+
'Fritz', 'Gianni', 'Gustav', 'Hans', 'Henrik', 'Ian', 'Jan', 'Karl', 'Lars',
|
|
23
|
+
'Martin', 'Marco', 'Markus', 'Nico', 'Nino', 'Otto', 'Olav',
|
|
24
|
+
'Patric', 'Pablo', 'Quentin', 'Ralf', 'Rudolf', 'Simon', 'Steve',
|
|
25
|
+
'Thomas', 'Tim', 'Urs', 'Udo', 'Zan'
|
|
26
|
+
]
|
|
27
|
+
};
|
|
28
|
+
var cities = [
|
|
29
|
+
'Aarberg', 'Bern', 'Burgdorf', 'Colombier', 'Diemerswil', 'Domdidier', 'Elm', 'Flawil', 'Fribourg',
|
|
30
|
+
'Goldiwil', 'Heimiswil', 'Hergiswil', 'Illiswil', 'Konolfingen', 'Lausanne', 'Locarno',
|
|
31
|
+
'Lugano', 'Martigny', 'Neuchatel', 'Orbe', 'Uzwil', 'Zürich'
|
|
32
|
+
];
|
|
33
|
+
var clubs = {
|
|
34
|
+
prefixes: ['OLG', 'OLV', 'OL', 'CA'],
|
|
35
|
+
names: ['Bernstein', 'Erdmannlistein', 'Blanc', 'Piz Balü', 'Bartli und Most', 'Aare', 'Reuss']
|
|
36
|
+
};
|
|
37
|
+
function random(arr) {
|
|
38
|
+
var idx = Math.floor(Math.random() * (arr.length - 1));
|
|
39
|
+
return arr[idx];
|
|
40
|
+
}
|
|
41
|
+
function randInt(from, to) {
|
|
42
|
+
return from + Math.round(Math.random() * (to - from));
|
|
43
|
+
}
|
|
44
|
+
function anonymize(competition) {
|
|
45
|
+
competition.categories.forEach(function (category) {
|
|
46
|
+
category.runners.forEach(function (runner) {
|
|
47
|
+
runner.name = random(names);
|
|
48
|
+
if (!runner.sex || runner.sex === 'f') {
|
|
49
|
+
runner.firstName = random(firstNames.f);
|
|
50
|
+
runner.sex = runner_1.Sex.female;
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
runner.firstName = random(firstNames.m);
|
|
54
|
+
runner.sex = runner_1.Sex.male;
|
|
55
|
+
}
|
|
56
|
+
runner.fullName = runner.firstName + ' ' + runner.name;
|
|
57
|
+
runner.city = random(cities);
|
|
58
|
+
runner.club = random(clubs.prefixes) + ' ' + random(clubs.names);
|
|
59
|
+
runner.yearOfBirth = '' + randInt(1925, 2010);
|
|
60
|
+
runner.nation = 'SUI';
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
return competition;
|
|
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rasifix/orienteering-utils",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.3",
|
|
4
4
|
"description": "utility functions for orienteering result analyzis",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@types/chai": "^5.2.3",
|
|
30
30
|
"@types/mocha": "^10.0.10",
|
|
31
|
-
"@types/node": "^
|
|
31
|
+
"@types/node": "^22.10.5",
|
|
32
32
|
"ts-node": "^10.9.2",
|
|
33
|
-
"typescript": "^
|
|
33
|
+
"typescript": "^5.7.3"
|
|
34
34
|
}
|
|
35
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
|
-
}
|