@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.
@@ -0,0 +1,6 @@
1
+ import { Competition } from "./model/competition";
2
+ export interface Format {
3
+ check(content: string): boolean;
4
+ parse(content: string, options: any): Competition;
5
+ serialize(competition: Competition): string;
6
+ }
package/lib/format.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,4 @@
1
+ import * as kraemer from './kraemer';
2
+ import * as oware from './oware';
3
+ import * as solv from './solv';
4
+ export { kraemer, oware, solv };
@@ -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,7 @@
1
+ import { Format } from "../format";
2
+ import { Competition } from "../model/competition";
3
+ export declare class OwareFormat implements Format {
4
+ parse(text: string): Competition;
5
+ serialize(event: Competition): string;
6
+ check(text: string): boolean;
7
+ }
@@ -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
@@ -0,0 +1,3 @@
1
+ import * as formats from './formats';
2
+ import * as ranking from './utils/ranking';
3
+ export { formats, ranking };
package/lib/index.js ADDED
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ranking = exports.formats = void 0;
4
+ var formats = require("./formats");
5
+ exports.formats = formats;
6
+ var ranking = require("./utils/ranking");
7
+ exports.ranking = ranking;
@@ -0,0 +1,8 @@
1
+ import { Runner } from "./runner";
2
+ export interface Category {
3
+ name: string;
4
+ distance?: number;
5
+ ascent?: number;
6
+ controls: number;
7
+ runners: Runner[];
8
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,8 @@
1
+ import { Category } from "./category";
2
+ export interface Competition {
3
+ name: string;
4
+ map?: string;
5
+ date: string;
6
+ startTime: string;
7
+ categories: Category[];
8
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -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
+ }
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Sex = void 0;
4
+ var Sex;
5
+ (function (Sex) {
6
+ Sex["male"] = "m";
7
+ Sex["female"] = "f";
8
+ })(Sex || (exports.Sex = Sex = {}));
@@ -0,0 +1,4 @@
1
+ export interface Split {
2
+ code: string;
3
+ time: string | undefined;
4
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
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,2 @@
1
+ import { Competition } from "../model/competition";
2
+ export declare function anonymize(competition: Competition): Competition;
@@ -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.1",
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": "^16.0.2",
31
+ "@types/node": "^22.10.5",
32
32
  "ts-node": "^10.9.2",
33
- "typescript": "^4.5.5"
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
- }