// vim:set sw=2 ts=2 sts et: // // Copyright 2024 Rutger Broekhoff. Licensed under the EUPL. #include using rune = uint32_t; static size_t decodeUtf8Cp(std::string_view s, rune *dest = nullptr) { rune res = 0xFFFD; size_t length = 1; if (s.size() == 0) return 0; const uint8_t *b = reinterpret_cast(s.data()); if (!(b[0] & 0x80)) res = static_cast(b[0]); else if ((b[0] & 0xE0) == 0xC0) { length = 2; if (s.size() >= 2 && (b[1] & 0xC0) == 0x80) { res = static_cast(b[0] & ~0xC0) << 6; res |= static_cast(b[1] & ~0x80); } } else if ((b[0] & 0xF0) == 0xE0) { length = 3; if (s.size() >= 3 && (b[1] & 0xC0) == 0x80 && (b[2] & 0xC0) == 0x80) { res = static_cast(b[0] & ~0xE0) << 12; res |= static_cast(b[1] & ~0x80) << 6; res |= static_cast(b[2] & ~0x80); } } else if (b[0] == 0xF0) { length = 4; if (s.size() >= 4 && (b[1] & 0xC0) == 0x80 && (b[2] & 0xC0) == 0x80 && (b[3] & 0xC0) == 0x80) { res = static_cast(b[0] & ~0xF0) << 18; res |= static_cast(b[1] & ~0x80) << 12; res |= static_cast(b[2] & ~0x80) << 6; res |= static_cast(b[3] & ~0x80); } } if (dest) *dest = res; return length; } // Counts the number of codepoints in a valid UTF-8 string. Returns SIZE_MAX if // the string contains invalid UTF-8 codepoints. static size_t stringViewLengthUtf8(std::string_view sv) { size_t codepoints = 0; while (sv.size() > 0) { size_t codepoint_size = decodeUtf8Cp(sv); if (codepoint_size == 0) return SIZE_MAX; codepoints++; sv = sv.substr(codepoint_size); } return codepoints; } Kv1Parser::Kv1Parser(std::vector tokens, Kv1Records &parse_into) : tokens(std::move(tokens)), records(parse_into) {} bool Kv1Parser::atEnd() const { return pos >= tokens.size(); } void Kv1Parser::eatRowEnds() { while (!atEnd() && tokens[pos].type == KV1_TOKEN_ROW_END) pos++; } const Kv1Token *Kv1Parser::cur() const { if (atEnd()) return nullptr; return &tokens[pos]; } const std::string *Kv1Parser::eatCell(std::string_view parsing_what) { const Kv1Token *tok = cur(); if (!tok) { record_errors.push_back(std::format("Expected cell but got end of file when parsing {}", parsing_what)); return nullptr; } if (tok->type == KV1_TOKEN_ROW_END) { record_errors.push_back(std::format("Expected cell but got end of row when parsing {}", parsing_what)); return nullptr; } pos++; return &tok->data; } void Kv1Parser::requireString(std::string_view field, bool mandatory, size_t max_length, std::string_view value) { if (value.empty() && mandatory) { record_errors.push_back(std::format("{} has length zero but is required", field)); return; } size_t codepoints = stringViewLengthUtf8(value); if (codepoints == SIZE_MAX) { global_errors.push_back(std::format("{} contains invalid UTF-8 code points", field)); return; } if (codepoints > max_length) { record_errors.push_back(std::format("{} has length ({}) that is greater than maximum length ({})", field, value.size(), max_length)); } } static inline std::optional parseBoolean(std::string_view src) { if (src == "1") return true; if (src == "0") return false; if (src == "true") return true; if (src == "false") return false; return std::nullopt; } std::optional Kv1Parser::requireBoolean(std::string_view field, bool mandatory, std::string_view value) { if (value.empty()) { if (mandatory) record_errors.push_back(std::format("{} is required, but has no value", field)); return std::nullopt; } auto parsed = parseBoolean(value); if (!parsed.has_value()) record_errors.push_back(std::format("{} should have value \"1\", \"0\", \"true\" or \"false\"", field)); return parsed; } static inline size_t countDigits(long x) { size_t digits = 0; while (x != 0) { digits++; x /= 10; } return digits; } std::optional Kv1Parser::requireNumber(std::string_view field, bool mandatory, size_t max_digits, std::string_view value) { if (value.empty()) { if (mandatory) record_errors.push_back(std::format("{} has no value but is required", field)); return std::nullopt; } double parsed; auto [ptr, ec] = std::from_chars(value.data(), value.data() + value.size(), parsed, std::chars_format::fixed); if (ec != std::errc()) { record_errors.push_back(std::format("{} has a bad value that cannot be parsed as a number", field)); return std::nullopt; } if (ptr != value.data() + value.size()) { record_errors.push_back(std::format("{} contains characters that were not parsed as a number", field)); return std::nullopt; } size_t digits = countDigits(static_cast(parsed)); if (digits > max_digits) { record_errors.push_back(std::format("{} contains more digits (in the integral part) ({}) than allowed ({})", field, digits, max_digits)); return std::nullopt; } return parsed; } static inline bool isHexDigit(char c) { return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F'); } static inline uint8_t fromHex(char c) { if (c >= '0' && c <= '9') return static_cast(c - '0'); else if (c >= 'A' && c <= 'F') return static_cast(c - 'A' + 10); return 0; } static std::optional parseRgbColor(std::string_view src) { bool valid = src.size() == 6 && isHexDigit(src[0]) && isHexDigit(src[1]) && isHexDigit(src[2]) && isHexDigit(src[3]) && isHexDigit(src[4]) && isHexDigit(src[5]); if (!valid) return std::nullopt; uint8_t r = static_cast(fromHex(src[0]) << 4) + fromHex(src[1]); uint8_t g = static_cast(fromHex(src[2]) << 4) + fromHex(src[3]); uint8_t b = static_cast(fromHex(src[4]) << 4) + fromHex(src[5]); return RgbColor{ r, g, b }; } std::optional Kv1Parser::requireRgbColor(std::string_view field, bool mandatory, std::string_view value) { if (value.empty()) { if (mandatory) record_errors.push_back(std::format("{} is required, but has no value", field)); return std::nullopt; } auto parsed = parseRgbColor(value); if (!parsed.has_value()) record_errors.push_back(std::format("{} should be an RGB color, i.e. a sequence of six hexadecimally represented nibbles", field)); return parsed; } std::optional Kv1Parser::requireRdCoord(std::string_view field, bool mandatory, size_t min_digits, std::string_view value) { if (value.empty()) { if (mandatory) record_errors.push_back(std::format("{} is required, but has no value", field)); return std::nullopt; } if (value.size() > 15) { record_errors.push_back(std::format("{} may not have more than 15 characters", field)); return std::nullopt; } double parsed; auto [ptr, ec] = std::from_chars(value.data(), value.data() + value.size(), parsed, std::chars_format::fixed); if (ec != std::errc()) { record_errors.push_back(std::format("{} has a bad value that cannot be parsed as a number", field)); return std::nullopt; } if (ptr != value.data() + value.size()) { record_errors.push_back(std::format("{} contains characters that were not parsed as a number", field)); return std::nullopt; } size_t digits = countDigits(static_cast(parsed)); if (digits < min_digits) { record_errors.push_back(std::format("{} contains less digits (in the integral part) ({}) than required ({}) [value: {}]", field, digits, min_digits, value)); return std::nullopt; } return parsed; } std::string Kv1Parser::eatString(std::string_view field, bool mandatory, size_t max_length) { auto value = eatCell(field); if (!record_errors.empty()) return {}; requireString(field, mandatory, max_length, *value); return std::move(*value); } std::optional Kv1Parser::eatBoolean(std::string_view field, bool mandatory) { auto value = eatCell(field); if (!record_errors.empty()) return {}; return requireBoolean(field, mandatory, *value); } std::optional Kv1Parser::eatNumber(std::string_view field, bool mandatory, size_t max_digits) { auto value = eatCell(field); if (!record_errors.empty()) return {}; return requireNumber(field, mandatory, max_digits, *value); } std::optional Kv1Parser::eatRgbColor(std::string_view field, bool mandatory) { auto value = eatCell(field); if (!record_errors.empty()) return {}; return requireRgbColor(field, mandatory, *value); } std::optional Kv1Parser::eatRdCoord(std::string_view field, bool mandatory, size_t min_digits) { auto value = eatCell(field); if (!record_errors.empty()) return {}; return requireRdCoord(field, mandatory, min_digits, *value); } std::string Kv1Parser::parseHeader() { auto record_type = eatString("
.Recordtype", true, 10); auto version_number = eatString("
.VersionNumber", true, 2); auto implicit_explicit = eatString("
.Implicit/Explicit", true, 1); if (!record_errors.empty()) return {}; if (version_number != "1") { record_errors.push_back("
.VersionNumber should be 1"); return ""; } if (implicit_explicit != "I") { record_errors.push_back("
.Implicit/Explicit should be 'I'"); return ""; } return record_type; } void Kv1Parser::eatRestOfRow() { while (!atEnd() && cur()->type != KV1_TOKEN_ROW_END) pos++; } void Kv1Parser::parse() { while (!atEnd()) { eatRowEnds(); if (atEnd()) return; std::string record_type = parseHeader(); if (!record_errors.empty()) break; if (!type_parsers.contains(record_type)) { warns.push_back(std::format("Recordtype ({}) is bad or names a record type that this program cannot process", record_type)); eatRestOfRow(); continue; } ParseFunc parseType = Kv1Parser::type_parsers.at(record_type); (this->*parseType)(); if (cur() && cur()->type != KV1_TOKEN_ROW_END) { record_errors.push_back(std::format("Parser function for Recordtype ({}) did not eat all record fields", record_type)); eatRestOfRow(); } if (!record_errors.empty()) { global_errors.insert(global_errors.end(), record_errors.begin(), record_errors.end()); record_errors.clear(); } } } void Kv1Parser::parseOrganizationalUnit() { auto data_owner_code = eatString("ORUN.DataOwnerCode", true, 10); auto organizational_unit_code = eatString("ORUN.OrganizationalUnitCode", true, 10); auto name = eatString("ORUN.Name", true, 50); auto organizational_unit_type = eatString("ORUN.OrganizationalUnitType", true, 10); auto description = eatString("ORUN.Description", false, 255); if (!record_errors.empty()) return; records.organizational_units.emplace_back( Kv1OrganizationalUnit::Key( data_owner_code, organizational_unit_code), name, organizational_unit_type, description); } static inline bool isDigit(char c) { return c >= '0' && c <= '9'; } // Parse a string of the format YYYY-MM-DD. static std::optional parseYyyymmdd(std::string_view src) { bool valid = src.size() == 10 && isDigit(src[0]) && isDigit(src[1]) && isDigit(src[2]) && isDigit(src[3]) && src[4] == '-' && isDigit(src[5]) && isDigit(src[6]) && src[7] == '-' && isDigit(src[8]) && isDigit(src[9]); if (!valid) return std::nullopt; int year = (src[0] - '0') * 1000 + (src[1] - '0') * 100 + (src[2] - '0') * 10 + src[3] - '0'; int month = (src[5] - '0') * 10 + src[6] - '0'; int day = (src[8] - '0') * 10 + src[9] - '0'; return std::chrono::year(year) / std::chrono::month(month) / std::chrono::day(day); } // Parse a string of the format HH:MM:SS. static std::optional> parseHhmmss(std::string_view src) { bool valid = src.size() == 8 && isDigit(src[0]) && isDigit(src[1]) && src[2] == ':' && isDigit(src[3]) && isDigit(src[4]) && src[5] == ':' && isDigit(src[6]) && isDigit(src[7]); if (!valid) return std::nullopt; int hh = (src[0] - '0') * 10 + src[1] - '0'; int mm = (src[3] - '0') * 10 + src[4] - '0'; int ss = (src[6] - '0') * 10 + src[7] - '0'; // The check for the hour not being greater than 32 comes from the fact the // specification explicitly allows hours greater than 23, noting that the // period 24:00-32:00 is equivalent to 00:00-08:00 in the next day, for // exploitation of two days. if (hh > 32 || mm > 59 || ss > 59) return std::nullopt; return std::chrono::hh_mm_ss(std::chrono::hours(hh) + std::chrono::minutes(mm) + std::chrono::seconds(ss)); } static std::optional parseDateTime(std::string_view src, const std::chrono::time_zone *amsterdam, std::string_view *error = nullptr) { #define ERROR(err) do { if (error) *error = err; return std::nullopt; } while (0) if (src.size() > 23) ERROR("timestamp string is too big"); if (src.size() < 17) ERROR("timestamp string is too small"); bool valid_year = isDigit(src[0]) && isDigit(src[1]) && isDigit(src[2]) && isDigit(src[3]); if (!valid_year) ERROR("year has bad format"); size_t month_off = src[4] == '-' ? 5 : 4; size_t day_off = src[month_off + 2] == '-' ? month_off + 3 : month_off + 2; size_t time_off = day_off + 2; if (src[time_off] != 'T' && src[time_off] != ' ') ERROR("missing date/time separator"); size_t tzd_off = time_off + 9; // For clarity, TZD stands for Time Zone Designator. It often takes the form // of Z (Zulu, UTC+00:00) or as an offset from UTC in hours and minutes, // formatted as +|-HH:MM (e.g. +01:00, -12:00). if (time_off + 8 >= src.size()) ERROR("bad format, not enough space for hh:mm:ss"); int year = (src[0] - '0') * 1000 + (src[1] - '0') * 100 + (src[2] - '0') * 10 + src[3] - '0'; int month = (src[month_off] - '0') * 10 + src[month_off + 1] - '0'; int day = (src[day_off] - '0') * 10 + src[day_off + 1] - '0'; int hour = (src[time_off + 1] - '0') * 10 + src[time_off + 2] - '0'; int minute = (src[time_off + 4] - '0') * 10 + src[time_off + 5] - '0'; int second = (src[time_off + 7] - '0') * 10 + src[time_off + 8] - '0'; auto date = std::chrono::year(year) / std::chrono::month(month) / std::chrono::day(day); auto time = std::chrono::hours(hour) + std::chrono::minutes(minute) + std::chrono::seconds(second); std::chrono::sys_seconds unix_start_of_day; if (tzd_off < src.size()) { unix_start_of_day = std::chrono::sys_days(date); } else { auto local_days = std::chrono::local_days(date); std::chrono::zoned_seconds zoned_start_of_day = std::chrono::zoned_time(amsterdam, local_days); unix_start_of_day = std::chrono::sys_seconds(zoned_start_of_day); } std::chrono::minutes offset(0); if (tzd_off + 1 == src.size() && src[tzd_off] != 'Z') { ERROR("bad TZD (missing Zulu indicator)"); } else if (tzd_off + 6 == src.size()) { bool valid_tzd = (src[tzd_off] == '+' || src[tzd_off] == '-') && isDigit(src[tzd_off + 1]) && isDigit(src[tzd_off + 2]) && src[tzd_off + 3] == ':' && isDigit(src[tzd_off + 4]) && isDigit(src[tzd_off + 5]); if (!valid_tzd) ERROR("bad offset TZD format (expected +|-hh:mm)"); int sign = src[tzd_off] == '-' ? -1 : 1; int tzd_hh = (src[tzd_off + 1] - '0') * 10 + src[tzd_off + 2] - '0'; int tzd_mm = (src[tzd_off + 3] - '0') * 10 + src[tzd_off + 4] - '0'; offset = sign * std::chrono::minutes(tzd_hh * 60 + tzd_mm); } else if (tzd_off < src.size()) { // There is a TZD but we literally have no clue how to parse it :/ ERROR("cannot parse TZD of unexpected length"); } return unix_start_of_day + time - offset; #undef ERROR } void Kv1Parser::parseHigherOrganizationalUnit() { auto data_owner_code = eatString("ORUNORUN.DataOwnerCode", true, 10); auto organizational_unit_code_parent = eatString("ORUNORUN.OrganizationalUnitCodeParent", true, 10); auto organizational_unit_code_child = eatString("ORUNORUN.OrganizationalUnitCodeChild", true, 10); auto valid_from_raw = eatString("ORUNORUN.ValidFrom", true, 10); if (!record_errors.empty()) return; auto valid_from = parseYyyymmdd(valid_from_raw); if (!valid_from) { record_errors.push_back("ORUNORUN.ValidFrom has invalid format, should be YYYY-MM-DD"); return; } records.higher_organizational_units.emplace_back( Kv1HigherOrganizationalUnit::Key( data_owner_code, organizational_unit_code_parent, organizational_unit_code_child, *valid_from)); } void Kv1Parser::parseUserStopPoint() { auto data_owner_code = eatString ("USRSTOP.DataOwnerCode", true, 10); auto user_stop_code = eatString ("USRSTOP.UserStopCode", true, 10); auto timing_point_code = eatString ("USRSTOP.TimingPointCode", false, 10); auto get_in = eatBoolean("USRSTOP.GetIn", true ); auto get_out = eatBoolean("USRSTOP.GetOut", true ); eatCell ("USRSTOP." ); auto name = eatString ("USRSTOP.Name", true, 50); auto town = eatString ("USRSTOP.Town", true, 50); auto user_stop_area_code = eatString ("USRSTOP.UserStopAreaCode", false, 10); auto stop_side_code = eatString ("USRSTOP.StopSideCode", true, 10); eatCell ("USRSTOP." ); eatCell ("USRSTOP." ); auto minimal_stop_time = eatNumber ("USRSTOP.MinimalStopTime", true, 5); auto stop_side_length = eatNumber ("USRSTOP.StopSideLength", false, 3); auto description = eatString ("USRSTOP.Description", false, 255); auto user_stop_type = eatString ("USRSTOP.UserStopType", true, 10); auto quay_code = eatString ("USRSTOP.QuayCode", false, 30); if (!record_errors.empty()) return; records.user_stop_points.emplace_back( Kv1UserStopPoint::Key( data_owner_code, user_stop_code), timing_point_code, *get_in, *get_out, name, town, user_stop_area_code, stop_side_code, *minimal_stop_time, stop_side_length, description, user_stop_type, quay_code); } void Kv1Parser::parseUserStopArea() { auto data_owner_code = eatString("USRSTAR.DataOwnerCode", true, 10); auto user_stop_area_code = eatString("USRSTAR.UserStopAreaCode", true, 10); auto name = eatString("USRSTAR.Name", true, 50); auto town = eatString("USRSTAR.Town", true, 50); eatCell ("USRSTAR." ); eatCell ("USRSTAR." ); auto description = eatString("USRSTAR.Description", false, 255); if (!record_errors.empty()) return; records.user_stop_areas.emplace_back( Kv1UserStopArea::Key( data_owner_code, user_stop_area_code), name, town, description); } void Kv1Parser::parseTimingLink() { auto data_owner_code = eatString("TILI.DataOwnerCode", true, 10); auto user_stop_code_begin = eatString("TILI.UserStopCodeBegin", true, 10); auto user_stop_code_end = eatString("TILI.UserStopCodeEnd", true, 10); auto minimal_drive_time = eatNumber("TILI.MinimalDriveTime", false, 5); auto description = eatString("TILI.Description", false, 255); if (!record_errors.empty()) return; records.timing_links.emplace_back( Kv1TimingLink::Key( data_owner_code, user_stop_code_begin, user_stop_code_end), minimal_drive_time, description); } void Kv1Parser::parseLink() { auto data_owner_code = eatString("LINK.DataOwnerCode", true, 10); auto user_stop_code_begin = eatString("LINK.UserStopCodeBegin", true, 10); auto user_stop_code_end = eatString("LINK.UserStopCodeEnd", true, 10); eatCell("LINK." ); auto distance = eatNumber("LINK.Distance", true, 6); auto description = eatString("LINK.Description", false, 255); auto transport_type = eatString("LINK.TransportType", true, 5); if (!record_errors.empty()) return; records.links.emplace_back( Kv1Link::Key( data_owner_code, user_stop_code_begin, user_stop_code_end, transport_type), *distance, description); } void Kv1Parser::parseLine() { auto data_owner_code = eatString ("LINE.DataOwnerCode", true, 10); auto line_planning_number = eatString ("LINE.LinePlanningNumber", true, 10); auto line_public_number = eatString ("LINE.LinePublicNumber", true, 4); auto line_name = eatString ("LINE.LineName", true, 50); auto line_ve_tag_number = eatNumber ("LINE.LineVeTagNumber", true, 3); auto description = eatString ("LINE.Description", false, 255); auto transport_type = eatString ("LINE.TransportType", true, 5); auto line_icon = eatNumber ("LINE.LineIcon", false, 4); auto line_color = eatRgbColor("LINE.LineColor", false ); auto line_text_color = eatRgbColor("LINE.LineTextColor", false ); if (!record_errors.empty()) return; // NOTE: This check, although it should be performed to comply with the // specification, is not actually honored by transit operators (such as // Connexxion) :/ That's enough reason to keep it disabled here for now. // if (*line_ve_tag_number < 0 || *line_ve_tag_number > 399) { // record_errors.push_back(std::format("LINE.LineVeTagNumber is out of range [0-399] with value {}", *line_ve_tag_number)); // return; // } if (*line_ve_tag_number != static_cast(*line_ve_tag_number)) record_errors.push_back("LINE.LineVeTagNumber should be an integer"); if (line_icon && *line_icon != static_cast(*line_icon)) record_errors.push_back("LINE.LineIcon should be an integer"); if (!record_errors.empty()) return; records.lines.emplace_back( Kv1Line::Key( data_owner_code, line_planning_number), line_public_number, line_name, static_cast(*line_ve_tag_number), description, transport_type, static_cast>(line_icon), line_color, line_text_color); } void Kv1Parser::parseDestination() { auto data_owner_code = eatString ("DEST.DataOwnerCode", true, 10); auto dest_code = eatString ("DEST.DestCode", true, 10); auto dest_name_full = eatString ("DEST.DestNameFull", true, 50); auto dest_name_main = eatString ("DEST.DestNameMain", true, 24); auto dest_name_detail = eatString ("DEST.DestNameDetail", false, 24); auto relevant_dest_name_detail = eatBoolean ("DEST.RelevantDestNameDetail", true ); auto dest_name_main_21 = eatString ("DEST.DestNameMain21", true, 21); auto dest_name_detail_21 = eatString ("DEST.DestNameDetail21", false, 21); auto dest_name_main_19 = eatString ("DEST.DestNameMain19", true, 19); auto dest_name_detail_19 = eatString ("DEST.DestNameDetail19", false, 19); auto dest_name_main_16 = eatString ("DEST.DestNameMain16", true, 16); auto dest_name_detail_16 = eatString ("DEST.DestNameDetail16", false, 16); auto dest_icon = eatNumber ("DEST.DestIcon", false, 4); auto dest_color = eatRgbColor("DEST.DestColor", false ); // NOTE: Deviating from the offical KV1 specification here. It specifies that // the maximum length for this field should be 30, but then proceeds to // specify that it should contain a RGB value comprising of three // hexadecimally encoded octets, i.e. six characters. We assume that the // latter is correct and the intended interpretation. auto dest_text_color = eatRgbColor("DEST.DestTextColor", false ); if (!record_errors.empty()) return; if (dest_icon && *dest_icon != static_cast(*dest_icon)) { record_errors.push_back("DEST.DestIcon should be an integer"); return; } records.destinations.emplace_back( Kv1Destination::Key( data_owner_code, dest_code), dest_name_full, dest_name_main, dest_name_detail, *relevant_dest_name_detail, dest_name_main_21, dest_name_detail_21, dest_name_main_19, dest_name_detail_19, dest_name_main_16, dest_name_detail_16, dest_icon, dest_color, dest_text_color); } void Kv1Parser::parseJourneyPattern() { auto data_owner_code = eatString("JOPA.DataOwnerCode", true, 10); auto line_planning_number = eatString("JOPA.LinePlanningNumber", true, 10); auto journey_pattern_code = eatString("JOPA.JourneyPatternCode", true, 10); auto journey_pattern_type = eatString("JOPA.JourneyPatternType", true, 10); auto direction = eatString("JOPA.Direction", true, 1); auto description = eatString("JOPA.Description", false, 255); if (!record_errors.empty()) return; if (direction != "1" && direction != "2" && direction != "A" && direction != "B") { record_errors.push_back("JOPA.Direction should be in [1, 2, A, B]"); return; } records.journey_patterns.emplace_back( Kv1JourneyPattern::Key( data_owner_code, line_planning_number, journey_pattern_code), journey_pattern_type, direction[0], description); } void Kv1Parser::parseConcessionFinancerRelation() { auto data_owner_code = eatString("CONFINREL.DataOwnerCode", true, 10); auto con_fin_rel_code = eatString("CONFINREL.ConFinRelCode", true, 10); auto concession_area_code = eatString("CONFINREL.ConcessionAreaCode", true, 10); auto financer_code = eatString("CONFINREL.FinancerCode", false, 10); if (!record_errors.empty()) return; records.concession_financer_relations.emplace_back( Kv1ConcessionFinancerRelation::Key( data_owner_code, con_fin_rel_code), concession_area_code, financer_code); } void Kv1Parser::parseConcessionArea() { auto data_owner_code = eatString("CONAREA.DataOwnerCode", true, 10); auto concession_area_code = eatString("CONAREA.ConcessionAreaCode", true, 10); auto description = eatString("CONAREA.Description", true, 255); if (!record_errors.empty()) return; records.concession_areas.emplace_back( Kv1ConcessionArea::Key( data_owner_code, concession_area_code), description); } void Kv1Parser::parseFinancer() { auto data_owner_code = eatString("FINANCER.DataOwnerCode", true, 10); auto financer_code = eatString("FINANCER.FinancerCode", true, 10); auto description = eatString("FINANCER.Description", true, 255); if (!record_errors.empty()) return; records.financers.emplace_back( Kv1Financer::Key( data_owner_code, financer_code), description); } void Kv1Parser::parseJourneyPatternTimingLink() { auto data_owner_code = eatString ("JOPATILI.DataOwnerCode", true, 10); auto line_planning_number = eatString ("JOPATILI.LinePlanningNumber", true, 10); auto journey_pattern_code = eatString ("JOPATILI.JourneyPatternCode", true, 10); auto timing_link_order = eatNumber ("JOPATILI.TimingLinkOrder", true, 3); auto user_stop_code_begin = eatString ("JOPATILI.UserStopCodeBegin", true, 10); auto user_stop_code_end = eatString ("JOPATILI.UserStopCodeEnd", true, 10); auto con_fin_rel_code = eatString ("JOPATILI.ConFinRelCode", true, 10); auto dest_code = eatString ("JOPATILI.DestCode", true, 10); eatCell ("JOPATILI." ); auto is_timing_stop = eatBoolean ("JOPATILI.IsTimingStop", true ); auto display_public_line = eatString ("JOPATILI.DisplayPublicLine", false, 4); auto product_formula_type = eatNumber ("JOPATILI.ProductFormulaType", false, 4); auto get_in = eatBoolean ("JOPATILI.GetIn", true ); auto get_out = eatBoolean ("JOPATILI.GetOut", true ); auto show_flexible_trip = eatString ("JOPATILI.ShowFlexibleTrip", false, 8); auto line_dest_icon = eatNumber ("JOPATILI.LineDestIcon", false, 4); auto line_dest_color = eatRgbColor("JOPATILI.LineDestColor", false ); auto line_dest_text_color = eatRgbColor("JOPATILI.LineDestTextColor", false ); if (!record_errors.empty()) return; if (line_dest_icon && *line_dest_icon != static_cast(*line_dest_icon)) record_errors.push_back("JOPATILI.LineDestIcon should be an integer"); if (!show_flexible_trip.empty() && show_flexible_trip != "TRUE" && show_flexible_trip != "FALSE" && show_flexible_trip != "REALTIME") record_errors.push_back("JOPATILI.ShowFlexibleTrip should be in BISON E21 values [TRUE, FALSE, REALTIME]"); if (!record_errors.empty()) return; records.journey_pattern_timing_links.emplace_back( Kv1JourneyPatternTimingLink::Key( data_owner_code, line_planning_number, journey_pattern_code, static_cast(*timing_link_order)), user_stop_code_begin, user_stop_code_end, con_fin_rel_code, dest_code, *is_timing_stop, display_public_line, product_formula_type, *get_in, *get_out, show_flexible_trip, line_dest_icon, line_dest_color, line_dest_text_color); } void Kv1Parser::parsePoint() { auto data_owner_code = eatString("POINT.DataOwnerCode", true, 10); auto point_code = eatString("POINT.PointCode", true, 10); eatCell ("POINT." ); auto point_type = eatString("POINT.PointType", true, 10); auto coordinate_system_type = eatString("POINT.CoordinateSystemType", true, 10); // NOTE: We deviate from the specification here once again. The specification // notes that LocationX_EW should contain 'at least 6 positions'. Assuming // that this is referring to the amount of digits, we have to lower this to // 4. Otherwise, some positions in the Netherlands and Belgium are // unrepresentable. auto location_x_ew = eatRdCoord("POINT.LocationX_EW", true, 4); auto location_y_ew = eatRdCoord("POINT.LocationX_EW", true, 6); auto location_z = eatRdCoord("POINT.LocationZ", false, 0); auto description = eatString ("POINT.Description", false, 255); if (!record_errors.empty()) return; records.points.emplace_back( Kv1Point::Key( std::move(data_owner_code), std::move(point_code)), std::move(point_type), std::move(coordinate_system_type), *location_x_ew, *location_y_ew, location_z, std::move(description)); } void Kv1Parser::parsePointOnLink() { auto data_owner_code = eatString("POOL.DataOwnerCode", true, 10); auto user_stop_code_begin = eatString("POOL.UserStopCodeBegin", true, 10); auto user_stop_code_end = eatString("POOL.UserStopCodeEnd", true, 10); eatCell ("POOL." ); auto point_data_owner_code = eatString("POOL.PointDataOwnerCode", true, 10); auto point_code = eatString("POOL.PointCode", true, 10); auto distance_since_start_of_link = eatNumber("POOL.DistanceSinceStartOfLink", true, 5); auto segment_speed = eatNumber("POOL.SegmentSpeed", false, 4); auto local_point_speed = eatNumber("POOL.LocalPointSpeed", false, 4); auto description = eatString("POOL.Description", false, 255); auto transport_type = eatString("POOL.TransportType", true, 5); if (!record_errors.empty()) return; records.point_on_links.emplace_back( Kv1PointOnLink::Key( data_owner_code, user_stop_code_begin, user_stop_code_end, point_data_owner_code, point_code, transport_type), *distance_since_start_of_link, segment_speed, local_point_speed, std::move(description)); } void Kv1Parser::parseIcon() { auto data_owner_code = eatString("ICON.DataOwnerCode", true, 10); auto icon_number = eatNumber("ICON.IconNumber", true, 4); auto icon_uri = eatString("ICON.IconURI", true, 1024); if (!record_errors.empty()) return; if (*icon_number != static_cast(*icon_number)) { record_errors.push_back("ICON.IconNumber should be an integer"); return; } records.icons.emplace_back( Kv1Icon::Key( data_owner_code, static_cast(*icon_number)), icon_uri); } void Kv1Parser::parseNotice() { auto data_owner_code = eatString("NOTICE.DataOwnerCode", true, 10); auto notice_code = eatString("NOTICE.NoticeCode", true, 20); auto notice_content = eatString("NOTICE.NoticeContent", true, 1024); if (!record_errors.empty()) return; records.notices.emplace_back( Kv1Notice::Key( data_owner_code, notice_code), notice_content); } void Kv1Parser::parseNoticeAssignment() { auto data_owner_code = eatString("NTCASSGNM.DataOwnerCode", true, 10); auto notice_code = eatString("NTCASSGNM.NoticeCode", true, 20); auto assigned_object = eatString("NTCASSGNM.AssignedObject", true, 8); auto timetable_version_code = eatString("NTCASSGNM.TimetableVersionCode", false, 10); auto organizational_unit_code = eatString("NTCASSGNM.OrganizationalUnitCode", false, 10); auto schedule_code = eatString("NTCASSGNM.ScheduleCode", false, 10); auto schedule_type_code = eatString("NTCASSGNM.ScheduleTypeCode", false, 10); auto period_group_code = eatString("NTCASSGNM.PeriodGroupCode", false, 10); auto specific_day_code = eatString("NTCASSGNM.SpecificDayCode", false, 10); auto day_type = eatString("NTCASSGNM.DayType", false, 7); auto line_planning_number = eatString("NTCASSGNM.LinePlanningNumber", true, 10); auto journey_number = eatNumber("NTCASSGNM.JourneyNumber", false, 6); auto stop_order = eatNumber("NTCASSGNM.StopOrder", false, 4); auto journey_pattern_code = eatString("NTCASSGNM.JourneyPatternCode", false, 10); auto timing_link_order = eatNumber("NTCASSGNM.TimingLinkOrder", false, 3); auto user_stop_code = eatString("NTCASSGNM.UserStopCode", false, 10); if (!record_errors.empty()) return; if (journey_number && *journey_number != static_cast(*journey_number)) record_errors.push_back("NTCASSGNM.JourneyNumber should be an integer"); if (journey_number && (*journey_number < 0 || *journey_number > 999'999)) record_errors.push_back("NTCASSGNM.JourneyNumber should be within the range [0-999999]"); if (stop_order && *stop_order != static_cast(*stop_order)) record_errors.push_back("NTCASSGNM.StopOrder should be an integer"); if (!journey_number && (assigned_object == "PUJO" || assigned_object == "PUJOPASS")) record_errors.push_back("NTCASSGNM.JourneyNumber is required for AssignedObject PUJO/PUJOPASS"); if (journey_pattern_code.empty() && assigned_object == "JOPATILI") record_errors.push_back("NTCASSGNM.JourneyPatternCode is required for AssignedObject JOPATILI"); if (!record_errors.empty()) return; records.notice_assignments.emplace_back( data_owner_code, notice_code, assigned_object, timetable_version_code, organizational_unit_code, schedule_code, schedule_type_code, period_group_code, specific_day_code, day_type, line_planning_number, static_cast>(journey_number), static_cast>(stop_order), journey_pattern_code, timing_link_order, user_stop_code); } void Kv1Parser::parseTimeDemandGroup() { auto data_owner_code = eatString("TIMDEMGRP.DataOwnerCode", true, 10); auto line_planning_number = eatString("TIMDEMGRP.LinePlanningNumber", true, 10); auto journey_pattern_code = eatString("TIMDEMGRP.JourneyPatternCode", true, 10); auto time_demand_group_code = eatString("TIMDEMGRP.TimeDemandGroupCode", true, 10); if (!record_errors.empty()) return; records.time_demand_groups.emplace_back( Kv1TimeDemandGroup::Key( data_owner_code, line_planning_number, journey_pattern_code, time_demand_group_code)); } void Kv1Parser::parseTimeDemandGroupRunTime() { auto data_owner_code = eatString("TIMDEMRNT.DataOwnerCode", true, 10); auto line_planning_number = eatString("TIMDEMRNT.LinePlanningNumber", true, 10); auto journey_pattern_code = eatString("TIMDEMRNT.JourneyPatternCode", true, 10); auto time_demand_group_code = eatString("TIMDEMRNT.TimeDemandGroupCode", true, 10); auto timing_link_order = eatNumber("TIMDEMRNT.TimingLinkOrder", true, 3); auto user_stop_code_begin = eatString("TIMDEMRNT.UserStopCodeBegin", true, 10); auto user_stop_code_end = eatString("TIMDEMRNT.UserStopCodeEnd", true, 10); auto total_drive_time = eatNumber("TIMDEMRNT.TotalDriveTime", true, 5); auto drive_time = eatNumber("TIMDEMRNT.DriveTime", true, 5); auto expected_delay = eatNumber("TIMDEMRNT.ExpectedDelay", false, 5); auto layover_time = eatNumber("TIMDEMRNT.LayOverTime", false, 5); auto stop_wait_time = eatNumber("TIMDEMRNT.StopWaitTime", true, 5); auto minimum_stop_time = eatNumber("TIMDEMRNT.MinimumStopTime", false, 5); if (!record_errors.empty()) return; if (timing_link_order && *timing_link_order != static_cast(*timing_link_order)) { record_errors.push_back("TIMDEMRNT.TimingLinkOrder should be an integer"); return; } records.time_demand_group_run_times.emplace_back( Kv1TimeDemandGroupRunTime::Key( data_owner_code, line_planning_number, journey_pattern_code, time_demand_group_code, static_cast(*timing_link_order)), user_stop_code_begin, user_stop_code_end, *total_drive_time, *drive_time, expected_delay, layover_time, *stop_wait_time, minimum_stop_time); } void Kv1Parser::parsePeriodGroup() { auto data_owner_code = eatString("PEGR.DataOwnerCode", true, 10); auto period_group_code = eatString("PEGR.PeriodGroupCode", true, 10); auto description = eatString("PEGR.Description", false, 255); if (!record_errors.empty()) return; records.period_groups.emplace_back( Kv1PeriodGroup::Key( data_owner_code, period_group_code), description); } void Kv1Parser::parseSpecificDay() { auto data_owner_code = eatString("SPECDAY.DataOwnerCode", true, 10); auto specific_day_code = eatString("SPECDAY.SpecificDayCode", true, 10); auto name = eatString("SPECDAY.Name", true, 50); auto description = eatString("SPECDAY.Description", false, 255); if (!record_errors.empty()) return; records.specific_days.emplace_back( Kv1SpecificDay::Key( data_owner_code, specific_day_code), name, description); } void Kv1Parser::parseTimetableVersion() { auto data_owner_code = eatString("TIVE.DataOwnerCode", true, 10); auto organizational_unit_code = eatString("TIVE.OrganizationalUnitCode", true, 10); auto timetable_version_code = eatString("TIVE.TimetableVersionCode", true, 10); auto period_group_code = eatString("TIVE.PeriodGroupCode", true, 10); auto specific_day_code = eatString("TIVE.SpecificDayCode", true, 10); auto valid_from_raw = eatString("TIVE.ValidFrom", true, 10); auto timetable_version_type = eatString("TIVE.TimetableVersionType", true, 10); auto valid_thru_raw = eatString("TIVE.ValidThru", false, 10); auto description = eatString("TIVE.Description", false, 255); if (!record_errors.empty()) return; auto valid_from = parseYyyymmdd(valid_from_raw); if (!valid_from) record_errors.push_back("TIVE.ValidFrom has invalid format, should be YYYY-MM-DD"); std::optional valid_thru; if (!valid_thru_raw.empty()) { valid_thru = parseYyyymmdd(valid_thru_raw); if (!valid_thru) { record_errors.push_back("TIVE.ValidFrom has invalid format, should be YYYY-MM-DD"); } } if (!description.empty()) record_errors.push_back("TIVE.Description should be empty"); if (!record_errors.empty()) return; records.timetable_versions.emplace_back( Kv1TimetableVersion::Key( data_owner_code, organizational_unit_code, timetable_version_code, period_group_code, specific_day_code), *valid_from, timetable_version_type, valid_thru, description); } void Kv1Parser::parsePublicJourney() { auto data_owner_code = eatString ("PUJO.DataOwnerCode", true, 10); auto timetable_version_code = eatString ("PUJO.TimetableVersionCode", true, 10); auto organizational_unit_code = eatString ("PUJO.OrganizationalUnitCode", true, 10); auto period_group_code = eatString ("PUJO.PeriodGroupCode", true, 10); auto specific_day_code = eatString ("PUJO.SpecificDayCode", true, 10); auto day_type = eatString ("PUJO.DayType", true, 7); auto line_planning_number = eatString ("PUJO.LinePlanningNumber", true, 10); auto journey_number = eatNumber ("PUJO.JourneyNumber", true, 6); auto time_demand_group_code = eatString ("PUJO.TimeDemandGroupCode", true, 10); auto journey_pattern_code = eatString ("PUJO.JourneyPatternCode", true, 10); auto departure_time_raw = eatString ("PUJO.DepartureTime", true, 8); auto wheelchair_accessible = eatString ("PUJO.WheelChairAccessible", true, 13); auto data_owner_is_operator = eatBoolean("PUJO.DataOwnerIsOperator", true ); auto planned_monitored = eatBoolean("PUJO.PlannedMonitored", true ); auto product_formula_type = eatNumber ("PUJO.ProductFormulaType", false, 4); auto show_flexible_trip = eatString ("PUJO.ShowFlexibleTrip", false, 8); if (!record_errors.empty()) return; auto departure_time = parseHhmmss(departure_time_raw); if (!departure_time) record_errors.push_back("PUJO.DepartureTime has a bad format"); if (*journey_number < 0 || *journey_number > 999'999) record_errors.push_back("PUJO.JourneyNumber should be within the range [0-999999]"); if (*journey_number != static_cast(*journey_number)) record_errors.push_back("PUJO.JourneyNumber should be an integer"); if (product_formula_type && *product_formula_type != static_cast(*product_formula_type)) record_errors.push_back("PUJO.ProductFormulaType should be an integer"); if (wheelchair_accessible != "ACCESSIBLE" && wheelchair_accessible != "NOTACCESSIBLE" && wheelchair_accessible != "UNKNOWN") record_errors.push_back("PUJO.WheelChairAccessible should be in BISON E3 values [ACCESSIBLE, NOTACCESSIBLE, UNKNOWN]"); if (!show_flexible_trip.empty() && show_flexible_trip != "TRUE" && show_flexible_trip != "FALSE" && show_flexible_trip != "REALTIME") record_errors.push_back("PUJO.ShowFlexibleTrip should be in BISON E21 values [TRUE, FALSE, REALTIME]"); if (!record_errors.empty()) return; records.public_journeys.emplace_back( Kv1PublicJourney::Key( data_owner_code, timetable_version_code, organizational_unit_code, period_group_code, specific_day_code, day_type, line_planning_number, static_cast(*journey_number)), time_demand_group_code, journey_pattern_code, *departure_time, wheelchair_accessible, *data_owner_is_operator, *planned_monitored, product_formula_type, show_flexible_trip); } void Kv1Parser::parsePeriodGroupValidity() { auto data_owner_code = eatString("PEGRVAL.DataOwnerCode", true, 10); auto organizational_unit_code = eatString("PEGRVAL.OrganizationalUnitCode", true, 10); auto period_group_code = eatString("PEGRVAL.PeriodGroupCode", true, 10); auto valid_from_raw = eatString("PEGRVAL.ValidFrom", true, 10); auto valid_thru_raw = eatString("PEGRVAL.ValidThru", true, 10); if (!record_errors.empty()) return; auto valid_from = parseYyyymmdd(valid_from_raw); auto valid_thru = parseYyyymmdd(valid_thru_raw); if (!valid_from) record_errors.push_back("PEGRVAL.ValidFrom has invalid format, should be YYYY-MM-DD"); if (!valid_thru) record_errors.push_back("PEGRVAL.ValidThru has invalid format, should be YYYY-MM-DD"); if (!record_errors.empty()) return; records.period_group_validities.emplace_back( Kv1PeriodGroupValidity::Key( data_owner_code, organizational_unit_code, period_group_code, *valid_from), *valid_thru); } void Kv1Parser::parseExceptionalOperatingDay() { auto data_owner_code = eatString("EXCOPDAY.DataOwnerCode", true, 10); auto organizational_unit_code = eatString("EXCOPDAY.OrganizationalUnitCode", true, 10); auto valid_date_raw = eatString("EXCOPDAY.ValidDate", true, 23); auto day_type_as_on = eatString("EXCOPDAY.DayTypeAsOn", true, 7); auto specific_day_code = eatString("EXCOPDAY.SpecificDayCode", true, 10); auto period_group_code = eatString("EXCOPDAY.PeriodGroupCode", false, 10); auto description = eatString("EXCOPDAY.Description", false, 255); if (!record_errors.empty()) return; std::string_view error; auto valid_date = parseDateTime(valid_date_raw, amsterdam, &error); if (!valid_date) { record_errors.push_back(std::format("EXCOPDAY.ValidDate has an bad format (value: {}): {}", valid_date_raw, error)); return; } records.exceptional_operating_days.emplace_back( Kv1ExceptionalOperatingDay::Key( data_owner_code, organizational_unit_code, *valid_date), day_type_as_on, specific_day_code, period_group_code, description); } void Kv1Parser::parseScheduleVersion() { auto data_owner_code = eatString("SCHEDVERS.DataOwnerCode", true, 10); auto organizational_unit_code = eatString("SCHEDVERS.OrganizationalUnitCode", true, 10); auto schedule_code = eatString("SCHEDVERS.ScheduleCode", true, 10); auto schedule_type_code = eatString("SCHEDVERS.ScheduleTypeCode", true, 10); auto valid_from_raw = eatString("SCHEDVERS.ValidFrom", true, 10); auto valid_thru_raw = eatString("SCHEDVERS.ValidThru", false, 10); auto description = eatString("SCHEDVERS.Description", false, 255); if (!record_errors.empty()) return; auto valid_from = parseYyyymmdd(valid_from_raw); if (!valid_from) record_errors.push_back("SCHEDVERS.ValidFrom has invalid format, should be YYYY-MM-DD"); std::optional valid_thru; if (!valid_thru_raw.empty()) { valid_thru = parseYyyymmdd(valid_thru_raw); if (!valid_thru) { record_errors.push_back("SCHEDVERS.ValidFrom has invalid format, should be YYYY-MM-DD"); } } if (!description.empty()) record_errors.push_back("SCHEDVERS.Description should be empty"); if (!record_errors.empty()) return; records.schedule_versions.emplace_back( Kv1ScheduleVersion::Key( data_owner_code, organizational_unit_code, schedule_code, schedule_type_code), *valid_from, valid_thru, description); } void Kv1Parser::parsePublicJourneyPassingTimes() { auto data_owner_code = eatString ("PUJOPASS.DataOwnerCode", true, 10); auto organizational_unit_code = eatString ("PUJOPASS.OrganizationalUnitCode", true, 10); auto schedule_code = eatString ("PUJOPASS.ScheduleCode", true, 10); auto schedule_type_code = eatString ("PUJOPASS.ScheduleTypeCode", true, 10); auto line_planning_number = eatString ("PUJOPASS.LinePlanningNumber", true, 10); auto journey_number = eatNumber ("PUJOPASS.JourneyNumber", true, 6); auto stop_order = eatNumber ("PUJOPASS.StopOrder", true, 4); auto journey_pattern_code = eatString ("PUJOPASS.JourneyPatternCode", true, 10); auto user_stop_code = eatString ("PUJOPASS.UserStopCode", true, 10); auto target_arrival_time_raw = eatString ("PUJOPASS.TargetArrivalTime", false, 8); auto target_departure_time_raw = eatString ("PUJOPASS.TargetDepartureTime", false, 8); auto wheelchair_accessible = eatString ("PUJOPASS.WheelChairAccessible", true, 13); auto data_owner_is_operator = eatBoolean("PUJOPASS.DataOwnerIsOperator", true ); auto planned_monitored = eatBoolean("PUJOPASS.PlannedMonitored", true ); auto product_formula_type = eatNumber ("PUJOPASS.ProductFormulaType", false, 4); auto show_flexible_trip = eatString ("PUJOPASS.ShowFlexibleTrip", false, 8); if (!record_errors.empty()) return; if (*journey_number < 0 || *journey_number > 999'999) record_errors.push_back("PUJOPASS.JourneyNumber should be within the range [0-999999]"); if (*journey_number != static_cast(*journey_number)) record_errors.push_back("PUJOPASS.JourneyNumber should be an integer"); if (*stop_order != static_cast(*stop_order)) record_errors.push_back("PUJOPASS.StopOrder should be an integer"); if (product_formula_type && *product_formula_type != static_cast(*product_formula_type)) record_errors.push_back("PUJOPASS.ProductFormulaType should be an integer"); if (wheelchair_accessible != "ACCESSIBLE" && wheelchair_accessible != "NOTACCESSIBLE" && wheelchair_accessible != "UNKNOWN") record_errors.push_back("PUJOPASS.WheelChairAccessible should be in BISON E3 values [ACCESSIBLE, NOTACCESSIBLE, UNKNOWN]"); if (!show_flexible_trip.empty() && show_flexible_trip != "TRUE" && show_flexible_trip != "FALSE" && show_flexible_trip != "REALTIME") record_errors.push_back("PUJOPASS.ShowFlexibleTrip should be in BISON E21 values [TRUE, FALSE, REALTIME]"); std::optional> target_arrival_time; if (!target_arrival_time_raw.empty()) { target_arrival_time = parseHhmmss(target_arrival_time_raw); if (!target_arrival_time) { record_errors.push_back("PUJOPASS.TargetArrivalTime has invalid format, should be HH:MM:SS"); } } std::optional> target_departure_time; if (!target_departure_time_raw.empty()) { target_departure_time = parseHhmmss(target_departure_time_raw); if (!target_departure_time) { record_errors.push_back("PUJOPASS.TargetDepartureTime has invalid format, should be HH:MM:SS"); } } if (!record_errors.empty()) return; records.public_journey_passing_times.emplace_back( Kv1PublicJourneyPassingTimes::Key( data_owner_code, organizational_unit_code, schedule_code, schedule_type_code, line_planning_number, static_cast(*journey_number), static_cast(*stop_order)), journey_pattern_code, user_stop_code, target_arrival_time, target_departure_time, wheelchair_accessible, *data_owner_is_operator, *planned_monitored, product_formula_type, show_flexible_trip); } void Kv1Parser::parseOperatingDay() { auto data_owner_code = eatString("OPERDAY.DataOwnerCode", true, 10); auto organizational_unit_code = eatString("OPERDAY.OrganizationalUnitCode", true, 10); auto schedule_code = eatString("OPERDAY.ScheduleCode", true, 10); auto schedule_type_code = eatString("OPERDAY.ScheduleTypeCode", true, 10); auto valid_date_raw = eatString("OPERDAY.ValidDate", true, 10); auto description = eatString("OPERDAY.Description", false, 255); if (!record_errors.empty()) return; auto valid_date = parseYyyymmdd(valid_date_raw); if (!valid_date) record_errors.push_back("OPERDAY.ValidDate has invalid format, should be YYYY-MM-DD"); if (!record_errors.empty()) return; records.operating_days.emplace_back( Kv1OperatingDay::Key( data_owner_code, organizational_unit_code, schedule_code, schedule_type_code, *valid_date), description); } const std::unordered_map Kv1Parser::type_parsers{ { "ORUN", &Kv1Parser::parseOrganizationalUnit }, { "ORUNORUN", &Kv1Parser::parseHigherOrganizationalUnit }, { "USRSTOP", &Kv1Parser::parseUserStopPoint }, { "USRSTAR", &Kv1Parser::parseUserStopArea }, { "TILI", &Kv1Parser::parseTimingLink }, { "LINK", &Kv1Parser::parseLink }, { "LINE", &Kv1Parser::parseLine }, { "DEST", &Kv1Parser::parseDestination }, { "JOPA", &Kv1Parser::parseJourneyPattern }, { "CONFINREL", &Kv1Parser::parseConcessionFinancerRelation }, { "CONAREA", &Kv1Parser::parseConcessionArea }, { "FINANCER", &Kv1Parser::parseFinancer }, { "JOPATILI", &Kv1Parser::parseJourneyPatternTimingLink }, { "POINT", &Kv1Parser::parsePoint }, { "POOL", &Kv1Parser::parsePointOnLink }, { "ICON", &Kv1Parser::parseIcon }, { "NOTICE", &Kv1Parser::parseNotice }, { "NTCASSGNM", &Kv1Parser::parseNoticeAssignment }, { "TIMDEMGRP", &Kv1Parser::parseTimeDemandGroup }, { "TIMDEMRNT", &Kv1Parser::parseTimeDemandGroupRunTime }, { "PEGR", &Kv1Parser::parsePeriodGroup }, { "SPECDAY", &Kv1Parser::parseSpecificDay }, { "TIVE", &Kv1Parser::parseTimetableVersion }, { "PUJO", &Kv1Parser::parsePublicJourney }, { "PEGRVAL", &Kv1Parser::parsePeriodGroupValidity }, { "EXCOPDAY", &Kv1Parser::parseExceptionalOperatingDay }, { "SCHEDVERS", &Kv1Parser::parseScheduleVersion }, { "PUJOPASS", &Kv1Parser::parsePublicJourneyPassingTimes }, { "OPERDAY", &Kv1Parser::parseOperatingDay }, };