aboutsummaryrefslogtreecommitdiffstats
path: root/lib/libtmi8/src/kv1_parser.cpp
diff options
context:
space:
mode:
authorLibravatar Rutger Broekhoff2024-05-02 20:27:40 +0200
committerLibravatar Rutger Broekhoff2024-05-02 20:27:40 +0200
commit17a3ea880402338420699e03bcb24181e4ff3924 (patch)
treeda666ef91e0b60d20aa0b01529644c136fd1f4ab /lib/libtmi8/src/kv1_parser.cpp
downloadoeuf-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.cpp1258
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
5using rune = uint32_t;
6
7static 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.
46static 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
57Kv1Parser::Kv1Parser(std::vector<Kv1Token> tokens, Kv1Records &parse_into)
58 : tokens(std::move(tokens)),
59 records(parse_into)
60{}
61
62bool Kv1Parser::atEnd() const {
63 return pos >= tokens.size();
64}
65
66void Kv1Parser::eatRowEnds() {
67 while (!atEnd() && tokens[pos].type == KV1_TOKEN_ROW_END) pos++;
68}
69
70const Kv1Token *Kv1Parser::cur() const {
71 if (atEnd()) return nullptr;
72 return &tokens[pos];
73}
74
75const 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
89void 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
105static 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
113std::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
125static inline size_t countDigits(long x) {
126 size_t digits = 0;
127 while (x != 0) { digits++; x /= 10; }
128 return digits;
129}
130
131std::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
159static inline bool isHexDigit(char c) {
160 return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F');
161}
162
163static 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
169static 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
181std::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
193std::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
225std::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
232std::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
238std::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
244std::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
250std::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
256std::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
274void Kv1Parser::eatRestOfRow() {
275 while (!atEnd() && cur()->type != KV1_TOKEN_ROW_END) pos++;
276}
277
278void 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
306void 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
323static inline bool isDigit(char c) {
324 return c >= '0' && c <= '9';
325}
326
327// Parse a string of the format YYYY-MM-DD.
328static 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.
342static 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
359static 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
419void 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
440void 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
478void 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
497void 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
514void 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
534void 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
574void 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
621void 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
645void 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
660void 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
673void 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
686void 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
735void 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
764void 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
792void 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
810void 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
823void 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
873void 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
888void 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
926void 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
939void 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
954void 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
993void 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
1048void 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
1073void 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
1101void 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
1136void 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
1204void 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
1228const 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};