@rasifix/orienteering-utils 1.0.6 → 2.0.0
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/package.json +10 -2
- package/src/format.ts +11 -0
- package/src/formats/index.ts +9 -0
- package/src/formats/kraemer.ts +145 -0
- package/src/formats/oware.ts +143 -0
- package/src/formats/solv.ts +169 -0
- package/src/index.ts +5 -0
- package/src/model/category.ts +9 -0
- package/src/model/competition.ts +9 -0
- package/src/model/ranking.ts +83 -0
- package/src/model/runner.ts +23 -0
- package/src/model/split.ts +4 -0
- package/{lib/time.js → src/time.ts} +11 -8
- package/{lib/anonymizer.js → src/utils/anonymizer.ts} +20 -21
- package/src/utils/ranking.ts +535 -0
- package/test/kraemer-test.ts +78 -0
- package/test/kraemer.csv +130 -0
- package/test/oware-test.ts +79 -0
- package/test/oware.csv +1 -1
- package/test/ranking-test.ts +55 -0
- package/test/solv-test.ts +65 -0
- package/test/test.js +6 -3
- package/tsconfig.json +11 -0
- package/index.d.ts +0 -4
- package/index.js +0 -11
- package/lib/butterfly.js +0 -59
- package/lib/kraemer.js +0 -125
- package/lib/oware.js +0 -105
- package/lib/ranking.js +0 -422
- package/lib/solv.js +0 -124
- package/test/oware-test.js +0 -82
- package/test/ranking-test.js +0 -102
- package/test/solv-test.js +0 -68
- /package/{lib/reorganize.js → src/utils/reorganize.ts} +0 -0
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rasifix/orienteering-utils",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "utility functions for orienteering result analyzis",
|
|
5
5
|
"main": "index",
|
|
6
6
|
"typings": "index",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"test": "mocha test/test.js"
|
|
8
|
+
"test": "mocha test/test.js",
|
|
9
|
+
"build": "tsc"
|
|
9
10
|
},
|
|
10
11
|
"keywords": [
|
|
11
12
|
"orienteering"
|
|
@@ -19,5 +20,12 @@
|
|
|
19
20
|
"dependencies": {
|
|
20
21
|
"chai": "^4.3.6",
|
|
21
22
|
"mocha": "^9.2.1"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/chai": "^5.2.3",
|
|
26
|
+
"@types/mocha": "^10.0.10",
|
|
27
|
+
"@types/node": "^16.0.2",
|
|
28
|
+
"ts-node": "^10.9.2",
|
|
29
|
+
"typescript": "^4.5.5"
|
|
22
30
|
}
|
|
23
31
|
}
|
package/src/format.ts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { Format } from "../format";
|
|
2
|
+
import { Category } from "../model/category";
|
|
3
|
+
import { Competition } from "../model/competition";
|
|
4
|
+
import { Runner } from "../model/runner";
|
|
5
|
+
import { parseTime, formatTime } from "../time";
|
|
6
|
+
|
|
7
|
+
function strip(str: string | undefined) {
|
|
8
|
+
if (!str) {
|
|
9
|
+
return "";
|
|
10
|
+
} else if (str.length > 0 && str[0] === '"') {
|
|
11
|
+
return str.substring(1, str.length - 1);
|
|
12
|
+
} else {
|
|
13
|
+
return str;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class KraemerFormater implements Format {
|
|
18
|
+
// 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;
|
|
19
|
+
// 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)...
|
|
20
|
+
parse(text: string, options: any = {}) {
|
|
21
|
+
// split text into lines
|
|
22
|
+
var lines = text.split(/\r?\n|\r|\n/g);
|
|
23
|
+
|
|
24
|
+
// extract header
|
|
25
|
+
var header = lines[0].split(";");
|
|
26
|
+
var firstTimeIdx: number;
|
|
27
|
+
|
|
28
|
+
// trying to get hold of correct column indices
|
|
29
|
+
var indices: { [key: string]: number } = {};
|
|
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
|
+
|
|
51
|
+
if (header[0] === "OE0014") {
|
|
52
|
+
indices["Chip"] = header.indexOf("Chipnr");
|
|
53
|
+
} else {
|
|
54
|
+
indices["Chip"] = header.indexOf("Chip");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
lines = lines.slice(1);
|
|
58
|
+
|
|
59
|
+
// the result object
|
|
60
|
+
var categories: { [key: string]: Category } = {};
|
|
61
|
+
|
|
62
|
+
function objectify(cols: string[]): any {
|
|
63
|
+
var result: any = {};
|
|
64
|
+
Object.keys(indices).forEach(function (key) {
|
|
65
|
+
result[key] = cols[indices[key]];
|
|
66
|
+
});
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
lines.forEach(function (line) {
|
|
71
|
+
var cols = line.split(";");
|
|
72
|
+
var lineObj = objectify(cols);
|
|
73
|
+
|
|
74
|
+
if (lineObj["Wertung"] !== "0") {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let categoryName = strip(lineObj["Katnr"]);
|
|
79
|
+
let runnerName = strip(lineObj["Nachname"]);
|
|
80
|
+
let runnerFirstname = strip(lineObj["Vorname"]);
|
|
81
|
+
|
|
82
|
+
let runTime = parseTime(lineObj["Zeit"]);
|
|
83
|
+
let startTime = parseTime(lineObj["Start"]);
|
|
84
|
+
|
|
85
|
+
var runner: Runner = {
|
|
86
|
+
id: lineObj["Melde Id"],
|
|
87
|
+
category: categoryName,
|
|
88
|
+
firstName: runnerFirstname,
|
|
89
|
+
name: runnerName,
|
|
90
|
+
fullName: [runnerFirstname, runnerName].join(" "),
|
|
91
|
+
yearOfBirth: lineObj["Jg"],
|
|
92
|
+
club: (strip(lineObj["Abk"]) + " " + strip(lineObj["Club"])).trim(),
|
|
93
|
+
city: strip(lineObj["Ort"]),
|
|
94
|
+
nation: strip(lineObj["Nat"]),
|
|
95
|
+
time: formatTime(runTime) || "",
|
|
96
|
+
startTime: formatTime(startTime) || "",
|
|
97
|
+
splits: [],
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
var category = categories[categoryName];
|
|
101
|
+
if (typeof category === "undefined") {
|
|
102
|
+
category = {
|
|
103
|
+
name: strip(lineObj["Katnr"]),
|
|
104
|
+
distance: parseInt(strip(lineObj["km"]), 10) * 1000,
|
|
105
|
+
ascent: parseInt(strip(lineObj["hm"])),
|
|
106
|
+
controls: parseInt(strip(lineObj["Posten"])),
|
|
107
|
+
runners: [],
|
|
108
|
+
};
|
|
109
|
+
categories[category.name] = category;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
var times = cols.slice(firstTimeIdx);
|
|
113
|
+
for (var idx = 0; idx < parseInt(lineObj["Posten"], 10) * 2; idx += 2) {
|
|
114
|
+
if (idx === times.length - 1) {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const parsedTime = parseTime(times[idx + 1]);
|
|
118
|
+
runner.splits.push({ code: times[idx], time: formatTime(parsedTime) });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
category.runners.push(runner);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
name: options.event || "Anonymous Event",
|
|
126
|
+
map: options.map || "Unknown Map",
|
|
127
|
+
date: options.date || "",
|
|
128
|
+
startTime: options.startTime || "",
|
|
129
|
+
categories: Object.keys(categories).map(function (category) {
|
|
130
|
+
return categories[category];
|
|
131
|
+
}),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
check(text: string) {
|
|
136
|
+
return (
|
|
137
|
+
text.substring(0, 6) === "OE0014" ||
|
|
138
|
+
text.substring(0, 23) === "Stnr;Chip;Datenbank Id;"
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
serialize(competition: Competition): string {
|
|
143
|
+
throw new Error("format does not implement serialization");
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { Format } from "../format";
|
|
2
|
+
import { Category } from "../model/category";
|
|
3
|
+
import { Competition } from "../model/competition";
|
|
4
|
+
import { Runner } from "../model/runner";
|
|
5
|
+
|
|
6
|
+
function parseCategory(row: string[]) {
|
|
7
|
+
return {
|
|
8
|
+
name: row[0],
|
|
9
|
+
distance: parseInt(row[1], 10),
|
|
10
|
+
ascent: parseInt(row[2], 10),
|
|
11
|
+
controls: parseInt(row[3], 10),
|
|
12
|
+
runners: [],
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function parseRunner(row: string[], category: string, id: number):Runner {
|
|
17
|
+
var headerLength = 15;
|
|
18
|
+
var i;
|
|
19
|
+
|
|
20
|
+
var splits = [];
|
|
21
|
+
for (i = headerLength; i < row.length; i += 2) {
|
|
22
|
+
splits.push({ code: row[i], time: row[i + 1] });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// split cleanup - detect two following splits with identical time
|
|
26
|
+
// --> control not working properly; set 's' as split time (substitute)
|
|
27
|
+
// going from back to front to catch several not working controls
|
|
28
|
+
for (i = splits.length - 1; i > 0; i--) {
|
|
29
|
+
if (splits[i].time === splits[i - 1].time && splits[i].time !== "-") {
|
|
30
|
+
splits[i].time = "s";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
id: id,
|
|
36
|
+
category: category,
|
|
37
|
+
rank: row[0] ? parseInt(row[0]) : undefined,
|
|
38
|
+
firstName: row[2],
|
|
39
|
+
name: row[1],
|
|
40
|
+
fullName: [row[2], row[1]].join(" "),
|
|
41
|
+
yearOfBirth: row[3],
|
|
42
|
+
club: row[8],
|
|
43
|
+
city: row[7],
|
|
44
|
+
nation: row[9],
|
|
45
|
+
time: row[12],
|
|
46
|
+
startTime: row[13],
|
|
47
|
+
splits: splits,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export class OwareFormat implements Format {
|
|
52
|
+
parse(text: string):Competition {
|
|
53
|
+
// split text into lines
|
|
54
|
+
var lines = text.trim().split(/[\r\n]+/);
|
|
55
|
+
|
|
56
|
+
// throw away first row containing headers
|
|
57
|
+
lines = lines.splice(1);
|
|
58
|
+
|
|
59
|
+
// second row contains information about the event
|
|
60
|
+
var header = lines[0].split(";");
|
|
61
|
+
|
|
62
|
+
var competition: Competition = {
|
|
63
|
+
// row starts with a double slash
|
|
64
|
+
name: header[0].substring(2, header[0].length),
|
|
65
|
+
map: header[1],
|
|
66
|
+
date: header[2],
|
|
67
|
+
startTime: header[3],
|
|
68
|
+
categories: [],
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// throw a way the now parsed header
|
|
72
|
+
lines = lines.splice(1);
|
|
73
|
+
|
|
74
|
+
var category: Category;
|
|
75
|
+
|
|
76
|
+
let idx = 0;
|
|
77
|
+
lines
|
|
78
|
+
.filter((line) => line.trim().length > 0)
|
|
79
|
+
.forEach(function (line) {
|
|
80
|
+
var cols = line.split(";");
|
|
81
|
+
if (cols.length === 4) {
|
|
82
|
+
category = parseCategory(cols);
|
|
83
|
+
competition.categories.push(category);
|
|
84
|
+
} else {
|
|
85
|
+
category.runners.push(parseRunner(cols, category.name, ++idx));
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return competition;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
serialize(event: Competition) {
|
|
93
|
+
var result =
|
|
94
|
+
"//Format: Rank;Name;Firstname;YearOfBirth;SexMF;FedNr;Zip;Town;Club;NationIOF;StartNr;eCardNr;RunTime;StartTime;FinishTime;CtrlCode;SplitTime; ...\n";
|
|
95
|
+
result +=
|
|
96
|
+
"//" +
|
|
97
|
+
[event.name, event.map, event.date, event.startTime, ""].join(";") +
|
|
98
|
+
"\n";
|
|
99
|
+
|
|
100
|
+
event.categories.forEach(function (category) {
|
|
101
|
+
result +=
|
|
102
|
+
[
|
|
103
|
+
category.name,
|
|
104
|
+
category.distance,
|
|
105
|
+
category.ascent,
|
|
106
|
+
category.controls,
|
|
107
|
+
].join(";") + "\n";
|
|
108
|
+
category.runners.forEach(function (runner) {
|
|
109
|
+
result += [
|
|
110
|
+
runner.rank,
|
|
111
|
+
runner.fullName,
|
|
112
|
+
"",
|
|
113
|
+
runner.yearOfBirth,
|
|
114
|
+
"",
|
|
115
|
+
"",
|
|
116
|
+
"",
|
|
117
|
+
runner.city,
|
|
118
|
+
runner.club,
|
|
119
|
+
runner.nation,
|
|
120
|
+
"",
|
|
121
|
+
"", // ecard
|
|
122
|
+
runner.time,
|
|
123
|
+
runner.startTime,
|
|
124
|
+
"",
|
|
125
|
+
].join(";");
|
|
126
|
+
result +=
|
|
127
|
+
";" +
|
|
128
|
+
runner.splits
|
|
129
|
+
.map(function (split) {
|
|
130
|
+
return split.code + ";" + split.time;
|
|
131
|
+
})
|
|
132
|
+
.join(";") +
|
|
133
|
+
"\n";
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
check(text: string) {
|
|
141
|
+
return text.substring(0, 8) === "//Format";
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { Format } from "../format";
|
|
2
|
+
import { Category } from "../model/category";
|
|
3
|
+
import { Competition } from "../model/competition";
|
|
4
|
+
import { Runner } from "../model/runner";
|
|
5
|
+
import { formatTime, parseTime } from "../time";
|
|
6
|
+
|
|
7
|
+
function reformatTime(str: string): string | undefined {
|
|
8
|
+
// "special" total times (like wrong or missing control)
|
|
9
|
+
if (str.indexOf(":") === -1) {
|
|
10
|
+
return str;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const parsed = parseTime(str);
|
|
14
|
+
return parsed ? formatTime(parsed) : undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function reformatSplitTime(str: string): string | undefined {
|
|
18
|
+
// normalize missing punch time
|
|
19
|
+
if (str === "-" || str === "-----") {
|
|
20
|
+
return "-";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// normalize not working control
|
|
24
|
+
if (str === "0.00") {
|
|
25
|
+
return "s";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const parsed = parseTime(str);
|
|
29
|
+
return parsed ? formatTime(parsed) : undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// flat csv file format - every row contains full info including category
|
|
33
|
+
// Kategorie;Laenge;Steigung;PoAnz;Rang;Name;Jahrgang;Ort;Club;Zeit;Startzeit;Zielzeit;Zwischenzeiten
|
|
34
|
+
export class SolvFormat implements Format {
|
|
35
|
+
parse(text: string, options: any = {}) {
|
|
36
|
+
const categories: { [key: string]: Category } = {};
|
|
37
|
+
|
|
38
|
+
const lines = text.split("\n");
|
|
39
|
+
|
|
40
|
+
// drop header column
|
|
41
|
+
lines.splice(0, 1)[0].split(";");
|
|
42
|
+
|
|
43
|
+
lines.forEach(function (line, idx) {
|
|
44
|
+
const tokens = line.split(";");
|
|
45
|
+
if (tokens.length < 11) {
|
|
46
|
+
// invalid input? not enough data for runner
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const categoryName = tokens[0];
|
|
51
|
+
let category = categories[categoryName];
|
|
52
|
+
if (!category) {
|
|
53
|
+
category = {
|
|
54
|
+
name: categoryName,
|
|
55
|
+
distance: Math.round(parseFloat(tokens[1]) * 1000),
|
|
56
|
+
ascent: parseInt(tokens[2]),
|
|
57
|
+
controls: parseInt(tokens[3]),
|
|
58
|
+
runners: [],
|
|
59
|
+
};
|
|
60
|
+
categories[categoryName] = category;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const name = tokens[5].split(" ");
|
|
64
|
+
const runner:Runner = {
|
|
65
|
+
id: idx,
|
|
66
|
+
category: categoryName,
|
|
67
|
+
rank: tokens[4] ? parseInt(tokens[4]) : undefined,
|
|
68
|
+
firstName: name[0],
|
|
69
|
+
name: name.slice(1).join(" "),
|
|
70
|
+
fullName: tokens[5],
|
|
71
|
+
yearOfBirth: tokens[6],
|
|
72
|
+
city: tokens[7],
|
|
73
|
+
club: tokens[8],
|
|
74
|
+
time: reformatTime(tokens[9]),
|
|
75
|
+
startTime: tokens[10],
|
|
76
|
+
splits: [],
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
if (tokens.length - 12 < category.controls * 2) {
|
|
80
|
+
// some crappy SOLV data...
|
|
81
|
+
console.log(
|
|
82
|
+
"fix crappy data from SOLV - not enough tokens on line for runner " +
|
|
83
|
+
runner.fullName
|
|
84
|
+
);
|
|
85
|
+
for (var i = tokens.length; i < category.controls * 2 + 12; i++) {
|
|
86
|
+
if (i % 2 === 0) {
|
|
87
|
+
tokens[i] =
|
|
88
|
+
category.runners.length === 0
|
|
89
|
+
? "???"
|
|
90
|
+
: category.runners[0].splits[(i - 12) / 2].code;
|
|
91
|
+
} else {
|
|
92
|
+
tokens[i] = "-";
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
for (let i = 12; i < tokens.length - 1; i += 2) {
|
|
98
|
+
let time = reformatSplitTime(tokens[i + 1]);
|
|
99
|
+
if (runner.splits.length > 0 && time) {
|
|
100
|
+
let prev = runner.splits[runner.splits.length - 1].time;
|
|
101
|
+
let parsedTime = parseTime(tokens[i + 1]);
|
|
102
|
+
if (
|
|
103
|
+
time === prev ||
|
|
104
|
+
tokens[i + 1] === "0.00" || (parsedTime && parsedTime > 180 * 60)) {
|
|
105
|
+
// normalize valid manual punches
|
|
106
|
+
time = "s";
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
runner.splits.push({ code: tokens[i], time: time });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
category.runners.push(runner);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
name: options.event || "Anonymous Event",
|
|
117
|
+
map: options.map || "Unknown Map",
|
|
118
|
+
date: options.date || "",
|
|
119
|
+
startTime: options.startTime || "",
|
|
120
|
+
categories: Object.keys(categories).map(function (category) {
|
|
121
|
+
return categories[category];
|
|
122
|
+
}),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
serialize(competition: Competition) {
|
|
127
|
+
var result =
|
|
128
|
+
"Kategorie;Laenge;Steigung;PoAnz;Rang;Name;Jahrgang;Ort;Club;Zeit;Startzeit;Zielzeit;Zwischenzeiten\n";
|
|
129
|
+
|
|
130
|
+
competition.categories.forEach(function (category) {
|
|
131
|
+
category.runners.forEach(function (runner) {
|
|
132
|
+
const distance = category.distance
|
|
133
|
+
? category.distance
|
|
134
|
+
: null;
|
|
135
|
+
const runTime = parseTime(runner.time);
|
|
136
|
+
const startTime = parseTime(runner.startTime);
|
|
137
|
+
const finishTime = runTime && startTime ? formatTime(runTime + startTime) : '';
|
|
138
|
+
result += [
|
|
139
|
+
category.name,
|
|
140
|
+
distance,
|
|
141
|
+
category.ascent,
|
|
142
|
+
category.controls,
|
|
143
|
+
runner.rank,
|
|
144
|
+
runner.fullName,
|
|
145
|
+
runner.yearOfBirth,
|
|
146
|
+
runner.city,
|
|
147
|
+
runner.club,
|
|
148
|
+
runner.time,
|
|
149
|
+
runner.startTime,
|
|
150
|
+
finishTime,
|
|
151
|
+
].join(";");
|
|
152
|
+
result +=
|
|
153
|
+
";" +
|
|
154
|
+
runner.splits
|
|
155
|
+
.map(function (split) {
|
|
156
|
+
return split.code + ";" + split.time;
|
|
157
|
+
})
|
|
158
|
+
.join(";") +
|
|
159
|
+
"\n";
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
check(text: string) {
|
|
167
|
+
return text.indexOf("Kategorie;Laenge;Steigung;PoAnz;Rang;") === 0;
|
|
168
|
+
}
|
|
169
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export interface Ranking {
|
|
2
|
+
/* indexed information about runners */
|
|
3
|
+
runners: { [key:string]: RunnerInfo },
|
|
4
|
+
|
|
5
|
+
/* extracted information about all legs found */
|
|
6
|
+
legs: { [key:string]: Leg },
|
|
7
|
+
|
|
8
|
+
/* this is the overall ranking of the given runners */
|
|
9
|
+
ranking: RankingEntry[]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface Leg {
|
|
13
|
+
from: string;
|
|
14
|
+
to: string;
|
|
15
|
+
ranking: RankingEntry[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface RankingEntry {
|
|
19
|
+
rank: number;
|
|
20
|
+
time: string;
|
|
21
|
+
runnerRef: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface RunnerInfo {
|
|
25
|
+
fullName: string;
|
|
26
|
+
club: string;
|
|
27
|
+
city: string;
|
|
28
|
+
category: string;
|
|
29
|
+
course: string[];
|
|
30
|
+
startTime: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
const sample:Ranking = {
|
|
35
|
+
runners: {
|
|
36
|
+
"22": {
|
|
37
|
+
fullName: "Hans Dobeli",
|
|
38
|
+
club: "OLG Hopp Hopp",
|
|
39
|
+
city: "Witzwil",
|
|
40
|
+
category: "HB",
|
|
41
|
+
startTime: "01:33:00",
|
|
42
|
+
course: [ "St", "31", "32", "33", "Zi" ]
|
|
43
|
+
},
|
|
44
|
+
"23": {
|
|
45
|
+
fullName: "Fritz Berger",
|
|
46
|
+
club: "OLV Chapf",
|
|
47
|
+
city: "Langnau",
|
|
48
|
+
category: "HB",
|
|
49
|
+
startTime: "00:55:00",
|
|
50
|
+
course: [ "St", "31", "32", "33", "Zi" ]
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
legs: {
|
|
54
|
+
"St-31": {
|
|
55
|
+
from: "St",
|
|
56
|
+
to: "31",
|
|
57
|
+
ranking: [
|
|
58
|
+
{
|
|
59
|
+
rank: 1,
|
|
60
|
+
time: "02:33",
|
|
61
|
+
runnerRef: "22"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
rank: 2,
|
|
65
|
+
time: "08:02",
|
|
66
|
+
runnerRef: "23"
|
|
67
|
+
}
|
|
68
|
+
]
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
ranking: [
|
|
72
|
+
{
|
|
73
|
+
rank: 1,
|
|
74
|
+
runnerRef: "22",
|
|
75
|
+
time: "38:22"
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
rank: 2,
|
|
79
|
+
runnerRef: "23",
|
|
80
|
+
time: "46:44"
|
|
81
|
+
}
|
|
82
|
+
]
|
|
83
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Split } from "./split";
|
|
2
|
+
|
|
3
|
+
export interface Runner {
|
|
4
|
+
id: number;
|
|
5
|
+
category: string;
|
|
6
|
+
rank?: number;
|
|
7
|
+
name: string;
|
|
8
|
+
firstName: string;
|
|
9
|
+
fullName: string;
|
|
10
|
+
yearOfBirth?: string;
|
|
11
|
+
sex?: Sex;
|
|
12
|
+
city?: string;
|
|
13
|
+
nation?: string;
|
|
14
|
+
club?: string;
|
|
15
|
+
time?: string;
|
|
16
|
+
startTime: string;
|
|
17
|
+
splits: Split[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export enum Sex {
|
|
21
|
+
male = 'm',
|
|
22
|
+
female = 'f'
|
|
23
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
function pad(value) {
|
|
1
|
+
function pad(value:number) {
|
|
2
2
|
return value < 10 ? '0' + value : value;
|
|
3
3
|
};
|
|
4
4
|
|
|
@@ -12,17 +12,17 @@ var regex = /(-)?[0-9]?[0-9]:[0-9][0-9](:[0-9][0-9])?/;
|
|
|
12
12
|
* @param str input string
|
|
13
13
|
* @returns the number of seconds in the given input string or null if input is not parseable
|
|
14
14
|
*/
|
|
15
|
-
|
|
15
|
+
export function parseTime(str:string|undefined):number|undefined {
|
|
16
16
|
if (!str) {
|
|
17
|
-
return
|
|
17
|
+
return undefined;
|
|
18
18
|
} else if (typeof str !== 'string') {
|
|
19
|
-
return
|
|
19
|
+
return undefined;
|
|
20
20
|
} else if (!regex.test(str)) {
|
|
21
|
-
return
|
|
21
|
+
return undefined;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
var split = str.split(":");
|
|
25
|
-
var result =
|
|
25
|
+
var result:number = NaN;
|
|
26
26
|
if (split.length === 2) {
|
|
27
27
|
var negative = split[0][0] === '-';
|
|
28
28
|
var minutes = parseInt(split[0], 10);
|
|
@@ -31,10 +31,13 @@ module.exports.parseTime = function(str) {
|
|
|
31
31
|
result = parseInt(split[0], 10) * 3600 + parseInt(split[1], 10) * 60 + parseInt(split[2], 10);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
return isNaN(result) ?
|
|
34
|
+
return isNaN(result) ? undefined : result;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
export function formatTime(seconds:number|undefined) {
|
|
38
|
+
if (!seconds) {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
38
41
|
const sign = seconds < 0 ? '-' : '';
|
|
39
42
|
const value = Math.abs(seconds);
|
|
40
43
|
if (value >= 3600) {
|