// vim:set sw=2 ts=2 sts et:
//
// Copyright 2024 Rutger Broekhoff. Licensed under the EUPL.
#include <tmi8/kv1_parser.hpp>
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<const uint8_t *>(s.data());
if (!(b[0] & 0x80))
res = static_cast<rune>(b[0]);
else if ((b[0] & 0xE0) == 0xC0) {
length = 2;
if (s.size() >= 2 && (b[1] & 0xC0) == 0x80) {
res = static_cast<rune>(b[0] & ~0xC0) << 6;
res |= static_cast<rune>(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<rune>(b[0] & ~0xE0) << 12;
res |= static_cast<rune>(b[1] & ~0x80) << 6;
res |= static_cast<rune>(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<rune>(b[0] & ~0xF0) << 18;
res |= static_cast<rune>(b[1] & ~0x80) << 12;
res |= static_cast<rune>(b[2] & ~0x80) << 6;
res |= static_cast<rune>(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<Kv1Token> 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<bool> 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<bool> 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<double> 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<long>(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<uint8_t>(c - '0');
else if (c >= 'A' && c <= 'F') return static_cast<uint8_t>(c - 'A' + 10);
return 0;
}
static std::optional<RgbColor> 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<uint8_t>(fromHex(src[0]) << 4) + fromHex(src[1]);
uint8_t g = static_cast<uint8_t>(fromHex(src[2]) << 4) + fromHex(src[3]);
uint8_t b = static_cast<uint8_t>(fromHex(src[4]) << 4) + fromHex(src[5]);
return RgbColor{ r, g, b };
}
std::optional<RgbColor> 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<double> 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<long>(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<bool> Kv1Parser::eatBoolean(std::string_view field, bool mandatory) {
auto value = eatCell(field);
if (!record_errors.empty()) return {};
return requireBoolean(field, mandatory, *value);
}
std::optional<double> 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<RgbColor> Kv1Parser::eatRgbColor(std::string_view field, bool mandatory) {
auto value = eatCell(field);
if (!record_errors.empty()) return {};
return requireRgbColor(field, mandatory, *value);
}
std::optional<double> 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("<header>.Recordtype", true, 10);
auto version_number = eatString("<header>.VersionNumber", true, 2);
auto implicit_explicit = eatString("<header>.Implicit/Explicit", true, 1);
if (!record_errors.empty()) return {};
if (version_number != "1") {
record_errors.push_back("<header>.VersionNumber should be 1");
return "";
}
if (implicit_explicit != "I") {
record_errors.push_back("<header>.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<std::chrono::year_month_day> 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<std::chrono::hh_mm_ss<std::chrono::seconds>> 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<std::chrono::sys_seconds> 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.<deprecated field #1>" );
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.<deprecated field #2>" );
eatCell ("USRSTOP.<deprecated field #3>" );
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.<deprecated field #1>" );
eatCell ("USRSTAR.<deprecated field #2>" );
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.<deprecated field #1>" );
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<short>(*line_ve_tag_number))
record_errors.push_back("LINE.LineVeTagNumber should be an integer");
if (line_icon && *line_icon != static_cast<short>(*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<short>(*line_ve_tag_number),
description,
transport_type,
static_cast<std::optional<short>>(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<short>(*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.<deprecated field #1>" );
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<short>(*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<short>(*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.<deprecated field #1>" );
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.<deprecated field #1>" );
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<short>(*icon_number)) {
record_errors.push_back("ICON.IconNumber should be an integer");
return;
}
records.icons.emplace_back(
Kv1Icon::Key(
data_owner_code,
static_cast<short>(*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<short>(*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<short>(*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<std::optional<int>>(journey_number),
static_cast<std::optional<short>>(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<short>(*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<short>(*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<std::chrono::year_month_day> 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<int>(*journey_number))
record_errors.push_back("PUJO.JourneyNumber should be an integer");
if (product_formula_type && *product_formula_type != static_cast<short>(*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<int>(*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<std::chrono::year_month_day> 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<int>(*journey_number))
record_errors.push_back("PUJOPASS.JourneyNumber should be an integer");
if (*stop_order != static_cast<short>(*stop_order))
record_errors.push_back("PUJOPASS.StopOrder should be an integer");
if (product_formula_type && *product_formula_type != static_cast<short>(*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<std::chrono::hh_mm_ss<std::chrono::seconds>> 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<std::chrono::hh_mm_ss<std::chrono::seconds>> 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<int>(*journey_number),
static_cast<short>(*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<std::string_view, Kv1Parser::ParseFunc> 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 },
};