diff options
author | Rutger Broekhoff | 2024-05-02 20:27:40 +0200 |
---|---|---|
committer | Rutger Broekhoff | 2024-05-02 20:27:40 +0200 |
commit | 17a3ea880402338420699e03bcb24181e4ff3924 (patch) | |
tree | da666ef91e0b60d20aa0b01529644c136fd1f4ab /lib/libtmi8/src/kv1_parser.cpp | |
download | oeuf-17a3ea880402338420699e03bcb24181e4ff3924.tar.gz oeuf-17a3ea880402338420699e03bcb24181e4ff3924.zip |
Initial commit
Based on dc4ba6a
Diffstat (limited to 'lib/libtmi8/src/kv1_parser.cpp')
-rw-r--r-- | lib/libtmi8/src/kv1_parser.cpp | 1258 |
1 files changed, 1258 insertions, 0 deletions
diff --git a/lib/libtmi8/src/kv1_parser.cpp b/lib/libtmi8/src/kv1_parser.cpp new file mode 100644 index 0000000..ac0c6bf --- /dev/null +++ b/lib/libtmi8/src/kv1_parser.cpp | |||
@@ -0,0 +1,1258 @@ | |||
1 | // vim:set sw=2 ts=2 sts et: | ||
2 | |||
3 | #include <tmi8/kv1_parser.hpp> | ||
4 | |||
5 | using rune = uint32_t; | ||
6 | |||
7 | static size_t decodeUtf8Cp(std::string_view s, rune *dest = nullptr) { | ||
8 | rune res = 0xFFFD; | ||
9 | size_t length = 1; | ||
10 | |||
11 | if (s.size() == 0) | ||
12 | return 0; | ||
13 | const uint8_t *b = reinterpret_cast<const uint8_t *>(s.data()); | ||
14 | if (!(b[0] & 0x80)) | ||
15 | res = static_cast<rune>(b[0]); | ||
16 | else if ((b[0] & 0xE0) == 0xC0) { | ||
17 | length = 2; | ||
18 | if (s.size() >= 2 && (b[1] & 0xC0) == 0x80) { | ||
19 | res = static_cast<rune>(b[0] & ~0xC0) << 6; | ||
20 | res |= static_cast<rune>(b[1] & ~0x80); | ||
21 | } | ||
22 | } else if ((b[0] & 0xF0) == 0xE0) { | ||
23 | length = 3; | ||
24 | if (s.size() >= 3 && (b[1] & 0xC0) == 0x80 && (b[2] & 0xC0) == 0x80) { | ||
25 | res = static_cast<rune>(b[0] & ~0xE0) << 12; | ||
26 | res |= static_cast<rune>(b[1] & ~0x80) << 6; | ||
27 | res |= static_cast<rune>(b[2] & ~0x80); | ||
28 | } | ||
29 | } else if (b[0] == 0xF0) { | ||
30 | length = 4; | ||
31 | if (s.size() >= 4 && (b[1] & 0xC0) == 0x80 && (b[2] & 0xC0) == 0x80 && (b[3] & 0xC0) == 0x80) { | ||
32 | res = static_cast<rune>(b[0] & ~0xF0) << 18; | ||
33 | res |= static_cast<rune>(b[1] & ~0x80) << 12; | ||
34 | res |= static_cast<rune>(b[2] & ~0x80) << 6; | ||
35 | res |= static_cast<rune>(b[3] & ~0x80); | ||
36 | } | ||
37 | } | ||
38 | |||
39 | if (dest) | ||
40 | *dest = res; | ||
41 | return length; | ||
42 | } | ||
43 | |||
44 | // Counts the number of codepoints in a valid UTF-8 string. Returns SIZE_MAX if | ||
45 | // the string contains invalid UTF-8 codepoints. | ||
46 | static size_t stringViewLengthUtf8(std::string_view sv) { | ||
47 | size_t codepoints = 0; | ||
48 | while (sv.size() > 0) { | ||
49 | size_t codepoint_size = decodeUtf8Cp(sv); | ||
50 | if (codepoint_size == 0) return SIZE_MAX; | ||
51 | codepoints++; | ||
52 | sv = sv.substr(codepoint_size); | ||
53 | } | ||
54 | return codepoints; | ||
55 | } | ||
56 | |||
57 | Kv1Parser::Kv1Parser(std::vector<Kv1Token> tokens, Kv1Records &parse_into) | ||
58 | : tokens(std::move(tokens)), | ||
59 | records(parse_into) | ||
60 | {} | ||
61 | |||
62 | bool Kv1Parser::atEnd() const { | ||
63 | return pos >= tokens.size(); | ||
64 | } | ||
65 | |||
66 | void Kv1Parser::eatRowEnds() { | ||
67 | while (!atEnd() && tokens[pos].type == KV1_TOKEN_ROW_END) pos++; | ||
68 | } | ||
69 | |||
70 | const Kv1Token *Kv1Parser::cur() const { | ||
71 | if (atEnd()) return nullptr; | ||
72 | return &tokens[pos]; | ||
73 | } | ||
74 | |||
75 | const std::string *Kv1Parser::eatCell(std::string_view parsing_what) { | ||
76 | const Kv1Token *tok = cur(); | ||
77 | if (!tok) { | ||
78 | record_errors.push_back(std::format("Expected cell but got end of file when parsing {}", parsing_what)); | ||
79 | return nullptr; | ||
80 | } | ||
81 | if (tok->type == KV1_TOKEN_ROW_END) { | ||
82 | record_errors.push_back(std::format("Expected cell but got end of row when parsing {}", parsing_what)); | ||
83 | return nullptr; | ||
84 | } | ||
85 | pos++; | ||
86 | return &tok->data; | ||
87 | } | ||
88 | |||
89 | void Kv1Parser::requireString(std::string_view field, bool mandatory, size_t max_length, std::string_view value) { | ||
90 | if (value.empty() && mandatory) { | ||
91 | record_errors.push_back(std::format("{} has length zero but is required", field)); | ||
92 | return; | ||
93 | } | ||
94 | size_t codepoints = stringViewLengthUtf8(value); | ||
95 | if (codepoints == SIZE_MAX) { | ||
96 | global_errors.push_back(std::format("{} contains invalid UTF-8 code points", field)); | ||
97 | return; | ||
98 | } | ||
99 | if (codepoints > max_length) { | ||
100 | record_errors.push_back(std::format("{} has length ({}) that is greater than maximum length ({})", | ||
101 | field, value.size(), max_length)); | ||
102 | } | ||
103 | } | ||
104 | |||
105 | static inline std::optional<bool> parseBoolean(std::string_view src) { | ||
106 | if (src == "1") return true; | ||
107 | if (src == "0") return false; | ||
108 | if (src == "true") return true; | ||
109 | if (src == "false") return false; | ||
110 | return std::nullopt; | ||
111 | } | ||
112 | |||
113 | std::optional<bool> Kv1Parser::requireBoolean(std::string_view field, bool mandatory, std::string_view value) { | ||
114 | if (value.empty()) { | ||
115 | if (mandatory) | ||
116 | record_errors.push_back(std::format("{} is required, but has no value", field)); | ||
117 | return std::nullopt; | ||
118 | } | ||
119 | auto parsed = parseBoolean(value); | ||
120 | if (!parsed.has_value()) | ||
121 | record_errors.push_back(std::format("{} should have value \"1\", \"0\", \"true\" or \"false\"", field)); | ||
122 | return parsed; | ||
123 | } | ||
124 | |||
125 | static inline size_t countDigits(long x) { | ||
126 | size_t digits = 0; | ||
127 | while (x != 0) { digits++; x /= 10; } | ||
128 | return digits; | ||
129 | } | ||
130 | |||
131 | std::optional<double> Kv1Parser::requireNumber(std::string_view field, bool mandatory, size_t max_digits, std::string_view value) { | ||
132 | if (value.empty()) { | ||
133 | if (mandatory) | ||
134 | record_errors.push_back(std::format("{} has no value but is required", field)); | ||
135 | return std::nullopt; | ||
136 | } | ||
137 | |||
138 | double parsed; | ||
139 | auto [ptr, ec] = std::from_chars(value.data(), value.data() + value.size(), parsed, std::chars_format::fixed); | ||
140 | if (ec != std::errc()) { | ||
141 | record_errors.push_back(std::format("{} has a bad value that cannot be parsed as a number", field)); | ||
142 | return std::nullopt; | ||
143 | } | ||
144 | if (ptr != value.data() + value.size()) { | ||
145 | record_errors.push_back(std::format("{} contains characters that were not parsed as a number", field)); | ||
146 | return std::nullopt; | ||
147 | } | ||
148 | |||
149 | size_t digits = countDigits(static_cast<long>(parsed)); | ||
150 | if (digits > max_digits) { | ||
151 | record_errors.push_back(std::format("{} contains more digits (in the integral part) ({}) than allowed ({})", | ||
152 | field, digits, max_digits)); | ||
153 | return std::nullopt; | ||
154 | } | ||
155 | |||
156 | return parsed; | ||
157 | } | ||
158 | |||
159 | static inline bool isHexDigit(char c) { | ||
160 | return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F'); | ||
161 | } | ||
162 | |||
163 | static inline uint8_t fromHex(char c) { | ||
164 | if (c >= '0' && c <= '9') return static_cast<uint8_t>(c - '0'); | ||
165 | else if (c >= 'A' && c <= 'F') return static_cast<uint8_t>(c - 'A' + 10); | ||
166 | return 0; | ||
167 | } | ||
168 | |||
169 | static std::optional<RgbColor> parseRgbColor(std::string_view src) { | ||
170 | bool valid = src.size() == 6 | ||
171 | && isHexDigit(src[0]) && isHexDigit(src[1]) | ||
172 | && isHexDigit(src[2]) && isHexDigit(src[3]) | ||
173 | && isHexDigit(src[4]) && isHexDigit(src[5]); | ||
174 | if (!valid) return std::nullopt; | ||
175 | uint8_t r = static_cast<uint8_t>(fromHex(src[0]) << 4) + fromHex(src[1]); | ||
176 | uint8_t g = static_cast<uint8_t>(fromHex(src[2]) << 4) + fromHex(src[3]); | ||
177 | uint8_t b = static_cast<uint8_t>(fromHex(src[4]) << 4) + fromHex(src[5]); | ||
178 | return RgbColor{ r, g, b }; | ||
179 | } | ||
180 | |||
181 | std::optional<RgbColor> Kv1Parser::requireRgbColor(std::string_view field, bool mandatory, std::string_view value) { | ||
182 | if (value.empty()) { | ||
183 | if (mandatory) | ||
184 | record_errors.push_back(std::format("{} is required, but has no value", field)); | ||
185 | return std::nullopt; | ||
186 | } | ||
187 | auto parsed = parseRgbColor(value); | ||
188 | if (!parsed.has_value()) | ||
189 | record_errors.push_back(std::format("{} should be an RGB color, i.e. a sequence of six hexadecimally represented nibbles", field)); | ||
190 | return parsed; | ||
191 | } | ||
192 | |||
193 | std::optional<double> Kv1Parser::requireRdCoord(std::string_view field, bool mandatory, size_t min_digits, std::string_view value) { | ||
194 | if (value.empty()) { | ||
195 | if (mandatory) | ||
196 | record_errors.push_back(std::format("{} is required, but has no value", field)); | ||
197 | return std::nullopt; | ||
198 | } | ||
199 | if (value.size() > 15) { | ||
200 | record_errors.push_back(std::format("{} may not have more than 15 characters", field)); | ||
201 | return std::nullopt; | ||
202 | } | ||
203 | |||
204 | double parsed; | ||
205 | auto [ptr, ec] = std::from_chars(value.data(), value.data() + value.size(), parsed, std::chars_format::fixed); | ||
206 | if (ec != std::errc()) { | ||
207 | record_errors.push_back(std::format("{} has a bad value that cannot be parsed as a number", field)); | ||
208 | return std::nullopt; | ||
209 | } | ||
210 | if (ptr != value.data() + value.size()) { | ||
211 | record_errors.push_back(std::format("{} contains characters that were not parsed as a number", field)); | ||
212 | return std::nullopt; | ||
213 | } | ||
214 | |||
215 | size_t digits = countDigits(static_cast<long>(parsed)); | ||
216 | if (digits < min_digits) { | ||
217 | record_errors.push_back(std::format("{} contains less digits (in the integral part) ({}) than required ({}) [value: {}]", | ||
218 | field, digits, min_digits, value)); | ||
219 | return std::nullopt; | ||
220 | } | ||
221 | |||
222 | return parsed; | ||
223 | } | ||
224 | |||
225 | std::string Kv1Parser::eatString(std::string_view field, bool mandatory, size_t max_length) { | ||
226 | auto value = eatCell(field); | ||
227 | if (!record_errors.empty()) return {}; | ||
228 | requireString(field, mandatory, max_length, *value); | ||
229 | return std::move(*value); | ||
230 | } | ||
231 | |||
232 | std::optional<bool> Kv1Parser::eatBoolean(std::string_view field, bool mandatory) { | ||
233 | auto value = eatCell(field); | ||
234 | if (!record_errors.empty()) return {}; | ||
235 | return requireBoolean(field, mandatory, *value); | ||
236 | } | ||
237 | |||
238 | std::optional<double> Kv1Parser::eatNumber(std::string_view field, bool mandatory, size_t max_digits) { | ||
239 | auto value = eatCell(field); | ||
240 | if (!record_errors.empty()) return {}; | ||
241 | return requireNumber(field, mandatory, max_digits, *value); | ||
242 | } | ||
243 | |||
244 | std::optional<RgbColor> Kv1Parser::eatRgbColor(std::string_view field, bool mandatory) { | ||
245 | auto value = eatCell(field); | ||
246 | if (!record_errors.empty()) return {}; | ||
247 | return requireRgbColor(field, mandatory, *value); | ||
248 | } | ||
249 | |||
250 | std::optional<double> Kv1Parser::eatRdCoord(std::string_view field, bool mandatory, size_t min_digits) { | ||
251 | auto value = eatCell(field); | ||
252 | if (!record_errors.empty()) return {}; | ||
253 | return requireRdCoord(field, mandatory, min_digits, *value); | ||
254 | } | ||
255 | |||
256 | std::string Kv1Parser::parseHeader() { | ||
257 | auto record_type = eatString("<header>.Recordtype", true, 10); | ||
258 | auto version_number = eatString("<header>.VersionNumber", true, 2); | ||
259 | auto implicit_explicit = eatString("<header>.Implicit/Explicit", true, 1); | ||
260 | if (!record_errors.empty()) return {}; | ||
261 | |||
262 | if (version_number != "1") { | ||
263 | record_errors.push_back("<header>.VersionNumber should be 1"); | ||
264 | return ""; | ||
265 | } | ||
266 | if (implicit_explicit != "I") { | ||
267 | record_errors.push_back("<header>.Implicit/Explicit should be 'I'"); | ||
268 | return ""; | ||
269 | } | ||
270 | |||
271 | return record_type; | ||
272 | } | ||
273 | |||
274 | void Kv1Parser::eatRestOfRow() { | ||
275 | while (!atEnd() && cur()->type != KV1_TOKEN_ROW_END) pos++; | ||
276 | } | ||
277 | |||
278 | void Kv1Parser::parse() { | ||
279 | while (!atEnd()) { | ||
280 | eatRowEnds(); | ||
281 | if (atEnd()) return; | ||
282 | |||
283 | std::string record_type = parseHeader(); | ||
284 | if (!record_errors.empty()) break; | ||
285 | if (!type_parsers.contains(record_type)) { | ||
286 | warns.push_back(std::format("Recordtype ({}) is bad or names a record type that this program cannot process", | ||
287 | record_type)); | ||
288 | eatRestOfRow(); | ||
289 | continue; | ||
290 | } | ||
291 | |||
292 | ParseFunc parseType = Kv1Parser::type_parsers.at(record_type); | ||
293 | (this->*parseType)(); | ||
294 | if (cur() && cur()->type != KV1_TOKEN_ROW_END) { | ||
295 | record_errors.push_back(std::format("Parser function for Recordtype ({}) did not eat all record fields", | ||
296 | record_type)); | ||
297 | eatRestOfRow(); | ||
298 | } | ||
299 | if (!record_errors.empty()) { | ||
300 | global_errors.insert(global_errors.end(), record_errors.begin(), record_errors.end()); | ||
301 | record_errors.clear(); | ||
302 | } | ||
303 | } | ||
304 | } | ||
305 | |||
306 | void Kv1Parser::parseOrganizationalUnit() { | ||
307 | auto data_owner_code = eatString("ORUN.DataOwnerCode", true, 10); | ||
308 | auto organizational_unit_code = eatString("ORUN.OrganizationalUnitCode", true, 10); | ||
309 | auto name = eatString("ORUN.Name", true, 50); | ||
310 | auto organizational_unit_type = eatString("ORUN.OrganizationalUnitType", true, 10); | ||
311 | auto description = eatString("ORUN.Description", false, 255); | ||
312 | if (!record_errors.empty()) return; | ||
313 | |||
314 | records.organizational_units.emplace_back( | ||
315 | Kv1OrganizationalUnit::Key( | ||
316 | data_owner_code, | ||
317 | organizational_unit_code), | ||
318 | name, | ||
319 | organizational_unit_type, | ||
320 | description); | ||
321 | } | ||
322 | |||
323 | static inline bool isDigit(char c) { | ||
324 | return c >= '0' && c <= '9'; | ||
325 | } | ||
326 | |||
327 | // Parse a string of the format YYYY-MM-DD. | ||
328 | static std::optional<std::chrono::year_month_day> parseYyyymmdd(std::string_view src) { | ||
329 | bool valid = src.size() == 10 | ||
330 | && isDigit(src[0]) && isDigit(src[1]) | ||
331 | && isDigit(src[2]) && isDigit(src[3]) && src[4] == '-' | ||
332 | && isDigit(src[5]) && isDigit(src[6]) && src[7] == '-' | ||
333 | && isDigit(src[8]) && isDigit(src[9]); | ||
334 | if (!valid) return std::nullopt; | ||
335 | int year = (src[0] - '0') * 1000 + (src[1] - '0') * 100 + (src[2] - '0') * 10 + src[3] - '0'; | ||
336 | int month = (src[5] - '0') * 10 + src[6] - '0'; | ||
337 | int day = (src[8] - '0') * 10 + src[9] - '0'; | ||
338 | return std::chrono::year(year) / std::chrono::month(month) / std::chrono::day(day); | ||
339 | } | ||
340 | |||
341 | // Parse a string of the format HH:MM:SS. | ||
342 | static std::optional<std::chrono::hh_mm_ss<std::chrono::seconds>> parseHhmmss(std::string_view src) { | ||
343 | bool valid = src.size() == 8 | ||
344 | && isDigit(src[0]) && isDigit(src[1]) && src[2] == ':' | ||
345 | && isDigit(src[3]) && isDigit(src[4]) && src[5] == ':' | ||
346 | && isDigit(src[6]) && isDigit(src[7]); | ||
347 | if (!valid) return std::nullopt; | ||
348 | int hh = (src[0] - '0') * 10 + src[1] - '0'; | ||
349 | int mm = (src[3] - '0') * 10 + src[4] - '0'; | ||
350 | int ss = (src[6] - '0') * 10 + src[7] - '0'; | ||
351 | // The check for the hour not being greater than 32 comes from the fact the | ||
352 | // specification explicitly allows hours greater than 23, noting that the | ||
353 | // period 24:00-32:00 is equivalent to 00:00-08:00 in the next day, for | ||
354 | // exploitation of two days. | ||
355 | if (hh > 32 || mm > 59 || ss > 59) return std::nullopt; | ||
356 | return std::chrono::hh_mm_ss(std::chrono::hours(hh) + std::chrono::minutes(mm) + std::chrono::seconds(ss)); | ||
357 | } | ||
358 | |||
359 | static std::optional<std::chrono::sys_seconds> parseDateTime(std::string_view src, const std::chrono::time_zone *amsterdam, std::string_view *error = nullptr) { | ||
360 | #define ERROR(err) do { if (error) *error = err; return std::nullopt; } while (0) | ||
361 | if (src.size() > 23) ERROR("timestamp string is too big"); | ||
362 | if (src.size() < 17) ERROR("timestamp string is too small"); | ||
363 | |||
364 | bool valid_year = isDigit(src[0]) && isDigit(src[1]) && isDigit(src[2]) && isDigit(src[3]); | ||
365 | if (!valid_year) ERROR("year has bad format"); | ||
366 | |||
367 | size_t month_off = src[4] == '-' ? 5 : 4; | ||
368 | size_t day_off = src[month_off + 2] == '-' ? month_off + 3 : month_off + 2; | ||
369 | size_t time_off = day_off + 2; | ||
370 | if (src[time_off] != 'T' && src[time_off] != ' ') | ||
371 | ERROR("missing date/time separator"); | ||
372 | size_t tzd_off = time_off + 9; | ||
373 | // For clarity, TZD stands for Time Zone Designator. It often takes the form | ||
374 | // of Z (Zulu, UTC+00:00) or as an offset from UTC in hours and minutes, | ||
375 | // formatted as +|-HH:MM (e.g. +01:00, -12:00). | ||
376 | |||
377 | if (time_off + 8 >= src.size()) ERROR("bad format, not enough space for hh:mm:ss"); | ||
378 | |||
379 | int year = (src[0] - '0') * 1000 + (src[1] - '0') * 100 + (src[2] - '0') * 10 + src[3] - '0'; | ||
380 | int month = (src[month_off] - '0') * 10 + src[month_off + 1] - '0'; | ||
381 | int day = (src[day_off] - '0') * 10 + src[day_off + 1] - '0'; | ||
382 | int hour = (src[time_off + 1] - '0') * 10 + src[time_off + 2] - '0'; | ||
383 | int minute = (src[time_off + 4] - '0') * 10 + src[time_off + 5] - '0'; | ||
384 | int second = (src[time_off + 7] - '0') * 10 + src[time_off + 8] - '0'; | ||
385 | |||
386 | auto date = std::chrono::year(year) / std::chrono::month(month) / std::chrono::day(day); | ||
387 | auto time = std::chrono::hours(hour) + std::chrono::minutes(minute) + std::chrono::seconds(second); | ||
388 | |||
389 | std::chrono::sys_seconds unix_start_of_day; | ||
390 | if (tzd_off < src.size()) { | ||
391 | unix_start_of_day = std::chrono::sys_days(date); | ||
392 | } else { | ||
393 | auto local_days = std::chrono::local_days(date); | ||
394 | std::chrono::zoned_seconds zoned_start_of_day = std::chrono::zoned_time(amsterdam, local_days); | ||
395 | unix_start_of_day = std::chrono::sys_seconds(zoned_start_of_day); | ||
396 | } | ||
397 | |||
398 | std::chrono::minutes offset(0); | ||
399 | if (tzd_off + 1 == src.size() && src[tzd_off] != 'Z') { | ||
400 | ERROR("bad TZD (missing Zulu indicator)"); | ||
401 | } else if (tzd_off + 6 == src.size()) { | ||
402 | bool valid_tzd = (src[tzd_off] == '+' || src[tzd_off] == '-') | ||
403 | && isDigit(src[tzd_off + 1]) && isDigit(src[tzd_off + 2]) && src[tzd_off + 3] == ':' | ||
404 | && isDigit(src[tzd_off + 4]) && isDigit(src[tzd_off + 5]); | ||
405 | if (!valid_tzd) ERROR("bad offset TZD format (expected +|-hh:mm)"); | ||
406 | int sign = src[tzd_off] == '-' ? -1 : 1; | ||
407 | int tzd_hh = (src[tzd_off + 1] - '0') * 10 + src[tzd_off + 2] - '0'; | ||
408 | int tzd_mm = (src[tzd_off + 3] - '0') * 10 + src[tzd_off + 4] - '0'; | ||
409 | offset = sign * std::chrono::minutes(tzd_hh * 60 + tzd_mm); | ||
410 | } else if (tzd_off < src.size()) { | ||
411 | // There is a TZD but we literally have no clue how to parse it :/ | ||
412 | ERROR("cannot parse TZD of unexpected length"); | ||
413 | } | ||
414 | |||
415 | return unix_start_of_day + time - offset; | ||
416 | #undef ERROR | ||
417 | } | ||
418 | |||
419 | void Kv1Parser::parseHigherOrganizationalUnit() { | ||
420 | auto data_owner_code = eatString("ORUNORUN.DataOwnerCode", true, 10); | ||
421 | auto organizational_unit_code_parent = eatString("ORUNORUN.OrganizationalUnitCodeParent", true, 10); | ||
422 | auto organizational_unit_code_child = eatString("ORUNORUN.OrganizationalUnitCodeChild", true, 10); | ||
423 | auto valid_from_raw = eatString("ORUNORUN.ValidFrom", true, 10); | ||
424 | if (!record_errors.empty()) return; | ||
425 | |||
426 | auto valid_from = parseYyyymmdd(valid_from_raw); | ||
427 | if (!valid_from) { | ||
428 | record_errors.push_back("ORUNORUN.ValidFrom has invalid format, should be YYYY-MM-DD"); | ||
429 | return; | ||
430 | } | ||
431 | |||
432 | records.higher_organizational_units.emplace_back( | ||
433 | Kv1HigherOrganizationalUnit::Key( | ||
434 | data_owner_code, | ||
435 | organizational_unit_code_parent, | ||
436 | organizational_unit_code_child, | ||
437 | *valid_from)); | ||
438 | } | ||
439 | |||
440 | void Kv1Parser::parseUserStopPoint() { | ||
441 | auto data_owner_code = eatString ("USRSTOP.DataOwnerCode", true, 10); | ||
442 | auto user_stop_code = eatString ("USRSTOP.UserStopCode", true, 10); | ||
443 | auto timing_point_code = eatString ("USRSTOP.TimingPointCode", false, 10); | ||
444 | auto get_in = eatBoolean("USRSTOP.GetIn", true ); | ||
445 | auto get_out = eatBoolean("USRSTOP.GetOut", true ); | ||
446 | eatCell ("USRSTOP.<deprecated field #1>" ); | ||
447 | auto name = eatString ("USRSTOP.Name", true, 50); | ||
448 | auto town = eatString ("USRSTOP.Town", true, 50); | ||
449 | auto user_stop_area_code = eatString ("USRSTOP.UserStopAreaCode", false, 10); | ||
450 | auto stop_side_code = eatString ("USRSTOP.StopSideCode", true, 10); | ||
451 | eatCell ("USRSTOP.<deprecated field #2>" ); | ||
452 | eatCell ("USRSTOP.<deprecated field #3>" ); | ||
453 | auto minimal_stop_time = eatNumber ("USRSTOP.MinimalStopTime", true, 5); | ||
454 | auto stop_side_length = eatNumber ("USRSTOP.StopSideLength", false, 3); | ||
455 | auto description = eatString ("USRSTOP.Description", false, 255); | ||
456 | auto user_stop_type = eatString ("USRSTOP.UserStopType", true, 10); | ||
457 | auto quay_code = eatString ("USRSTOP.QuayCode", false, 30); | ||
458 | if (!record_errors.empty()) return; | ||
459 | |||
460 | records.user_stop_points.emplace_back( | ||
461 | Kv1UserStopPoint::Key( | ||
462 | data_owner_code, | ||
463 | user_stop_code), | ||
464 | timing_point_code, | ||
465 | *get_in, | ||
466 | *get_out, | ||
467 | name, | ||
468 | town, | ||
469 | user_stop_area_code, | ||
470 | stop_side_code, | ||
471 | *minimal_stop_time, | ||
472 | stop_side_length, | ||
473 | description, | ||
474 | user_stop_type, | ||
475 | quay_code); | ||
476 | } | ||
477 | |||
478 | void Kv1Parser::parseUserStopArea() { | ||
479 | auto data_owner_code = eatString("USRSTAR.DataOwnerCode", true, 10); | ||
480 | auto user_stop_area_code = eatString("USRSTAR.UserStopAreaCode", true, 10); | ||
481 | auto name = eatString("USRSTAR.Name", true, 50); | ||
482 | auto town = eatString("USRSTAR.Town", true, 50); | ||
483 | eatCell ("USRSTAR.<deprecated field #1>" ); | ||
484 | eatCell ("USRSTAR.<deprecated field #2>" ); | ||
485 | auto description = eatString("USRSTAR.Description", false, 255); | ||
486 | if (!record_errors.empty()) return; | ||
487 | |||
488 | records.user_stop_areas.emplace_back( | ||
489 | Kv1UserStopArea::Key( | ||
490 | data_owner_code, | ||
491 | user_stop_area_code), | ||
492 | name, | ||
493 | town, | ||
494 | description); | ||
495 | } | ||
496 | |||
497 | void Kv1Parser::parseTimingLink() { | ||
498 | auto data_owner_code = eatString("TILI.DataOwnerCode", true, 10); | ||
499 | auto user_stop_code_begin = eatString("TILI.UserStopCodeBegin", true, 10); | ||
500 | auto user_stop_code_end = eatString("TILI.UserStopCodeEnd", true, 10); | ||
501 | auto minimal_drive_time = eatNumber("TILI.MinimalDriveTime", false, 5); | ||
502 | auto description = eatString("TILI.Description", false, 255); | ||
503 | if (!record_errors.empty()) return; | ||
504 | |||
505 | records.timing_links.emplace_back( | ||
506 | Kv1TimingLink::Key( | ||
507 | data_owner_code, | ||
508 | user_stop_code_begin, | ||
509 | user_stop_code_end), | ||
510 | minimal_drive_time, | ||
511 | description); | ||
512 | } | ||
513 | |||
514 | void Kv1Parser::parseLink() { | ||
515 | auto data_owner_code = eatString("LINK.DataOwnerCode", true, 10); | ||
516 | auto user_stop_code_begin = eatString("LINK.UserStopCodeBegin", true, 10); | ||
517 | auto user_stop_code_end = eatString("LINK.UserStopCodeEnd", true, 10); | ||
518 | eatCell("LINK.<deprecated field #1>" ); | ||
519 | auto distance = eatNumber("LINK.Distance", true, 6); | ||
520 | auto description = eatString("LINK.Description", false, 255); | ||
521 | auto transport_type = eatString("LINK.TransportType", true, 5); | ||
522 | if (!record_errors.empty()) return; | ||
523 | |||
524 | records.links.emplace_back( | ||
525 | Kv1Link::Key( | ||
526 | data_owner_code, | ||
527 | user_stop_code_begin, | ||
528 | user_stop_code_end, | ||
529 | transport_type), | ||
530 | *distance, | ||
531 | description); | ||
532 | } | ||
533 | |||
534 | void Kv1Parser::parseLine() { | ||
535 | auto data_owner_code = eatString ("LINE.DataOwnerCode", true, 10); | ||
536 | auto line_planning_number = eatString ("LINE.LinePlanningNumber", true, 10); | ||
537 | auto line_public_number = eatString ("LINE.LinePublicNumber", true, 4); | ||
538 | auto line_name = eatString ("LINE.LineName", true, 50); | ||
539 | auto line_ve_tag_number = eatNumber ("LINE.LineVeTagNumber", true, 3); | ||
540 | auto description = eatString ("LINE.Description", false, 255); | ||
541 | auto transport_type = eatString ("LINE.TransportType", true, 5); | ||
542 | auto line_icon = eatNumber ("LINE.LineIcon", false, 4); | ||
543 | auto line_color = eatRgbColor("LINE.LineColor", false ); | ||
544 | auto line_text_color = eatRgbColor("LINE.LineTextColor", false ); | ||
545 | if (!record_errors.empty()) return; | ||
546 | |||
547 | // NOTE: This check, although it should be performed to comply with the | ||
548 | // specification, is not actually honored by transit operators (such as | ||
549 | // Connexxion) :/ That's enough reason to keep it disabled here for now. | ||
550 | // if (*line_ve_tag_number < 0 || *line_ve_tag_number > 399) { | ||
551 | // record_errors.push_back(std::format("LINE.LineVeTagNumber is out of range [0-399] with value {}", *line_ve_tag_number)); | ||
552 | // return; | ||
553 | // } | ||
554 | if (*line_ve_tag_number != static_cast<short>(*line_ve_tag_number)) | ||
555 | record_errors.push_back("LINE.LineVeTagNumber should be an integer"); | ||
556 | if (line_icon && *line_icon != static_cast<short>(*line_icon)) | ||
557 | record_errors.push_back("LINE.LineIcon should be an integer"); | ||
558 | if (!record_errors.empty()) return; | ||
559 | |||
560 | records.lines.emplace_back( | ||
561 | Kv1Line::Key( | ||
562 | data_owner_code, | ||
563 | line_planning_number), | ||
564 | line_public_number, | ||
565 | line_name, | ||
566 | static_cast<short>(*line_ve_tag_number), | ||
567 | description, | ||
568 | transport_type, | ||
569 | static_cast<std::optional<short>>(line_icon), | ||
570 | line_color, | ||
571 | line_text_color); | ||
572 | } | ||
573 | |||
574 | void Kv1Parser::parseDestination() { | ||
575 | auto data_owner_code = eatString ("DEST.DataOwnerCode", true, 10); | ||
576 | auto dest_code = eatString ("DEST.DestCode", true, 10); | ||
577 | auto dest_name_full = eatString ("DEST.DestNameFull", true, 50); | ||
578 | auto dest_name_main = eatString ("DEST.DestNameMain", true, 24); | ||
579 | auto dest_name_detail = eatString ("DEST.DestNameDetail", false, 24); | ||
580 | auto relevant_dest_name_detail = eatBoolean ("DEST.RelevantDestNameDetail", true ); | ||
581 | auto dest_name_main_21 = eatString ("DEST.DestNameMain21", true, 21); | ||
582 | auto dest_name_detail_21 = eatString ("DEST.DestNameDetail21", false, 21); | ||
583 | auto dest_name_main_19 = eatString ("DEST.DestNameMain19", true, 19); | ||
584 | auto dest_name_detail_19 = eatString ("DEST.DestNameDetail19", false, 19); | ||
585 | auto dest_name_main_16 = eatString ("DEST.DestNameMain16", true, 16); | ||
586 | auto dest_name_detail_16 = eatString ("DEST.DestNameDetail16", false, 16); | ||
587 | auto dest_icon = eatNumber ("DEST.DestIcon", false, 4); | ||
588 | auto dest_color = eatRgbColor("DEST.DestColor", false ); | ||
589 | // NOTE: Deviating from the offical KV1 specification here. It specifies that | ||
590 | // the maximum length for this field should be 30, but then proceeds to | ||
591 | // specify that it should contain a RGB value comprising of three | ||
592 | // hexadecimally encoded octets, i.e. six characters. We assume that the | ||
593 | // latter is correct and the intended interpretation. | ||
594 | auto dest_text_color = eatRgbColor("DEST.DestTextColor", false ); | ||
595 | if (!record_errors.empty()) return; | ||
596 | |||
597 | if (dest_icon && *dest_icon != static_cast<short>(*dest_icon)) { | ||
598 | record_errors.push_back("DEST.DestIcon should be an integer"); | ||
599 | return; | ||
600 | } | ||
601 | |||
602 | records.destinations.emplace_back( | ||
603 | Kv1Destination::Key( | ||
604 | data_owner_code, | ||
605 | dest_code), | ||
606 | dest_name_full, | ||
607 | dest_name_main, | ||
608 | dest_name_detail, | ||
609 | *relevant_dest_name_detail, | ||
610 | dest_name_main_21, | ||
611 | dest_name_detail_21, | ||
612 | dest_name_main_19, | ||
613 | dest_name_detail_19, | ||
614 | dest_name_main_16, | ||
615 | dest_name_detail_16, | ||
616 | dest_icon, | ||
617 | dest_color, | ||
618 | dest_text_color); | ||
619 | } | ||
620 | |||
621 | void Kv1Parser::parseJourneyPattern() { | ||
622 | auto data_owner_code = eatString("JOPA.DataOwnerCode", true, 10); | ||
623 | auto line_planning_number = eatString("JOPA.LinePlanningNumber", true, 10); | ||
624 | auto journey_pattern_code = eatString("JOPA.JourneyPatternCode", true, 10); | ||
625 | auto journey_pattern_type = eatString("JOPA.JourneyPatternType", true, 10); | ||
626 | auto direction = eatString("JOPA.Direction", true, 1); | ||
627 | auto description = eatString("JOPA.Description", false, 255); | ||
628 | if (!record_errors.empty()) return; | ||
629 | |||
630 | if (direction != "1" && direction != "2" && direction != "A" && direction != "B") { | ||
631 | record_errors.push_back("JOPA.Direction should be in [1, 2, A, B]"); | ||
632 | return; | ||
633 | } | ||
634 | |||
635 | records.journey_patterns.emplace_back( | ||
636 | Kv1JourneyPattern::Key( | ||
637 | data_owner_code, | ||
638 | line_planning_number, | ||
639 | journey_pattern_code), | ||
640 | journey_pattern_type, | ||
641 | direction[0], | ||
642 | description); | ||
643 | } | ||
644 | |||
645 | void Kv1Parser::parseConcessionFinancerRelation() { | ||
646 | auto data_owner_code = eatString("CONFINREL.DataOwnerCode", true, 10); | ||
647 | auto con_fin_rel_code = eatString("CONFINREL.ConFinRelCode", true, 10); | ||
648 | auto concession_area_code = eatString("CONFINREL.ConcessionAreaCode", true, 10); | ||
649 | auto financer_code = eatString("CONFINREL.FinancerCode", false, 10); | ||
650 | if (!record_errors.empty()) return; | ||
651 | |||
652 | records.concession_financer_relations.emplace_back( | ||
653 | Kv1ConcessionFinancerRelation::Key( | ||
654 | data_owner_code, | ||
655 | con_fin_rel_code), | ||
656 | concession_area_code, | ||
657 | financer_code); | ||
658 | } | ||
659 | |||
660 | void Kv1Parser::parseConcessionArea() { | ||
661 | auto data_owner_code = eatString("CONAREA.DataOwnerCode", true, 10); | ||
662 | auto concession_area_code = eatString("CONAREA.ConcessionAreaCode", true, 10); | ||
663 | auto description = eatString("CONAREA.Description", true, 255); | ||
664 | if (!record_errors.empty()) return; | ||
665 | |||
666 | records.concession_areas.emplace_back( | ||
667 | Kv1ConcessionArea::Key( | ||
668 | data_owner_code, | ||
669 | concession_area_code), | ||
670 | description); | ||
671 | } | ||
672 | |||
673 | void Kv1Parser::parseFinancer() { | ||
674 | auto data_owner_code = eatString("FINANCER.DataOwnerCode", true, 10); | ||
675 | auto financer_code = eatString("FINANCER.FinancerCode", true, 10); | ||
676 | auto description = eatString("FINANCER.Description", true, 255); | ||
677 | if (!record_errors.empty()) return; | ||
678 | |||
679 | records.financers.emplace_back( | ||
680 | Kv1Financer::Key( | ||
681 | data_owner_code, | ||
682 | financer_code), | ||
683 | description); | ||
684 | } | ||
685 | |||
686 | void Kv1Parser::parseJourneyPatternTimingLink() { | ||
687 | auto data_owner_code = eatString ("JOPATILI.DataOwnerCode", true, 10); | ||
688 | auto line_planning_number = eatString ("JOPATILI.LinePlanningNumber", true, 10); | ||
689 | auto journey_pattern_code = eatString ("JOPATILI.JourneyPatternCode", true, 10); | ||
690 | auto timing_link_order = eatNumber ("JOPATILI.TimingLinkOrder", true, 3); | ||
691 | auto user_stop_code_begin = eatString ("JOPATILI.UserStopCodeBegin", true, 10); | ||
692 | auto user_stop_code_end = eatString ("JOPATILI.UserStopCodeEnd", true, 10); | ||
693 | auto con_fin_rel_code = eatString ("JOPATILI.ConFinRelCode", true, 10); | ||
694 | auto dest_code = eatString ("JOPATILI.DestCode", true, 10); | ||
695 | eatCell ("JOPATILI.<deprecated field #1>" ); | ||
696 | auto is_timing_stop = eatBoolean ("JOPATILI.IsTimingStop", true ); | ||
697 | auto display_public_line = eatString ("JOPATILI.DisplayPublicLine", false, 4); | ||
698 | auto product_formula_type = eatNumber ("JOPATILI.ProductFormulaType", false, 4); | ||
699 | auto get_in = eatBoolean ("JOPATILI.GetIn", true ); | ||
700 | auto get_out = eatBoolean ("JOPATILI.GetOut", true ); | ||
701 | auto show_flexible_trip = eatString ("JOPATILI.ShowFlexibleTrip", false, 8); | ||
702 | auto line_dest_icon = eatNumber ("JOPATILI.LineDestIcon", false, 4); | ||
703 | auto line_dest_color = eatRgbColor("JOPATILI.LineDestColor", false ); | ||
704 | auto line_dest_text_color = eatRgbColor("JOPATILI.LineDestTextColor", false ); | ||
705 | if (!record_errors.empty()) return; | ||
706 | |||
707 | if (line_dest_icon && *line_dest_icon != static_cast<short>(*line_dest_icon)) | ||
708 | record_errors.push_back("JOPATILI.LineDestIcon should be an integer"); | ||
709 | if (!show_flexible_trip.empty() && show_flexible_trip != "TRUE" && | ||
710 | show_flexible_trip != "FALSE" && show_flexible_trip != "REALTIME") | ||
711 | record_errors.push_back("JOPATILI.ShowFlexibleTrip should be in BISON E21 values [TRUE, FALSE, REALTIME]"); | ||
712 | if (!record_errors.empty()) return; | ||
713 | |||
714 | records.journey_pattern_timing_links.emplace_back( | ||
715 | Kv1JourneyPatternTimingLink::Key( | ||
716 | data_owner_code, | ||
717 | line_planning_number, | ||
718 | journey_pattern_code, | ||
719 | static_cast<short>(*timing_link_order)), | ||
720 | user_stop_code_begin, | ||
721 | user_stop_code_end, | ||
722 | con_fin_rel_code, | ||
723 | dest_code, | ||
724 | *is_timing_stop, | ||
725 | display_public_line, | ||
726 | product_formula_type, | ||
727 | *get_in, | ||
728 | *get_out, | ||
729 | show_flexible_trip, | ||
730 | line_dest_icon, | ||
731 | line_dest_color, | ||
732 | line_dest_text_color); | ||
733 | } | ||
734 | |||
735 | void Kv1Parser::parsePoint() { | ||
736 | auto data_owner_code = eatString("POINT.DataOwnerCode", true, 10); | ||
737 | auto point_code = eatString("POINT.PointCode", true, 10); | ||
738 | eatCell ("POINT.<deprecated field #1>" ); | ||
739 | auto point_type = eatString("POINT.PointType", true, 10); | ||
740 | auto coordinate_system_type = eatString("POINT.CoordinateSystemType", true, 10); | ||
741 | // NOTE: We deviate from the specification here once again. The specification | ||
742 | // notes that LocationX_EW should contain 'at least 6 positions'. Assuming | ||
743 | // that this is referring to the amount of digits, we have to lower this to | ||
744 | // 4. Otherwise, some positions in the Netherlands and Belgium are | ||
745 | // unrepresentable. | ||
746 | auto location_x_ew = eatRdCoord("POINT.LocationX_EW", true, 4); | ||
747 | auto location_y_ew = eatRdCoord("POINT.LocationX_EW", true, 6); | ||
748 | auto location_z = eatRdCoord("POINT.LocationZ", false, 0); | ||
749 | auto description = eatString ("POINT.Description", false, 255); | ||
750 | if (!record_errors.empty()) return; | ||
751 | |||
752 | records.points.emplace_back( | ||
753 | Kv1Point::Key( | ||
754 | std::move(data_owner_code), | ||
755 | std::move(point_code)), | ||
756 | std::move(point_type), | ||
757 | std::move(coordinate_system_type), | ||
758 | *location_x_ew, | ||
759 | *location_y_ew, | ||
760 | location_z, | ||
761 | std::move(description)); | ||
762 | } | ||
763 | |||
764 | void Kv1Parser::parsePointOnLink() { | ||
765 | auto data_owner_code = eatString("POOL.DataOwnerCode", true, 10); | ||
766 | auto user_stop_code_begin = eatString("POOL.UserStopCodeBegin", true, 10); | ||
767 | auto user_stop_code_end = eatString("POOL.UserStopCodeEnd", true, 10); | ||
768 | eatCell ("POOL.<deprecated field #1>" ); | ||
769 | auto point_data_owner_code = eatString("POOL.PointDataOwnerCode", true, 10); | ||
770 | auto point_code = eatString("POOL.PointCode", true, 10); | ||
771 | auto distance_since_start_of_link = eatNumber("POOL.DistanceSinceStartOfLink", true, 5); | ||
772 | auto segment_speed = eatNumber("POOL.SegmentSpeed", false, 4); | ||
773 | auto local_point_speed = eatNumber("POOL.LocalPointSpeed", false, 4); | ||
774 | auto description = eatString("POOL.Description", false, 255); | ||
775 | auto transport_type = eatString("POOL.TransportType", true, 5); | ||
776 | if (!record_errors.empty()) return; | ||
777 | |||
778 | records.point_on_links.emplace_back( | ||
779 | Kv1PointOnLink::Key( | ||
780 | data_owner_code, | ||
781 | user_stop_code_begin, | ||
782 | user_stop_code_end, | ||
783 | point_data_owner_code, | ||
784 | point_code, | ||
785 | transport_type), | ||
786 | *distance_since_start_of_link, | ||
787 | segment_speed, | ||
788 | local_point_speed, | ||
789 | std::move(description)); | ||
790 | } | ||
791 | |||
792 | void Kv1Parser::parseIcon() { | ||
793 | auto data_owner_code = eatString("ICON.DataOwnerCode", true, 10); | ||
794 | auto icon_number = eatNumber("ICON.IconNumber", true, 4); | ||
795 | auto icon_uri = eatString("ICON.IconURI", true, 1024); | ||
796 | if (!record_errors.empty()) return; | ||
797 | |||
798 | if (*icon_number != static_cast<short>(*icon_number)) { | ||
799 | record_errors.push_back("ICON.IconNumber should be an integer"); | ||
800 | return; | ||
801 | } | ||
802 | |||
803 | records.icons.emplace_back( | ||
804 | Kv1Icon::Key( | ||
805 | data_owner_code, | ||
806 | static_cast<short>(*icon_number)), | ||
807 | icon_uri); | ||
808 | } | ||
809 | |||
810 | void Kv1Parser::parseNotice() { | ||
811 | auto data_owner_code = eatString("NOTICE.DataOwnerCode", true, 10); | ||
812 | auto notice_code = eatString("NOTICE.NoticeCode", true, 20); | ||
813 | auto notice_content = eatString("NOTICE.NoticeContent", true, 1024); | ||
814 | if (!record_errors.empty()) return; | ||
815 | |||
816 | records.notices.emplace_back( | ||
817 | Kv1Notice::Key( | ||
818 | data_owner_code, | ||
819 | notice_code), | ||
820 | notice_content); | ||
821 | } | ||
822 | |||
823 | void Kv1Parser::parseNoticeAssignment() { | ||
824 | auto data_owner_code = eatString("NTCASSGNM.DataOwnerCode", true, 10); | ||
825 | auto notice_code = eatString("NTCASSGNM.NoticeCode", true, 20); | ||
826 | auto assigned_object = eatString("NTCASSGNM.AssignedObject", true, 8); | ||
827 | auto timetable_version_code = eatString("NTCASSGNM.TimetableVersionCode", false, 10); | ||
828 | auto organizational_unit_code = eatString("NTCASSGNM.OrganizationalUnitCode", false, 10); | ||
829 | auto schedule_code = eatString("NTCASSGNM.ScheduleCode", false, 10); | ||
830 | auto schedule_type_code = eatString("NTCASSGNM.ScheduleTypeCode", false, 10); | ||
831 | auto period_group_code = eatString("NTCASSGNM.PeriodGroupCode", false, 10); | ||
832 | auto specific_day_code = eatString("NTCASSGNM.SpecificDayCode", false, 10); | ||
833 | auto day_type = eatString("NTCASSGNM.DayType", false, 7); | ||
834 | auto line_planning_number = eatString("NTCASSGNM.LinePlanningNumber", true, 10); | ||
835 | auto journey_number = eatNumber("NTCASSGNM.JourneyNumber", false, 6); | ||
836 | auto stop_order = eatNumber("NTCASSGNM.StopOrder", false, 4); | ||
837 | auto journey_pattern_code = eatString("NTCASSGNM.JourneyPatternCode", false, 10); | ||
838 | auto timing_link_order = eatNumber("NTCASSGNM.TimingLinkOrder", false, 3); | ||
839 | auto user_stop_code = eatString("NTCASSGNM.UserStopCode", false, 10); | ||
840 | if (!record_errors.empty()) return; | ||
841 | |||
842 | if (journey_number && *journey_number != static_cast<short>(*journey_number)) | ||
843 | record_errors.push_back("NTCASSGNM.JourneyNumber should be an integer"); | ||
844 | if (journey_number && (*journey_number < 0 || *journey_number > 999'999)) | ||
845 | record_errors.push_back("NTCASSGNM.JourneyNumber should be within the range [0-999999]"); | ||
846 | if (stop_order && *stop_order != static_cast<short>(*stop_order)) | ||
847 | record_errors.push_back("NTCASSGNM.StopOrder should be an integer"); | ||
848 | if (!journey_number && (assigned_object == "PUJO" || assigned_object == "PUJOPASS")) | ||
849 | record_errors.push_back("NTCASSGNM.JourneyNumber is required for AssignedObject PUJO/PUJOPASS"); | ||
850 | if (journey_pattern_code.empty() && assigned_object == "JOPATILI") | ||
851 | record_errors.push_back("NTCASSGNM.JourneyPatternCode is required for AssignedObject JOPATILI"); | ||
852 | if (!record_errors.empty()) return; | ||
853 | |||
854 | records.notice_assignments.emplace_back( | ||
855 | data_owner_code, | ||
856 | notice_code, | ||
857 | assigned_object, | ||
858 | timetable_version_code, | ||
859 | organizational_unit_code, | ||
860 | schedule_code, | ||
861 | schedule_type_code, | ||
862 | period_group_code, | ||
863 | specific_day_code, | ||
864 | day_type, | ||
865 | line_planning_number, | ||
866 | static_cast<std::optional<int>>(journey_number), | ||
867 | static_cast<std::optional<short>>(stop_order), | ||
868 | journey_pattern_code, | ||
869 | timing_link_order, | ||
870 | user_stop_code); | ||
871 | } | ||
872 | |||
873 | void Kv1Parser::parseTimeDemandGroup() { | ||
874 | auto data_owner_code = eatString("TIMDEMGRP.DataOwnerCode", true, 10); | ||
875 | auto line_planning_number = eatString("TIMDEMGRP.LinePlanningNumber", true, 10); | ||
876 | auto journey_pattern_code = eatString("TIMDEMGRP.JourneyPatternCode", true, 10); | ||
877 | auto time_demand_group_code = eatString("TIMDEMGRP.TimeDemandGroupCode", true, 10); | ||
878 | if (!record_errors.empty()) return; | ||
879 | |||
880 | records.time_demand_groups.emplace_back( | ||
881 | Kv1TimeDemandGroup::Key( | ||
882 | data_owner_code, | ||
883 | line_planning_number, | ||
884 | journey_pattern_code, | ||
885 | time_demand_group_code)); | ||
886 | } | ||
887 | |||
888 | void Kv1Parser::parseTimeDemandGroupRunTime() { | ||
889 | auto data_owner_code = eatString("TIMDEMRNT.DataOwnerCode", true, 10); | ||
890 | auto line_planning_number = eatString("TIMDEMRNT.LinePlanningNumber", true, 10); | ||
891 | auto journey_pattern_code = eatString("TIMDEMRNT.JourneyPatternCode", true, 10); | ||
892 | auto time_demand_group_code = eatString("TIMDEMRNT.TimeDemandGroupCode", true, 10); | ||
893 | auto timing_link_order = eatNumber("TIMDEMRNT.TimingLinkOrder", true, 3); | ||
894 | auto user_stop_code_begin = eatString("TIMDEMRNT.UserStopCodeBegin", true, 10); | ||
895 | auto user_stop_code_end = eatString("TIMDEMRNT.UserStopCodeEnd", true, 10); | ||
896 | auto total_drive_time = eatNumber("TIMDEMRNT.TotalDriveTime", true, 5); | ||
897 | auto drive_time = eatNumber("TIMDEMRNT.DriveTime", true, 5); | ||
898 | auto expected_delay = eatNumber("TIMDEMRNT.ExpectedDelay", false, 5); | ||
899 | auto layover_time = eatNumber("TIMDEMRNT.LayOverTime", false, 5); | ||
900 | auto stop_wait_time = eatNumber("TIMDEMRNT.StopWaitTime", true, 5); | ||
901 | auto minimum_stop_time = eatNumber("TIMDEMRNT.MinimumStopTime", false, 5); | ||
902 | if (!record_errors.empty()) return; | ||
903 | |||
904 | if (timing_link_order && *timing_link_order != static_cast<short>(*timing_link_order)) { | ||
905 | record_errors.push_back("TIMDEMRNT.TimingLinkOrder should be an integer"); | ||
906 | return; | ||
907 | } | ||
908 | |||
909 | records.time_demand_group_run_times.emplace_back( | ||
910 | Kv1TimeDemandGroupRunTime::Key( | ||
911 | data_owner_code, | ||
912 | line_planning_number, | ||
913 | journey_pattern_code, | ||
914 | time_demand_group_code, | ||
915 | static_cast<short>(*timing_link_order)), | ||
916 | user_stop_code_begin, | ||
917 | user_stop_code_end, | ||
918 | *total_drive_time, | ||
919 | *drive_time, | ||
920 | expected_delay, | ||
921 | layover_time, | ||
922 | *stop_wait_time, | ||
923 | minimum_stop_time); | ||
924 | } | ||
925 | |||
926 | void Kv1Parser::parsePeriodGroup() { | ||
927 | auto data_owner_code = eatString("PEGR.DataOwnerCode", true, 10); | ||
928 | auto period_group_code = eatString("PEGR.PeriodGroupCode", true, 10); | ||
929 | auto description = eatString("PEGR.Description", false, 255); | ||
930 | if (!record_errors.empty()) return; | ||
931 | |||
932 | records.period_groups.emplace_back( | ||
933 | Kv1PeriodGroup::Key( | ||
934 | data_owner_code, | ||
935 | period_group_code), | ||
936 | description); | ||
937 | } | ||
938 | |||
939 | void Kv1Parser::parseSpecificDay() { | ||
940 | auto data_owner_code = eatString("SPECDAY.DataOwnerCode", true, 10); | ||
941 | auto specific_day_code = eatString("SPECDAY.SpecificDayCode", true, 10); | ||
942 | auto name = eatString("SPECDAY.Name", true, 50); | ||
943 | auto description = eatString("SPECDAY.Description", false, 255); | ||
944 | if (!record_errors.empty()) return; | ||
945 | |||
946 | records.specific_days.emplace_back( | ||
947 | Kv1SpecificDay::Key( | ||
948 | data_owner_code, | ||
949 | specific_day_code), | ||
950 | name, | ||
951 | description); | ||
952 | } | ||
953 | |||
954 | void Kv1Parser::parseTimetableVersion() { | ||
955 | auto data_owner_code = eatString("TIVE.DataOwnerCode", true, 10); | ||
956 | auto organizational_unit_code = eatString("TIVE.OrganizationalUnitCode", true, 10); | ||
957 | auto timetable_version_code = eatString("TIVE.TimetableVersionCode", true, 10); | ||
958 | auto period_group_code = eatString("TIVE.PeriodGroupCode", true, 10); | ||
959 | auto specific_day_code = eatString("TIVE.SpecificDayCode", true, 10); | ||
960 | auto valid_from_raw = eatString("TIVE.ValidFrom", true, 10); | ||
961 | auto timetable_version_type = eatString("TIVE.TimetableVersionType", true, 10); | ||
962 | auto valid_thru_raw = eatString("TIVE.ValidThru", false, 10); | ||
963 | auto description = eatString("TIVE.Description", false, 255); | ||
964 | if (!record_errors.empty()) return; | ||
965 | |||
966 | auto valid_from = parseYyyymmdd(valid_from_raw); | ||
967 | if (!valid_from) | ||
968 | record_errors.push_back("TIVE.ValidFrom has invalid format, should be YYYY-MM-DD"); | ||
969 | std::optional<std::chrono::year_month_day> valid_thru; | ||
970 | if (!valid_thru_raw.empty()) { | ||
971 | valid_thru = parseYyyymmdd(valid_thru_raw); | ||
972 | if (!valid_thru) { | ||
973 | record_errors.push_back("TIVE.ValidFrom has invalid format, should be YYYY-MM-DD"); | ||
974 | } | ||
975 | } | ||
976 | if (!description.empty()) | ||
977 | record_errors.push_back("TIVE.Description should be empty"); | ||
978 | if (!record_errors.empty()) return; | ||
979 | |||
980 | records.timetable_versions.emplace_back( | ||
981 | Kv1TimetableVersion::Key( | ||
982 | data_owner_code, | ||
983 | organizational_unit_code, | ||
984 | timetable_version_code, | ||
985 | period_group_code, | ||
986 | specific_day_code), | ||
987 | *valid_from, | ||
988 | timetable_version_type, | ||
989 | valid_thru, | ||
990 | description); | ||
991 | } | ||
992 | |||
993 | void Kv1Parser::parsePublicJourney() { | ||
994 | auto data_owner_code = eatString ("PUJO.DataOwnerCode", true, 10); | ||
995 | auto timetable_version_code = eatString ("PUJO.TimetableVersionCode", true, 10); | ||
996 | auto organizational_unit_code = eatString ("PUJO.OrganizationalUnitCode", true, 10); | ||
997 | auto period_group_code = eatString ("PUJO.PeriodGroupCode", true, 10); | ||
998 | auto specific_day_code = eatString ("PUJO.SpecificDayCode", true, 10); | ||
999 | auto day_type = eatString ("PUJO.DayType", true, 7); | ||
1000 | auto line_planning_number = eatString ("PUJO.LinePlanningNumber", true, 10); | ||
1001 | auto journey_number = eatNumber ("PUJO.JourneyNumber", true, 6); | ||
1002 | auto time_demand_group_code = eatString ("PUJO.TimeDemandGroupCode", true, 10); | ||
1003 | auto journey_pattern_code = eatString ("PUJO.JourneyPatternCode", true, 10); | ||
1004 | auto departure_time_raw = eatString ("PUJO.DepartureTime", true, 8); | ||
1005 | auto wheelchair_accessible = eatString ("PUJO.WheelChairAccessible", true, 13); | ||
1006 | auto data_owner_is_operator = eatBoolean("PUJO.DataOwnerIsOperator", true ); | ||
1007 | auto planned_monitored = eatBoolean("PUJO.PlannedMonitored", true ); | ||
1008 | auto product_formula_type = eatNumber ("PUJO.ProductFormulaType", false, 4); | ||
1009 | auto show_flexible_trip = eatString ("PUJO.ShowFlexibleTrip", false, 8); | ||
1010 | if (!record_errors.empty()) return; | ||
1011 | |||
1012 | auto departure_time = parseHhmmss(departure_time_raw); | ||
1013 | if (!departure_time) | ||
1014 | record_errors.push_back("PUJO.DepartureTime has a bad format"); | ||
1015 | if (*journey_number < 0 || *journey_number > 999'999) | ||
1016 | record_errors.push_back("PUJO.JourneyNumber should be within the range [0-999999]"); | ||
1017 | if (*journey_number != static_cast<int>(*journey_number)) | ||
1018 | record_errors.push_back("PUJO.JourneyNumber should be an integer"); | ||
1019 | if (product_formula_type && *product_formula_type != static_cast<short>(*product_formula_type)) | ||
1020 | record_errors.push_back("PUJO.ProductFormulaType should be an integer"); | ||
1021 | if (wheelchair_accessible != "ACCESSIBLE" && wheelchair_accessible != "NOTACCESSIBLE" && wheelchair_accessible != "UNKNOWN") | ||
1022 | record_errors.push_back("PUJO.WheelChairAccessible should be in BISON E3 values [ACCESSIBLE, NOTACCESSIBLE, UNKNOWN]"); | ||
1023 | if (!show_flexible_trip.empty() && show_flexible_trip != "TRUE" && | ||
1024 | show_flexible_trip != "FALSE" && show_flexible_trip != "REALTIME") | ||
1025 | record_errors.push_back("PUJO.ShowFlexibleTrip should be in BISON E21 values [TRUE, FALSE, REALTIME]"); | ||
1026 | if (!record_errors.empty()) return; | ||
1027 | |||
1028 | records.public_journeys.emplace_back( | ||
1029 | Kv1PublicJourney::Key( | ||
1030 | data_owner_code, | ||
1031 | timetable_version_code, | ||
1032 | organizational_unit_code, | ||
1033 | period_group_code, | ||
1034 | specific_day_code, | ||
1035 | day_type, | ||
1036 | line_planning_number, | ||
1037 | static_cast<int>(*journey_number)), | ||
1038 | time_demand_group_code, | ||
1039 | journey_pattern_code, | ||
1040 | *departure_time, | ||
1041 | wheelchair_accessible, | ||
1042 | *data_owner_is_operator, | ||
1043 | *planned_monitored, | ||
1044 | product_formula_type, | ||
1045 | show_flexible_trip); | ||
1046 | } | ||
1047 | |||
1048 | void Kv1Parser::parsePeriodGroupValidity() { | ||
1049 | auto data_owner_code = eatString("PEGRVAL.DataOwnerCode", true, 10); | ||
1050 | auto organizational_unit_code = eatString("PEGRVAL.OrganizationalUnitCode", true, 10); | ||
1051 | auto period_group_code = eatString("PEGRVAL.PeriodGroupCode", true, 10); | ||
1052 | auto valid_from_raw = eatString("PEGRVAL.ValidFrom", true, 10); | ||
1053 | auto valid_thru_raw = eatString("PEGRVAL.ValidThru", true, 10); | ||
1054 | if (!record_errors.empty()) return; | ||
1055 | |||
1056 | auto valid_from = parseYyyymmdd(valid_from_raw); | ||
1057 | auto valid_thru = parseYyyymmdd(valid_thru_raw); | ||
1058 | if (!valid_from) | ||
1059 | record_errors.push_back("PEGRVAL.ValidFrom has invalid format, should be YYYY-MM-DD"); | ||
1060 | if (!valid_thru) | ||
1061 | record_errors.push_back("PEGRVAL.ValidThru has invalid format, should be YYYY-MM-DD"); | ||
1062 | if (!record_errors.empty()) return; | ||
1063 | |||
1064 | records.period_group_validities.emplace_back( | ||
1065 | Kv1PeriodGroupValidity::Key( | ||
1066 | data_owner_code, | ||
1067 | organizational_unit_code, | ||
1068 | period_group_code, | ||
1069 | *valid_from), | ||
1070 | *valid_thru); | ||
1071 | } | ||
1072 | |||
1073 | void Kv1Parser::parseExceptionalOperatingDay() { | ||
1074 | auto data_owner_code = eatString("EXCOPDAY.DataOwnerCode", true, 10); | ||
1075 | auto organizational_unit_code = eatString("EXCOPDAY.OrganizationalUnitCode", true, 10); | ||
1076 | auto valid_date_raw = eatString("EXCOPDAY.ValidDate", true, 23); | ||
1077 | auto day_type_as_on = eatString("EXCOPDAY.DayTypeAsOn", true, 7); | ||
1078 | auto specific_day_code = eatString("EXCOPDAY.SpecificDayCode", true, 10); | ||
1079 | auto period_group_code = eatString("EXCOPDAY.PeriodGroupCode", false, 10); | ||
1080 | auto description = eatString("EXCOPDAY.Description", false, 255); | ||
1081 | if (!record_errors.empty()) return; | ||
1082 | |||
1083 | std::string_view error; | ||
1084 | auto valid_date = parseDateTime(valid_date_raw, amsterdam, &error); | ||
1085 | if (!valid_date) { | ||
1086 | record_errors.push_back(std::format("EXCOPDAY.ValidDate has an bad format (value: {}): {}", valid_date_raw, error)); | ||
1087 | return; | ||
1088 | } | ||
1089 | |||
1090 | records.exceptional_operating_days.emplace_back( | ||
1091 | Kv1ExceptionalOperatingDay::Key( | ||
1092 | data_owner_code, | ||
1093 | organizational_unit_code, | ||
1094 | *valid_date), | ||
1095 | day_type_as_on, | ||
1096 | specific_day_code, | ||
1097 | period_group_code, | ||
1098 | description); | ||
1099 | } | ||
1100 | |||
1101 | void Kv1Parser::parseScheduleVersion() { | ||
1102 | auto data_owner_code = eatString("SCHEDVERS.DataOwnerCode", true, 10); | ||
1103 | auto organizational_unit_code = eatString("SCHEDVERS.OrganizationalUnitCode", true, 10); | ||
1104 | auto schedule_code = eatString("SCHEDVERS.ScheduleCode", true, 10); | ||
1105 | auto schedule_type_code = eatString("SCHEDVERS.ScheduleTypeCode", true, 10); | ||
1106 | auto valid_from_raw = eatString("SCHEDVERS.ValidFrom", true, 10); | ||
1107 | auto valid_thru_raw = eatString("SCHEDVERS.ValidThru", false, 10); | ||
1108 | auto description = eatString("SCHEDVERS.Description", false, 255); | ||
1109 | if (!record_errors.empty()) return; | ||
1110 | |||
1111 | auto valid_from = parseYyyymmdd(valid_from_raw); | ||
1112 | if (!valid_from) | ||
1113 | record_errors.push_back("SCHEDVERS.ValidFrom has invalid format, should be YYYY-MM-DD"); | ||
1114 | std::optional<std::chrono::year_month_day> valid_thru; | ||
1115 | if (!valid_thru_raw.empty()) { | ||
1116 | valid_thru = parseYyyymmdd(valid_thru_raw); | ||
1117 | if (!valid_thru) { | ||
1118 | record_errors.push_back("SCHEDVERS.ValidFrom has invalid format, should be YYYY-MM-DD"); | ||
1119 | } | ||
1120 | } | ||
1121 | if (!description.empty()) | ||
1122 | record_errors.push_back("SCHEDVERS.Description should be empty"); | ||
1123 | if (!record_errors.empty()) return; | ||
1124 | |||
1125 | records.schedule_versions.emplace_back( | ||
1126 | Kv1ScheduleVersion::Key( | ||
1127 | data_owner_code, | ||
1128 | organizational_unit_code, | ||
1129 | schedule_code, | ||
1130 | schedule_type_code), | ||
1131 | *valid_from, | ||
1132 | valid_thru, | ||
1133 | description); | ||
1134 | } | ||
1135 | |||
1136 | void Kv1Parser::parsePublicJourneyPassingTimes() { | ||
1137 | auto data_owner_code = eatString ("PUJOPASS.DataOwnerCode", true, 10); | ||
1138 | auto organizational_unit_code = eatString ("PUJOPASS.OrganizationalUnitCode", true, 10); | ||
1139 | auto schedule_code = eatString ("PUJOPASS.ScheduleCode", true, 10); | ||
1140 | auto schedule_type_code = eatString ("PUJOPASS.ScheduleTypeCode", true, 10); | ||
1141 | auto line_planning_number = eatString ("PUJOPASS.LinePlanningNumber", true, 10); | ||
1142 | auto journey_number = eatNumber ("PUJOPASS.JourneyNumber", true, 6); | ||
1143 | auto stop_order = eatNumber ("PUJOPASS.StopOrder", true, 4); | ||
1144 | auto journey_pattern_code = eatString ("PUJOPASS.JourneyPatternCode", true, 10); | ||
1145 | auto user_stop_code = eatString ("PUJOPASS.UserStopCode", true, 10); | ||
1146 | auto target_arrival_time_raw = eatString ("PUJOPASS.TargetArrivalTime", false, 8); | ||
1147 | auto target_departure_time_raw = eatString ("PUJOPASS.TargetDepartureTime", false, 8); | ||
1148 | auto wheelchair_accessible = eatString ("PUJOPASS.WheelChairAccessible", true, 13); | ||
1149 | auto data_owner_is_operator = eatBoolean("PUJOPASS.DataOwnerIsOperator", true ); | ||
1150 | auto planned_monitored = eatBoolean("PUJOPASS.PlannedMonitored", true ); | ||
1151 | auto product_formula_type = eatNumber ("PUJOPASS.ProductFormulaType", false, 4); | ||
1152 | auto show_flexible_trip = eatString ("PUJOPASS.ShowFlexibleTrip", false, 8); | ||
1153 | if (!record_errors.empty()) return; | ||
1154 | |||
1155 | if (*journey_number < 0 || *journey_number > 999'999) | ||
1156 | record_errors.push_back("PUJOPASS.JourneyNumber should be within the range [0-999999]"); | ||
1157 | if (*journey_number != static_cast<int>(*journey_number)) | ||
1158 | record_errors.push_back("PUJOPASS.JourneyNumber should be an integer"); | ||
1159 | if (*stop_order != static_cast<short>(*stop_order)) | ||
1160 | record_errors.push_back("PUJOPASS.StopOrder should be an integer"); | ||
1161 | if (product_formula_type && *product_formula_type != static_cast<short>(*product_formula_type)) | ||
1162 | record_errors.push_back("PUJOPASS.ProductFormulaType should be an integer"); | ||
1163 | if (wheelchair_accessible != "ACCESSIBLE" && wheelchair_accessible != "NOTACCESSIBLE" && wheelchair_accessible != "UNKNOWN") | ||
1164 | record_errors.push_back("PUJOPASS.WheelChairAccessible should be in BISON E3 values [ACCESSIBLE, NOTACCESSIBLE, UNKNOWN]"); | ||
1165 | if (!show_flexible_trip.empty() && show_flexible_trip != "TRUE" && | ||
1166 | show_flexible_trip != "FALSE" && show_flexible_trip != "REALTIME") | ||
1167 | record_errors.push_back("PUJOPASS.ShowFlexibleTrip should be in BISON E21 values [TRUE, FALSE, REALTIME]"); | ||
1168 | std::optional<std::chrono::hh_mm_ss<std::chrono::seconds>> target_arrival_time; | ||
1169 | if (!target_arrival_time_raw.empty()) { | ||
1170 | target_arrival_time = parseHhmmss(target_arrival_time_raw); | ||
1171 | if (!target_arrival_time) { | ||
1172 | record_errors.push_back("PUJOPASS.TargetArrivalTime has invalid format, should be HH:MM:SS"); | ||
1173 | } | ||
1174 | } | ||
1175 | std::optional<std::chrono::hh_mm_ss<std::chrono::seconds>> target_departure_time; | ||
1176 | if (!target_departure_time_raw.empty()) { | ||
1177 | target_departure_time = parseHhmmss(target_departure_time_raw); | ||
1178 | if (!target_departure_time) { | ||
1179 | record_errors.push_back("PUJOPASS.TargetDepartureTime has invalid format, should be HH:MM:SS"); | ||
1180 | } | ||
1181 | } | ||
1182 | if (!record_errors.empty()) return; | ||
1183 | |||
1184 | records.public_journey_passing_times.emplace_back( | ||
1185 | Kv1PublicJourneyPassingTimes::Key( | ||
1186 | data_owner_code, | ||
1187 | organizational_unit_code, | ||
1188 | schedule_code, | ||
1189 | schedule_type_code, | ||
1190 | line_planning_number, | ||
1191 | static_cast<int>(*journey_number), | ||
1192 | static_cast<short>(*stop_order)), | ||
1193 | journey_pattern_code, | ||
1194 | user_stop_code, | ||
1195 | target_arrival_time, | ||
1196 | target_departure_time, | ||
1197 | wheelchair_accessible, | ||
1198 | *data_owner_is_operator, | ||
1199 | *planned_monitored, | ||
1200 | product_formula_type, | ||
1201 | show_flexible_trip); | ||
1202 | } | ||
1203 | |||
1204 | void Kv1Parser::parseOperatingDay() { | ||
1205 | auto data_owner_code = eatString("OPERDAY.DataOwnerCode", true, 10); | ||
1206 | auto organizational_unit_code = eatString("OPERDAY.OrganizationalUnitCode", true, 10); | ||
1207 | auto schedule_code = eatString("OPERDAY.ScheduleCode", true, 10); | ||
1208 | auto schedule_type_code = eatString("OPERDAY.ScheduleTypeCode", true, 10); | ||
1209 | auto valid_date_raw = eatString("OPERDAY.ValidDate", true, 10); | ||
1210 | auto description = eatString("OPERDAY.Description", false, 255); | ||
1211 | if (!record_errors.empty()) return; | ||
1212 | |||
1213 | auto valid_date = parseYyyymmdd(valid_date_raw); | ||
1214 | if (!valid_date) | ||
1215 | record_errors.push_back("OPERDAY.ValidDate has invalid format, should be YYYY-MM-DD"); | ||
1216 | if (!record_errors.empty()) return; | ||
1217 | |||
1218 | records.operating_days.emplace_back( | ||
1219 | Kv1OperatingDay::Key( | ||
1220 | data_owner_code, | ||
1221 | organizational_unit_code, | ||
1222 | schedule_code, | ||
1223 | schedule_type_code, | ||
1224 | *valid_date), | ||
1225 | description); | ||
1226 | } | ||
1227 | |||
1228 | const std::unordered_map<std::string_view, Kv1Parser::ParseFunc> Kv1Parser::type_parsers{ | ||
1229 | { "ORUN", &Kv1Parser::parseOrganizationalUnit }, | ||
1230 | { "ORUNORUN", &Kv1Parser::parseHigherOrganizationalUnit }, | ||
1231 | { "USRSTOP", &Kv1Parser::parseUserStopPoint }, | ||
1232 | { "USRSTAR", &Kv1Parser::parseUserStopArea }, | ||
1233 | { "TILI", &Kv1Parser::parseTimingLink }, | ||
1234 | { "LINK", &Kv1Parser::parseLink }, | ||
1235 | { "LINE", &Kv1Parser::parseLine }, | ||
1236 | { "DEST", &Kv1Parser::parseDestination }, | ||
1237 | { "JOPA", &Kv1Parser::parseJourneyPattern }, | ||
1238 | { "CONFINREL", &Kv1Parser::parseConcessionFinancerRelation }, | ||
1239 | { "CONAREA", &Kv1Parser::parseConcessionArea }, | ||
1240 | { "FINANCER", &Kv1Parser::parseFinancer }, | ||
1241 | { "JOPATILI", &Kv1Parser::parseJourneyPatternTimingLink }, | ||
1242 | { "POINT", &Kv1Parser::parsePoint }, | ||
1243 | { "POOL", &Kv1Parser::parsePointOnLink }, | ||
1244 | { "ICON", &Kv1Parser::parseIcon }, | ||
1245 | { "NOTICE", &Kv1Parser::parseNotice }, | ||
1246 | { "NTCASSGNM", &Kv1Parser::parseNoticeAssignment }, | ||
1247 | { "TIMDEMGRP", &Kv1Parser::parseTimeDemandGroup }, | ||
1248 | { "TIMDEMRNT", &Kv1Parser::parseTimeDemandGroupRunTime }, | ||
1249 | { "PEGR", &Kv1Parser::parsePeriodGroup }, | ||
1250 | { "SPECDAY", &Kv1Parser::parseSpecificDay }, | ||
1251 | { "TIVE", &Kv1Parser::parseTimetableVersion }, | ||
1252 | { "PUJO", &Kv1Parser::parsePublicJourney }, | ||
1253 | { "PEGRVAL", &Kv1Parser::parsePeriodGroupValidity }, | ||
1254 | { "EXCOPDAY", &Kv1Parser::parseExceptionalOperatingDay }, | ||
1255 | { "SCHEDVERS", &Kv1Parser::parseScheduleVersion }, | ||
1256 | { "PUJOPASS", &Kv1Parser::parsePublicJourneyPassingTimes }, | ||
1257 | { "OPERDAY", &Kv1Parser::parseOperatingDay }, | ||
1258 | }; | ||