use std::path::Path;
use chrono::NaiveTime;
use nom::{
IResult, Parser,
branch::alt,
bytes::tag,
character::{char, complete::space1},
combinator::map,
sequence::preceded,
};
use rustc_hash::{FxHashMap, FxHashSet};
use crate::{
JourneyId,
error::{HResult, HrdfError},
models::{Journey, JourneyMetadataEntry, JourneyMetadataType, JourneyRouteEntry},
parsing::{
error::{PResult, ParsingError},
helpers::{
direction_parser, i32_from_n_digits_parser, optional_i32_from_n_digits_parser,
read_lines, string_from_n_chars_parser,
},
},
storage::ResourceStorage,
utils::{AutoIncrement, create_time_from_value},
};
type JourneyAndTypeConverter = (ResourceStorage<Journey>, FxHashSet<JourneyId>);
#[derive(Debug)]
enum JourneyLines {
Zline {
journey_id: i32,
transport_company_id: String,
#[allow(unused)]
transport_variant: i32,
#[allow(unused)]
num_cycles: Option<i32>,
#[allow(unused)]
cycle_dura_min: Option<i32>,
},
Gline {
offer: String,
stop_from_id: Option<i32>,
stop_to_id: Option<i32>,
},
AVEline {
stop_from_id: Option<i32>,
stop_to_id: Option<i32>,
bit_field_id: Option<i32>,
},
Aline {
offer: String,
stop_from_id: Option<i32>,
stop_to_id: Option<i32>,
#[allow(unused)]
reference: Option<i32>,
},
Iline {
info_code: String,
stop_from_id: Option<i32>,
stop_to_id: Option<i32>,
validity_ref: Option<i32>,
info_ref: i32,
departure_time: Option<i32>,
arrival_time: Option<i32>,
},
Rline {
direction: String,
ref_direction_code: String,
stop_from_id: Option<i32>,
stop_to_id: Option<i32>,
departure_time: Option<i32>,
arrival_time: Option<i32>,
},
Lline {
line_info: String,
stop_from_id: Option<i32>,
stop_to_id: Option<i32>,
departure_time: Option<i32>,
arrival_time: Option<i32>,
},
CiLine {
num_minutes: i32,
stop_from_id: Option<i32>,
stop_to_id: Option<i32>,
departure_time: Option<i32>,
arrival_time: Option<i32>,
},
CoLine {
num_minutes: i32,
stop_from_id: Option<i32>,
stop_to_id: Option<i32>,
departure_time: Option<i32>,
arrival_time: Option<i32>,
},
JourneyLine {
stop_id: i32,
#[allow(unused)]
stop_name: String,
arrival_time: Option<i32>,
departure_time: Option<i32>,
#[allow(unused)]
journey_id: Option<i32>,
#[allow(unused)]
administration: String,
},
}
fn row_z_combinator(input: &str) -> IResult<&str, JourneyLines> {
map(
preceded(
tag("*Z "),
(
i32_from_n_digits_parser(6),
preceded(char(' '), string_from_n_chars_parser(6)),
preceded(space1, i32_from_n_digits_parser(3)), preceded(char(' '), optional_i32_from_n_digits_parser(3)),
preceded(char(' '), optional_i32_from_n_digits_parser(3)),
),
),
|(journey_id, transport_company_id, transport_variant, num_cycles, cycle_dura_min)| {
JourneyLines::Zline {
journey_id,
transport_company_id,
transport_variant,
num_cycles,
cycle_dura_min,
}
},
)
.parse(input)
}
fn row_g_combinator(input: &str) -> IResult<&str, JourneyLines> {
map(
preceded(
tag("*G "),
(
string_from_n_chars_parser(3),
preceded(char(' '), optional_i32_from_n_digits_parser(7)),
preceded(char(' '), optional_i32_from_n_digits_parser(7)),
),
),
|(offer, stop_from_id, stop_to_id)| JourneyLines::Gline {
offer,
stop_from_id,
stop_to_id,
},
)
.parse(input)
}
fn row_a_ve_combinator(input: &str) -> IResult<&str, JourneyLines> {
map(
preceded(
tag("*A VE "),
(
optional_i32_from_n_digits_parser(7),
preceded(char(' '), optional_i32_from_n_digits_parser(7)),
preceded(char(' '), optional_i32_from_n_digits_parser(6)),
),
),
|(stop_from_id, stop_to_id, bit_field_id)| JourneyLines::AVEline {
stop_from_id,
stop_to_id,
bit_field_id,
},
)
.parse(input)
}
fn row_a_combinator(input: &str) -> IResult<&str, JourneyLines> {
map(
preceded(
tag("*A "),
(
string_from_n_chars_parser(2),
preceded(char(' '), optional_i32_from_n_digits_parser(7)),
preceded(char(' '), optional_i32_from_n_digits_parser(7)),
preceded(char(' '), optional_i32_from_n_digits_parser(6)),
),
),
|(offer, stop_from_id, stop_to_id, reference)| JourneyLines::Aline {
offer,
stop_from_id,
stop_to_id,
reference,
},
)
.parse(input)
}
fn row_i_combinator(input: &str) -> IResult<&str, JourneyLines> {
map(
preceded(
tag("*I "),
(
string_from_n_chars_parser(2),
preceded(char(' '), optional_i32_from_n_digits_parser(7)),
preceded(char(' '), optional_i32_from_n_digits_parser(7)),
preceded(char(' '), optional_i32_from_n_digits_parser(6)),
preceded(char(' '), i32_from_n_digits_parser(9)),
preceded(char(' '), optional_i32_from_n_digits_parser(6)),
preceded(char(' '), optional_i32_from_n_digits_parser(6)),
),
),
|(
info_code,
stop_from_id,
stop_to_id,
validity_ref,
info_ref,
departure_time,
arrival_time,
)| {
JourneyLines::Iline {
info_code,
stop_from_id,
stop_to_id,
validity_ref,
info_ref,
departure_time,
arrival_time,
}
},
)
.parse(input)
}
fn row_l_combinator(input: &str) -> IResult<&str, JourneyLines> {
map(
preceded(
tag("*L "),
(
string_from_n_chars_parser(8),
preceded(char(' '), optional_i32_from_n_digits_parser(7)),
preceded(char(' '), optional_i32_from_n_digits_parser(7)),
preceded(char(' '), optional_i32_from_n_digits_parser(6)),
preceded(char(' '), optional_i32_from_n_digits_parser(6)),
),
),
|(line_info, stop_from_id, stop_to_id, departure_time, arrival_time)| JourneyLines::Lline {
line_info,
stop_from_id,
stop_to_id,
departure_time,
arrival_time,
},
)
.parse(input)
}
fn row_r_combinator(input: &str) -> IResult<&str, JourneyLines> {
map(
preceded(
tag("*R "),
(
string_from_n_chars_parser(1),
preceded(
char(' '),
alt((
map(direction_parser, |(prefix, id)| format!("{prefix}{id}")),
string_from_n_chars_parser(7),
)),
),
preceded(char(' '), optional_i32_from_n_digits_parser(7)),
preceded(char(' '), optional_i32_from_n_digits_parser(7)),
preceded(char(' '), optional_i32_from_n_digits_parser(6)),
preceded(char(' '), optional_i32_from_n_digits_parser(6)),
),
),
|(
direction,
ref_direction_code,
stop_from_id,
stop_to_id,
departure_time,
arrival_time,
)| {
JourneyLines::Rline {
direction,
ref_direction_code,
stop_from_id,
stop_to_id,
departure_time,
arrival_time,
}
},
)
.parse(input)
}
fn row_ci_co_combinator(input: &str) -> IResult<&str, JourneyLines> {
map(
(
alt((tag("*CI"), tag("*CO"))),
preceded(char(' '), i32_from_n_digits_parser(4)),
preceded(char(' '), optional_i32_from_n_digits_parser(7)),
preceded(char(' '), optional_i32_from_n_digits_parser(7)),
preceded(char(' '), optional_i32_from_n_digits_parser(6)),
preceded(char(' '), optional_i32_from_n_digits_parser(6)),
),
|(ci_co, num_minutes, stop_from_id, stop_to_id, departure_time, arrival_time)| {
if ci_co == "*CI" {
JourneyLines::CiLine {
num_minutes,
stop_from_id,
stop_to_id,
departure_time,
arrival_time,
}
} else {
JourneyLines::CoLine {
num_minutes,
stop_from_id,
stop_to_id,
departure_time,
arrival_time,
}
}
},
)
.parse(input)
}
fn row_journey_description_combinator(input: &str) -> IResult<&str, JourneyLines> {
map(
(
i32_from_n_digits_parser(7),
preceded(char(' '), string_from_n_chars_parser(20)),
preceded(char(' '), optional_i32_from_n_digits_parser(6)),
preceded(char(' '), optional_i32_from_n_digits_parser(6)),
preceded(char(' '), optional_i32_from_n_digits_parser(6)),
preceded(char(' '), string_from_n_chars_parser(6)),
),
|(stop_id, stop_name, arrival_time, departure_time, journey_id, administration)| {
JourneyLines::JourneyLine {
stop_id,
stop_name,
arrival_time,
departure_time,
journey_id,
administration,
}
},
)
.parse(input)
}
fn parse_line(
line: &str,
data: &mut FxHashMap<i32, Journey>,
pk_type_converter: &mut FxHashSet<JourneyId>,
auto_increment: &AutoIncrement,
transport_types_pk_type_converter: &FxHashMap<String, i32>,
attributes_pk_type_converter: &FxHashMap<String, i32>,
directions_pk_type_converter: &FxHashMap<String, i32>,
) -> PResult<()> {
let (_res, journey_lines) = alt((
row_z_combinator,
row_g_combinator,
row_a_ve_combinator,
row_a_combinator,
row_i_combinator,
row_l_combinator,
row_r_combinator,
row_ci_co_combinator,
row_journey_description_combinator,
))
.parse(line)?;
match journey_lines {
JourneyLines::Zline {
journey_id,
transport_company_id,
transport_variant: _,
num_cycles: _,
cycle_dura_min: _,
} => {
let id = auto_increment.next();
pk_type_converter.insert((journey_id, transport_company_id.to_owned()));
data.insert(id, Journey::new(id, journey_id, transport_company_id));
}
JourneyLines::Gline {
offer,
stop_from_id,
stop_to_id,
} => {
let journey = data.get_mut(&auto_increment.get()).ok_or_else(|| {
ParsingError::UnknownId(format!(
"Type A row missing for id {}.",
auto_increment.get()
))
})?;
let transport_type_id =
*transport_types_pk_type_converter
.get(&offer)
.ok_or_else(|| {
ParsingError::UnknownId(format!("Unknown Offer legacy ID: {offer}"))
})?;
journey.add_metadata_entry(
JourneyMetadataType::TransportType,
JourneyMetadataEntry::new(
stop_from_id,
stop_to_id,
Some(transport_type_id),
None,
None,
None,
None,
None,
),
);
}
JourneyLines::AVEline {
stop_from_id,
stop_to_id,
bit_field_id,
} => {
let journey = data.get_mut(&auto_increment.get()).ok_or_else(|| {
ParsingError::UnknownId(format!(
"Type A row missing for id {}.",
auto_increment.get()
))
})?;
journey.add_metadata_entry(
JourneyMetadataType::BitField,
JourneyMetadataEntry::new(
stop_from_id,
stop_to_id,
None,
bit_field_id,
None,
None,
None,
None,
),
);
}
JourneyLines::Aline {
offer,
stop_from_id,
stop_to_id,
reference: _,
} => {
let journey = data.get_mut(&auto_increment.get()).ok_or_else(|| {
ParsingError::UnknownId(format!(
"Type A row missing for id {}.",
auto_increment.get()
))
})?;
let attribute_id = *attributes_pk_type_converter.get(&offer).ok_or_else(|| {
ParsingError::UnknownId(format!("Unknown offer legacy Id: {offer}."))
})?;
journey.add_metadata_entry(
JourneyMetadataType::Attribute,
JourneyMetadataEntry::new(
stop_from_id,
stop_to_id,
Some(attribute_id),
None,
None,
None,
None,
None,
),
);
}
JourneyLines::Iline {
info_code,
stop_from_id,
stop_to_id,
validity_ref,
info_ref,
departure_time,
arrival_time,
} => {
let journey = data.get_mut(&auto_increment.get()).ok_or_else(|| {
ParsingError::UnknownId(format!(
"Type A row missing for id {}.",
auto_increment.get()
))
})?;
let arrival_time = create_time(arrival_time)?;
let departure_time = create_time(departure_time)?;
journey.add_metadata_entry(
JourneyMetadataType::InformationText,
JourneyMetadataEntry::new(
stop_from_id,
stop_to_id,
Some(info_ref),
validity_ref,
departure_time,
arrival_time,
Some(info_code),
None,
),
);
}
JourneyLines::Rline {
direction,
ref_direction_code,
stop_from_id,
stop_to_id,
departure_time,
arrival_time,
} => {
let journey = data.get_mut(&auto_increment.get()).ok_or_else(|| {
ParsingError::UnknownId(format!(
"Type A row missing for id {}.",
auto_increment.get()
))
})?;
let arrival_time = create_time(arrival_time)?;
let departure_time = create_time(departure_time)?;
let direction_id = if ref_direction_code.is_empty() {
None
} else {
let id = *directions_pk_type_converter
.get(&ref_direction_code)
.ok_or_else(|| {
ParsingError::UnknownId(format!(
"Type A row missing for id {ref_direction_code}."
))
})?;
Some(id)
};
journey.add_metadata_entry(
JourneyMetadataType::Direction,
JourneyMetadataEntry::new(
stop_from_id,
stop_to_id,
direction_id,
None,
departure_time,
arrival_time,
Some(direction),
None,
),
);
}
JourneyLines::Lline {
mut line_info,
stop_from_id,
stop_to_id,
departure_time,
arrival_time,
} => {
let journey = data.get_mut(&auto_increment.get()).ok_or_else(|| {
ParsingError::UnknownId(format!(
"Type A row missing for id {}.",
auto_increment.get()
))
})?;
let arrival_time = create_time(arrival_time)?;
let departure_time = create_time(departure_time)?;
let line_info_first_char = line_info
.chars()
.next()
.ok_or("Missing line info (the string is empty).")?;
let (resource_id, extra_field_1) = if line_info_first_char == '#' {
line_info.drain(..line_info_first_char.len_utf8());
(Some(line_info.parse::<i32>()?), None)
} else {
(None, Some(line_info))
};
journey.add_metadata_entry(
JourneyMetadataType::Line,
JourneyMetadataEntry::new(
stop_from_id,
stop_to_id,
resource_id,
None,
departure_time,
arrival_time,
extra_field_1,
None,
),
);
}
JourneyLines::CiLine {
num_minutes,
stop_from_id,
stop_to_id,
departure_time,
arrival_time,
} => {
let journey = data.get_mut(&auto_increment.get()).ok_or_else(|| {
ParsingError::UnknownId(format!(
"Type A row missing for id {}.",
auto_increment.get()
))
})?;
let arrival_time = create_time(arrival_time)?;
let departure_time = create_time(departure_time)?;
journey.add_metadata_entry(
JourneyMetadataType::ExchangeTimeBoarding,
JourneyMetadataEntry::new(
stop_from_id,
stop_to_id,
None,
None,
departure_time,
arrival_time,
None,
Some(num_minutes),
),
);
}
JourneyLines::CoLine {
num_minutes,
stop_from_id,
stop_to_id,
departure_time,
arrival_time,
} => {
let journey = data.get_mut(&auto_increment.get()).ok_or_else(|| {
ParsingError::UnknownId(format!(
"Type A row missing for id {}.",
auto_increment.get()
))
})?;
let arrival_time = create_time(arrival_time)?;
let departure_time = create_time(departure_time)?;
journey.add_metadata_entry(
JourneyMetadataType::ExchangeTimeDisembarking,
JourneyMetadataEntry::new(
stop_from_id,
stop_to_id,
None,
None,
departure_time,
arrival_time,
None,
Some(num_minutes),
),
);
}
JourneyLines::JourneyLine {
stop_id,
stop_name: _,
arrival_time,
departure_time,
journey_id: _,
administration: _,
} => {
let journey = data.get_mut(&auto_increment.get()).ok_or_else(|| {
ParsingError::UnknownId(format!(
"Type A row missing for id {}.",
auto_increment.get()
))
})?;
let arrival_time = create_time(arrival_time)?;
let departure_time = create_time(departure_time)?;
journey.add_route_entry(JourneyRouteEntry::new(
stop_id,
arrival_time,
departure_time,
));
}
}
Ok(())
}
pub fn parse(
path: &Path,
transport_types_pk_type_converter: &FxHashMap<String, i32>,
attributes_pk_type_converter: &FxHashMap<String, i32>,
directions_pk_type_converter: &FxHashMap<String, i32>,
) -> HResult<JourneyAndTypeConverter> {
log::info!("Parsing FPLAN...");
let file = path.join("FPLAN");
let lines = read_lines(&file, 0)?;
let auto_increment = AutoIncrement::new();
let mut data = FxHashMap::default();
let mut pk_type_converter = FxHashSet::default();
lines
.into_iter()
.enumerate()
.filter(|(_, line)| !line.trim().is_empty())
.try_for_each(|(line_number, line)| {
parse_line(
&line,
&mut data,
&mut pk_type_converter,
&auto_increment,
transport_types_pk_type_converter,
attributes_pk_type_converter,
directions_pk_type_converter,
)
.map_err(|e| HrdfError::Parsing {
error: e,
file: String::from(file.to_string_lossy()),
line,
line_number,
})
})?;
Ok((ResourceStorage::new(data), pk_type_converter))
}
fn create_time(time: Option<i32>) -> PResult<Option<NaiveTime>> {
time.map(|value| {
create_time_from_value(match value.abs() {
val if val >= 2400 => val % 2400,
val => val,
} as u32)
})
.transpose()
}
#[cfg(test)]
mod tests {
use crate::parsing::tests::get_json_values;
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn parsing_rows() {
let rows = vec![
"*Z 002359 000011 101 % -- 37649518273 --"
.to_string(),
"*G IR 8507000 8509000 %".to_string(),
"*A VE 8507000 8509000 348508 %".to_string(),
"*A FS 8507000 8509000 %".to_string(),
"*I JY 000001370 %".to_string(),
"*L 35 8507000 8509000 %".to_string(),
"*R H %".to_string(),
"*CI 0002 8507000 8507000 %".to_string(),
"8507000 Bern 00638 %".to_string(),
"8508005 Burgdorf 00652 00653 %".to_string(),
"8508008 Herzogenbuchsee 00704 00705 %".to_string(),
"8509000 Chur 00948 %".to_string(),
];
let auto_increment = AutoIncrement::new();
let mut data = FxHashMap::default();
let mut pk_type_converter = FxHashSet::default();
let mut transport_types_pk_type_converter = FxHashMap::<String, i32>::default();
transport_types_pk_type_converter.insert("IR".to_string(), 100);
let mut attributes_pk_type_converter = FxHashMap::<String, i32>::default();
attributes_pk_type_converter.insert("FS".to_string(), 100);
let directions_pk_type_converter = FxHashMap::<String, i32>::default();
for line in rows {
parse_line(
&line,
&mut data,
&mut pk_type_converter,
&auto_increment,
&transport_types_pk_type_converter,
&attributes_pk_type_converter,
&directions_pk_type_converter,
)
.unwrap();
}
let reference = r#"
{
"id": 1,
"legacy_id": 2359,
"administration": "000011",
"metadata": {
"Attribute": [
{
"from_stop_id": 8507000,
"until_stop_id": 8509000,
"resource_id": 100,
"bit_field_id": null,
"departure_time": null,
"arrival_time": null,
"extra_field_1": null,
"extra_field_2": null
}
],
"TransportType": [
{
"from_stop_id": 8507000,
"until_stop_id": 8509000,
"resource_id": 100,
"bit_field_id": null,
"departure_time": null,
"arrival_time": null,
"extra_field_1": null,
"extra_field_2": null
}
],
"InformationText": [
{
"from_stop_id": null,
"until_stop_id": null,
"resource_id": 1370,
"bit_field_id": null,
"departure_time": null,
"arrival_time": null,
"extra_field_1": "JY",
"extra_field_2": null
}
],
"BitField": [
{
"from_stop_id": 8507000,
"until_stop_id": 8509000,
"resource_id": null,
"bit_field_id": 348508,
"departure_time": null,
"arrival_time": null,
"extra_field_1": null,
"extra_field_2": null
}
],
"ExchangeTimeBoarding": [
{
"from_stop_id": 8507000,
"until_stop_id": 8507000,
"resource_id": null,
"bit_field_id": null,
"departure_time": null,
"arrival_time": null,
"extra_field_1": null,
"extra_field_2": 2
}
],
"Line": [
{
"from_stop_id": 8507000,
"until_stop_id": 8509000,
"resource_id": null,
"bit_field_id": null,
"departure_time": null,
"arrival_time": null,
"extra_field_1": "35",
"extra_field_2": null
}
],
"Direction": [
{
"from_stop_id": null,
"until_stop_id": null,
"resource_id": null,
"bit_field_id": null,
"departure_time": null,
"arrival_time": null,
"extra_field_1": "H",
"extra_field_2": null
}
]
},
"route": [
{
"stop_id": 8507000,
"arrival_time": null,
"departure_time": "06:38:00"
},
{
"stop_id": 8508005,
"arrival_time": "06:52:00",
"departure_time": "06:53:00"
},
{
"stop_id": 8508008,
"arrival_time": "07:04:00",
"departure_time": "07:05:00"
},
{
"stop_id": 8509000,
"arrival_time": "09:48:00",
"departure_time": null
}
]
}"#;
let (attribute, reference) = get_json_values(data.get(&1).unwrap(), reference).unwrap();
assert_eq!(attribute, reference);
}
mod row_z {
use super::*;
use pretty_assertions::assert_eq;
type ZlineRow = (i32, String, i32, Option<i32>, Option<i32>);
fn row_z_parser(input: &str) -> PResult<(&str, ZlineRow)> {
let (res, row_z) = row_z_combinator(input)?;
match row_z {
JourneyLines::Zline {
journey_id,
transport_company_id,
transport_variant,
num_cycles,
cycle_dura_min,
} => Ok((
res,
(
journey_id,
transport_company_id,
transport_variant,
num_cycles,
cycle_dura_min,
),
)),
l => Err(format!("Zline expected but got {l:?}").as_str().into()),
}
}
#[test]
fn success_no_options() {
let input = "*Z 000003 000011 101 % Fahrtnummer 3, für TU 11 (SBB), mit Variante 101 (ignore)";
let (
res,
(journey_id, transport_company_id, transport_variant, num_cycles, cycle_dura_min),
) = row_z_parser(input).unwrap();
assert_eq!(3, journey_id);
assert_eq!("000011", transport_company_id);
assert_eq!(101, transport_variant);
assert_eq!(None, num_cycles);
assert_eq!(None, cycle_dura_min);
assert_eq!(
res.trim(),
"% Fahrtnummer 3, für TU 11 (SBB), mit Variante 101 (ignore)"
);
}
#[test]
fn success_with_options() {
let input = "*Z 123456 000011 101 012 060 % Fahrtnummer 123456, für TU 11 (SBB), mit Variante 101 (ignore), 12 mal, alle 60 Minuten";
let (
res,
(journey_id, transport_company_id, transport_variant, num_cycles, cycle_dura_min),
) = row_z_parser(input).unwrap();
assert_eq!(123456, journey_id);
assert_eq!("000011", transport_company_id);
assert_eq!(101, transport_variant);
assert_eq!(Some(12), num_cycles);
assert_eq!(Some(60), cycle_dura_min);
assert_eq!(
res.trim(),
"% Fahrtnummer 123456, für TU 11 (SBB), mit Variante 101 (ignore), 12 mal, alle 60 Minuten"
);
}
}
mod row_g {
use super::*;
use pretty_assertions::assert_eq;
type GlineRow = (String, Option<i32>, Option<i32>);
fn row_g_parser(input: &str) -> PResult<(&str, GlineRow)> {
let (res, row_g) = row_g_combinator(input)?;
match row_g {
JourneyLines::Gline {
offer,
stop_from_id,
stop_to_id,
} => Ok((res, (offer, stop_from_id, stop_to_id))),
l => Err(format!("Gline expected but got {l:?}").as_str().into()),
}
}
#[test]
fn success_with_options() {
let input = "*G ICE 8500090 8503000 % Angebotskategorie ICE gilt ab HS-Nr. 8500090 bis HS-Nr. 8503000";
let (res, (offer, stop_from_id, stop_to_id)) = row_g_parser(input).unwrap();
assert_eq!("ICE", offer);
assert_eq!(Some(8500090), stop_from_id);
assert_eq!(Some(8503000), stop_to_id);
assert_eq!(
res.trim(),
"% Angebotskategorie ICE gilt ab HS-Nr. 8500090 bis HS-Nr. 8503000"
);
}
#[test]
fn success_no_options() {
let input = "*G ICE % Angebotskategorie ICE gilt ab HS-Nr. 8500090 bis HS-Nr. 8503000";
let (res, (offer, stop_from_id, stop_to_id)) = row_g_parser(input).unwrap();
assert_eq!("ICE", offer);
assert_eq!(None, stop_from_id);
assert_eq!(None, stop_to_id);
assert_eq!(
res.trim(),
"% Angebotskategorie ICE gilt ab HS-Nr. 8500090 bis HS-Nr. 8503000"
);
}
}
mod row_a_ve {
use super::*;
use pretty_assertions::assert_eq;
type AVElineRow = (Option<i32>, Option<i32>, Option<i32>);
fn row_a_ve_parser(input: &str) -> PResult<(&str, AVElineRow)> {
let (res, row_a_ve) = row_a_ve_combinator(input)?;
match row_a_ve {
JourneyLines::AVEline {
stop_from_id,
stop_to_id,
bit_field_id: reference,
} => Ok((res, (stop_from_id, stop_to_id, reference))),
l => Err(format!("AVEline expected but got {l:?}").as_str().into()),
}
}
#[test]
fn success_with_options() {
let input = "*A VE 8500090 8503000 001417 % Ab HS-Nr. 8500090 bis HS-Nr. 8503000, gelten die Gültigkeitstage 001417 (Bitfeld für bspw. alle Montage)";
let (res, (stop_from_id, stop_to_id, reference)) = row_a_ve_parser(input).unwrap();
assert_eq!(Some(8500090), stop_from_id);
assert_eq!(Some(8503000), stop_to_id);
assert_eq!(Some(1417), reference);
assert_eq!(
res.trim(),
"% Ab HS-Nr. 8500090 bis HS-Nr. 8503000, gelten die Gültigkeitstage 001417 (Bitfeld für bspw. alle Montage)"
);
}
#[test]
fn success_no_options() {
let input = "*A VE % Ab HS-Nr. 8500090 bis HS-Nr. 8503000, gelten die Gültigkeitstage 001417 (Bitfeld für bspw. alle Montage)";
let (res, (stop_from_id, stop_to_id, reference)) = row_a_ve_parser(input).unwrap();
assert_eq!(None, stop_from_id);
assert_eq!(None, stop_to_id);
assert_eq!(None, reference);
assert_eq!(
res.trim(),
"% Ab HS-Nr. 8500090 bis HS-Nr. 8503000, gelten die Gültigkeitstage 001417 (Bitfeld für bspw. alle Montage)"
);
}
}
mod row_a {
use super::*;
use pretty_assertions::assert_eq;
type AlineRow = (String, Option<i32>, Option<i32>, Option<i32>);
fn row_a_parser(input: &str) -> PResult<(&str, AlineRow)> {
let (res, row_a) = row_a_combinator(input)?;
match row_a {
JourneyLines::Aline {
offer,
stop_from_id,
stop_to_id,
reference,
} => Ok((res, (offer, stop_from_id, stop_to_id, reference))),
l => Err(format!("Aline expected but got {l:?}").as_str().into()),
}
}
#[test]
fn success_with_partial_options1() {
let input = "*A R 8500090 8503000 % Attribut R gilt ab HS-Nr. 8500090 bis HS-Nr. 8503000";
let (res, (offer, stop_from_id, stop_to_id, reference)) = row_a_parser(input).unwrap();
assert_eq!("R", offer);
assert_eq!(Some(8500090), stop_from_id);
assert_eq!(Some(8503000), stop_to_id);
assert_eq!(None, reference);
assert_eq!(
res.trim(),
"% Attribut R gilt ab HS-Nr. 8500090 bis HS-Nr. 8503000"
);
}
#[test]
fn success_partial_options() {
let input = "*A VR 8500090 8503000 % Attribut VR gilt ab HS-Nr. 8500090 bis HS-Nr. 8503000";
let (res, (offer, stop_from_id, stop_to_id, reference)) = row_a_parser(input).unwrap();
assert_eq!("VR", offer);
assert_eq!(Some(8500090), stop_from_id);
assert_eq!(Some(8503000), stop_to_id);
assert_eq!(None, reference);
assert_eq!(
res.trim(),
"% Attribut VR gilt ab HS-Nr. 8500090 bis HS-Nr. 8503000"
);
}
#[test]
fn success_with_options() {
let input = "*A WR 8500090 8503000 047873 % Attribut WR gilt ab HS-Nr. 8500090 bis HS-Nr. 8503000 mit den Gültigkeitstagen 047873";
let (res, (offer, stop_from_id, stop_to_id, reference)) = row_a_parser(input).unwrap();
assert_eq!("WR", offer);
assert_eq!(Some(8500090), stop_from_id);
assert_eq!(Some(8503000), stop_to_id);
assert_eq!(Some(47873), reference);
assert_eq!(
res.trim(),
"% Attribut WR gilt ab HS-Nr. 8500090 bis HS-Nr. 8503000 mit den Gültigkeitstagen 047873"
);
}
}
mod row_i {
use super::*;
use pretty_assertions::assert_eq;
type IlineRow = (
String,
Option<i32>,
Option<i32>,
Option<i32>,
i32,
Option<i32>,
Option<i32>,
);
fn row_i_parser(input: &str) -> PResult<(&str, IlineRow)> {
let (res, row_i) = row_i_combinator(input)?;
match row_i {
JourneyLines::Iline {
info_code,
stop_from_id,
stop_to_id,
validity_ref,
info_ref,
departure_time,
arrival_time,
} => Ok((
res,
(
info_code,
stop_from_id,
stop_to_id,
validity_ref,
info_ref,
departure_time,
arrival_time,
),
)),
l => Err(format!("Iline expected but got {l:?}").as_str().into()),
}
}
#[test]
fn success_with_partial_options() {
let input = "*I hi 8573602 8587744 000018040 % Hinweis auf Infotext (hi) ab HS-Nr. 8573602 bis HS-Nr. 8587744 mit Infotext 18040";
let (
res,
(
info_code,
stop_from_id,
stop_to_id,
validity_ref,
info_ref,
departure_time,
arrival_time,
),
) = row_i_parser(input).unwrap();
assert_eq!("hi", info_code);
assert_eq!(Some(8573602), stop_from_id);
assert_eq!(Some(8587744), stop_to_id);
assert_eq!(None, validity_ref);
assert_eq!(18040, info_ref);
assert_eq!(None, departure_time);
assert_eq!(None, arrival_time);
assert_eq!(
res.trim(),
"% Hinweis auf Infotext (hi) ab HS-Nr. 8573602 bis HS-Nr. 8587744 mit Infotext 18040"
);
}
#[test]
fn success_with_options() {
let input = "*I hi 8578157 8589334 000018037 01126 01159 % Hinweis auf Infotext (hi) ab HS-Nr. 8578157 bis HS-Nr. 8589334 mit Infotext 18037 Abfahrt 11:26 Ankunft 11:59";
let (
res,
(
info_code,
stop_from_id,
stop_to_id,
validity_ref,
info_ref,
departure_time,
arrival_time,
),
) = row_i_parser(input).unwrap();
assert_eq!("hi", info_code);
assert_eq!(Some(8578157), stop_from_id);
assert_eq!(Some(8589334), stop_to_id);
assert_eq!(None, validity_ref);
assert_eq!(18037, info_ref);
assert_eq!(Some(1126), departure_time);
assert_eq!(Some(1159), arrival_time);
assert_eq!(
res.trim(),
"% Hinweis auf Infotext (hi) ab HS-Nr. 8578157 bis HS-Nr. 8589334 mit Infotext 18037 Abfahrt 11:26 Ankunft 11:59"
);
}
}
mod row_l {
use super::*;
use pretty_assertions::assert_eq;
type LlineRow = (String, Option<i32>, Option<i32>, Option<i32>, Option<i32>);
fn row_l_parser(input: &str) -> PResult<(&str, LlineRow)> {
let (res, row_l) = row_l_combinator(input)?;
match row_l {
JourneyLines::Lline {
line_info,
stop_from_id,
stop_to_id,
departure_time,
arrival_time,
} => Ok((
res,
(
line_info,
stop_from_id,
stop_to_id,
departure_time,
arrival_time,
),
)),
l => Err(format!("Lline expected but got {l:?}").as_str().into()),
}
}
#[test]
fn success_with_options() {
let input = "*L 8 8578157 8589334 01126 01159 % Linie 8 ab HS-Nr. 8578157 bis HS-Nr. 8589334 Abfahrt 11:26 Ankunft 11:59";
let (res, (line_info, stop_from_id, stop_to_id, departure_time, arrival_time)) =
row_l_parser(input).unwrap();
assert_eq!("8", line_info);
assert_eq!(Some(8578157), stop_from_id);
assert_eq!(Some(8589334), stop_to_id);
assert_eq!(Some(1126), departure_time);
assert_eq!(Some(1159), arrival_time);
assert_eq!(
"% Linie 8 ab HS-Nr. 8578157 bis HS-Nr. 8589334 Abfahrt 11:26 Ankunft 11:59",
res.trim()
);
}
#[test]
fn success_with_partial_options() {
let input = "*L #0000022 8589601 8589913 % Referenz auf Linie No. 22 ab HS-Nr. 8589601 bis HS-Nr. 8589913";
let (res, (line_info, stop_from_id, stop_to_id, departure_time, arrival_time)) =
row_l_parser(input).unwrap();
assert_eq!("#0000022", line_info);
assert_eq!(Some(8589601), stop_from_id);
assert_eq!(Some(8589913), stop_to_id);
assert_eq!(None, departure_time);
assert_eq!(None, arrival_time);
assert_eq!(
"% Referenz auf Linie No. 22 ab HS-Nr. 8589601 bis HS-Nr. 8589913",
res.trim()
);
}
#[test]
fn success_with_partial_options61() {
let input = "*L 61 8500010 8507492 %";
let (res, (line_info, stop_from_id, stop_to_id, departure_time, arrival_time)) =
row_l_parser(input).unwrap();
assert_eq!("61", line_info);
assert_eq!(Some(8500010), stop_from_id);
assert_eq!(Some(8507492), stop_to_id);
assert_eq!(None, departure_time);
assert_eq!(None, arrival_time);
assert_eq!("%", res.trim());
}
}
mod row_r {
use super::*;
use pretty_assertions::assert_eq;
type RlineRow = (
String,
String,
Option<i32>,
Option<i32>,
Option<i32>,
Option<i32>,
);
fn row_r_parser(input: &str) -> PResult<(&str, RlineRow)> {
let (res, row_r) = row_r_combinator(input)?;
match row_r {
JourneyLines::Rline {
direction,
ref_direction_code,
stop_from_id,
stop_to_id,
departure_time,
arrival_time,
} => {
Ok((
res.trim(), (
direction,
ref_direction_code,
stop_from_id,
stop_to_id,
departure_time,
arrival_time,
),
))
}
l => Err(format!("Rline expected but got {l:?}").as_str().into()),
}
}
#[test]
fn success_with_options() {
let input = "*R R R000063 1300146 8574808 % gilt für Rück-Richtung 63 ab HS-Nr. 1300146 bis HS-Nr. 8574808";
let (
res, (
direction,
ref_direction_code,
stop_from_id,
stop_to_id,
departure_time,
arrival_time,
),
) = row_r_parser(input).unwrap();
assert_eq!("R", direction);
assert_eq!("R63", ref_direction_code);
assert_eq!(Some(1300146), stop_from_id);
assert_eq!(Some(8574808), stop_to_id);
assert_eq!(None, departure_time);
assert_eq!(None, arrival_time);
assert_eq!(
"% gilt für Rück-Richtung 63 ab HS-Nr. 1300146 bis HS-Nr. 8574808",
res
);
}
#[test]
fn success_with_partial_options() {
let input =
"*R H % gilt für die gesamte Hin-Richtung";
let (
res, (
direction,
ref_direction_code,
stop_from_id,
stop_to_id,
departure_time,
arrival_time,
),
) = row_r_parser(input).unwrap();
assert_eq!("H", direction);
assert_eq!("", ref_direction_code);
assert_eq!(None, stop_from_id);
assert_eq!(None, stop_to_id);
assert_eq!(None, departure_time);
assert_eq!(None, arrival_time);
assert_eq!("% gilt für die gesamte Hin-Richtung", res);
}
}
mod row_ci_co {
use super::*;
use pretty_assertions::assert_eq;
type CiCoLine<'a> = (
&'a str,
i32,
Option<i32>,
Option<i32>,
Option<i32>,
Option<i32>,
);
fn row_ci_co_parser<'a>(input: &'a str) -> PResult<(&'a str, CiCoLine<'a>)> {
let (res, row_ci_co) = row_ci_co_combinator(input)?;
match row_ci_co {
JourneyLines::CiLine {
num_minutes,
stop_from_id,
stop_to_id,
departure_time,
arrival_time,
} => Ok((
res,
(
"*CI",
num_minutes,
stop_from_id,
stop_to_id,
departure_time,
arrival_time,
),
)),
JourneyLines::CoLine {
num_minutes,
stop_from_id,
stop_to_id,
departure_time,
arrival_time,
} => Ok((
res,
(
"*CO",
num_minutes,
stop_from_id,
stop_to_id,
departure_time,
arrival_time,
),
)),
l => Err(format!("Rline expected but got {l:?}").as_str().into()),
}
}
#[test]
fn success_ci_options() {
let input = "*CI 0002 8507000 8507000 % Check-in 2 Min. ab HS-Nr. 8507000 bis HS-Nr. 8507000";
let (res, (ci_co, num_minutes, stop_from_id, stop_to_id, departure_time, arrival_time)) =
row_ci_co_parser(input).unwrap();
assert_eq!("*CI", ci_co);
assert_eq!(2, num_minutes);
assert_eq!(Some(8507000), stop_from_id);
assert_eq!(Some(8507000), stop_to_id);
assert_eq!(None, departure_time);
assert_eq!(None, arrival_time);
assert_eq!(
"% Check-in 2 Min. ab HS-Nr. 8507000 bis HS-Nr. 8507000",
res.trim()
);
}
#[test]
fn success_with_partial_options() {
let input = "*CO 0002 8507000 8507000 % Check-out 2 Min. ab HS-Nr. 8507000 bis HS-Nr. 8507000";
let (res, (ci_co, num_minutes, stop_from_id, stop_to_id, departure_time, arrival_time)) =
row_ci_co_parser(input).unwrap();
assert_eq!("*CO", ci_co);
assert_eq!(2, num_minutes);
assert_eq!(Some(8507000), stop_from_id);
assert_eq!(Some(8507000), stop_to_id);
assert_eq!(None, departure_time);
assert_eq!(None, arrival_time);
assert_eq!(
"% Check-out 2 Min. ab HS-Nr. 8507000 bis HS-Nr. 8507000",
res.trim()
);
}
}
mod row_journey_description {
type JourneyDescriptorRow = (i32, String, Option<i32>, Option<i32>, Option<i32>, String);
fn row_journey_description_parser(input: &str) -> PResult<(&str, JourneyDescriptorRow)> {
let (res, row_j) = row_journey_description_combinator(input)?;
match row_j {
JourneyLines::JourneyLine {
stop_id,
stop_name,
arrival_time,
departure_time,
journey_id,
administration,
} => Ok((
res,
(
stop_id,
stop_name,
arrival_time,
departure_time,
journey_id,
administration,
),
)),
l => Err(format!("Rline expected but got {l:?}").as_str().into()),
}
}
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn success_journey_options1() {
let input = "0053301 S Wannsee DB 02014 % HS-Nr. 0053301 Ankunft N/A, Abfahrt 20:14";
let (
res,
(stop_id, stop_name, arrival_time, departure_time, journey_id, administration),
) = row_journey_description_parser(input).unwrap();
assert_eq!(53301, stop_id);
assert_eq!("S Wannsee DB", stop_name);
assert_eq!(None, arrival_time);
assert_eq!(Some(2014), departure_time);
assert_eq!(None, journey_id);
assert_eq!("", administration);
assert_eq!("% HS-Nr. 0053301 Ankunft N/A, Abfahrt 20:14", res.trim());
}
#[test]
fn success_journey_options2() {
let input = "0053202 Am Kl. Wannsee/Am Gr 02016 02016 %";
let (
res,
(stop_id, stop_name, arrival_time, departure_time, journey_id, administration),
) = row_journey_description_parser(input).unwrap();
assert_eq!(53202, stop_id);
assert_eq!("Am Kl. Wannsee/Am Gr", stop_name);
assert_eq!(Some(2016), arrival_time);
assert_eq!(Some(2016), departure_time);
assert_eq!(None, journey_id);
assert_eq!("", administration);
assert_eq!("%", res.trim());
}
#[test]
fn success_journey_all_options() {
let input = "0053291 Wannseebrücke 02015 02015 052344 80____ % HS-Nr. 0053291 Ankunft 20:15, Abfahrt 20:15, Fahrtnummer 052344, Verwaltung 80____ (DB)";
let (
res,
(stop_id, stop_name, arrival_time, departure_time, journey_id, administration),
) = row_journey_description_parser(input).unwrap();
assert_eq!(53291, stop_id);
assert_eq!("Wannseebrücke", stop_name);
assert_eq!(Some(2015), arrival_time);
assert_eq!(Some(2015), departure_time);
assert_eq!(Some(52344), journey_id);
assert_eq!("80____", administration);
assert_eq!(
"% HS-Nr. 0053291 Ankunft 20:15, Abfahrt 20:15, Fahrtnummer 052344, Verwaltung 80____ (DB)",
res.trim()
);
}
#[test]
fn success_journey_negative_time() {
let input = "0000175 Hauenstein-Basistunn -00833 -00833 %";
let (
res,
(stop_id, stop_name, arrival_time, departure_time, journey_id, administration),
) = row_journey_description_parser(input).unwrap();
assert_eq!(175, stop_id);
assert_eq!("Hauenstein-Basistunn", stop_name);
assert_eq!(Some(-833), arrival_time);
assert_eq!(Some(-833), departure_time);
assert_eq!(None, journey_id);
assert_eq!("", administration);
assert_eq!("%", res.trim());
}
}
}