use std::path::Path;
use nom::{
IResult, Parser,
branch::alt,
bytes::complete::{tag, take_until},
character::complete::{digit1, i16, i32, space1},
combinator::{map, map_res},
multi::many0,
number::complete::double,
sequence::{preceded, terminated},
};
use rustc_hash::FxHashMap;
use crate::{
error::{HResult, HrdfError},
models::{CoordinateSystem, Coordinates, Stop, Version},
parsing::{
error::{PResult, ParsingError},
helpers::{read_lines, string_from_n_chars_parser, string_till_eol_parser},
},
storage::ResourceStorage,
};
type StopStorageAndExchangeTimes = (ResourceStorage<Stop>, (i16, i16));
struct StopLine {
stop_id: i32,
designation: String,
long_name: Option<String>,
abbreviation: Option<String>,
synonyms: Option<Vec<String>>,
}
struct CoordLine {
stop_id: i32,
x: f64,
y: f64,
#[allow(unused)]
altitude: f64,
}
struct PriosLine {
stop_id: i32,
exchange_priority: i16,
#[allow(unused)]
name: String,
}
struct FlagsLine {
stop_id: i32,
exchange_flag: i16,
}
struct TimesLines {
stop_id: i32,
exchange_time_inter_city: i16,
exchange_time_other: i16,
}
enum DescriptionLine {
Comment,
Restriction {
stop_id: i32,
restrictions: i16,
},
Sloid {
stop_id: i32,
sloid: String,
},
Boarding {
stop_id: i32,
sloid: String,
},
Country {
#[allow(unused)]
stop_id: i32,
#[allow(unused)]
country_code: String,
},
Canton {
#[allow(unused)]
stop_id: i32,
#[allow(unused)]
canton_id: i32,
},
}
fn comment_combinator(input: &str) -> IResult<&str, DescriptionLine> {
map(tag("%"), |_| DescriptionLine::Comment).parse(input)
}
fn restriction_combinator(input: &str) -> IResult<&str, DescriptionLine> {
map(
(
i32,
preceded(preceded(space1, tag("B")), preceded(space1, i16)),
),
|(stop_id, restrictions)| DescriptionLine::Restriction {
stop_id,
restrictions,
},
)
.parse(input)
}
fn sloid_combinator(input: &str) -> IResult<&str, DescriptionLine> {
map(
(
i32,
preceded(
preceded(space1, tag("G A")),
preceded(space1, string_till_eol_parser),
),
),
|(stop_id, sloid)| DescriptionLine::Sloid { stop_id, sloid },
)
.parse(input)
}
fn boarding_combinator(input: &str) -> IResult<&str, DescriptionLine> {
map(
(
i32,
preceded(
preceded(space1, tag("G a")),
preceded(space1, string_till_eol_parser),
),
),
|(stop_id, sloid)| DescriptionLine::Boarding { stop_id, sloid },
)
.parse(input)
}
fn country_combinator(input: &str) -> IResult<&str, DescriptionLine> {
map(
(
i32,
preceded(
preceded(space1, tag("L")),
preceded(space1, string_from_n_chars_parser(2)),
),
),
|(stop_id, country_code)| DescriptionLine::Country {
stop_id,
country_code,
},
)
.parse(input)
}
fn canton_combinator(input: &str) -> IResult<&str, DescriptionLine> {
map(
(
i32,
preceded(preceded(space1, tag("I KT")), preceded(space1, i32)),
),
|(stop_id, canton_id)| DescriptionLine::Canton { stop_id, canton_id },
)
.parse(input)
}
fn parse_description_line(line: &str, stops: &mut FxHashMap<i32, Stop>) -> PResult<()> {
let (_, description_line) = alt((
comment_combinator,
restriction_combinator,
sloid_combinator,
boarding_combinator,
country_combinator,
canton_combinator,
))
.parse(line)?;
match description_line {
DescriptionLine::Comment => {
}
DescriptionLine::Restriction {
stop_id,
restrictions,
} => {
if let Some(stop) = stops.get_mut(&stop_id) {
stop.set_restrictions(restrictions);
} else {
log::info!("Unknown stop ID: {stop_id} for restrictions");
}
}
DescriptionLine::Sloid { stop_id, sloid } => {
if let Some(stop) = stops.get_mut(&stop_id) {
stop.set_sloid(sloid);
} else {
log::info!("Unknown stop ID: {stop_id} for sloid");
}
}
DescriptionLine::Boarding { stop_id, sloid } => {
if let Some(stop) = stops.get_mut(&stop_id) {
stop.add_boarding_area(sloid);
} else {
log::info!("Unknown stop ID: {stop_id} for boarding area");
}
}
DescriptionLine::Country {
stop_id: _,
country_code: _,
} => {
}
DescriptionLine::Canton {
stop_id: _,
canton_id: _,
} => {
}
}
Ok(())
}
fn designation_number_combinator(input: &str) -> IResult<&str, i8> {
map_res(
terminated(preceded(tag("$<"), digit1), tag(">")),
|num: &str| num.parse::<i8>(),
)
.parse(input)
}
fn station_combinator(input: &str) -> IResult<&str, StopLine> {
map_res(
(
i32,
preceded(space1, map(take_until("$<"), |s: &str| String::from(s))),
designation_number_combinator,
many0((
preceded(tag("$"), take_until("$<")),
designation_number_combinator,
)),
),
|(stop_id, designation, num, optional_designations)| {
if num != 1 {
Err(format!("Error: absent principal name, got {num} instead"))
} else {
let mut long_name = None;
let mut abbreviation = None;
let mut synonyms = Vec::new();
for (d, tag) in optional_designations {
if tag == 2 {
long_name = Some(String::from(d));
} else if tag == 3 {
abbreviation = Some(String::from(d));
} else if tag == 4 {
synonyms.push(String::from(d))
} else {
return Err(format!(
"Error: invalid num must be in range [1, 4], got {tag} instead"
));
}
}
Ok(StopLine {
stop_id,
designation,
long_name,
abbreviation,
synonyms: if synonyms.is_empty() {
None
} else {
Some(synonyms)
},
})
}
},
)
.parse(input)
}
fn coordinates_combinator(input: &str) -> IResult<&str, CoordLine> {
map(
(
i32,
preceded(space1, double),
preceded(space1, double),
preceded(space1, double),
),
|(stop_id, x, y, altitude)| CoordLine {
stop_id,
x,
y,
altitude,
},
)
.parse(input)
}
fn prios_combinator(input: &str) -> IResult<&str, PriosLine> {
map(
(
i32,
preceded(space1, i16),
preceded(space1, string_till_eol_parser),
),
|(stop_id, exchange_priority, name)| PriosLine {
stop_id,
exchange_priority,
name,
},
)
.parse(input)
}
fn flags_combinator(input: &str) -> IResult<&str, FlagsLine> {
map((i32, preceded(space1, i16)), |(stop_id, exchange_flag)| {
FlagsLine {
stop_id,
exchange_flag,
}
})
.parse(input)
}
fn times_combinator(input: &str) -> IResult<&str, TimesLines> {
map(
(i32, preceded(space1, i16), preceded(space1, i16)),
|(stop_id, exchange_time_inter_city, exchange_time_other)| TimesLines {
stop_id,
exchange_time_inter_city,
exchange_time_other,
},
)
.parse(input)
}
fn parse_stop_line(line: &str, stops: &mut FxHashMap<i32, Stop>) -> PResult<()> {
let (
_,
StopLine {
stop_id,
designation,
long_name,
abbreviation,
synonyms,
},
) = station_combinator.parse(line)?;
stops.insert(
stop_id,
Stop::new(stop_id, designation, long_name, abbreviation, synonyms),
);
Ok(())
}
fn parse_coord_line(
line: &str,
stops: &mut FxHashMap<i32, Stop>,
coordinate_system: CoordinateSystem,
) -> PResult<()> {
let (
_,
CoordLine {
stop_id,
x,
y,
altitude: _, },
) = coordinates_combinator.parse(line)?;
let stop = stops
.get_mut(&stop_id)
.ok_or_else(|| ParsingError::UnknownId(format!("Unknown stop ID {stop_id}")))?;
match coordinate_system {
CoordinateSystem::LV95 => {
stop.set_lv95_coordinates(Coordinates::new(coordinate_system, x, y))
}
CoordinateSystem::WGS84 => {
stop.set_wgs84_coordinates(Coordinates::new(coordinate_system, y, x))
}
}
Ok(())
}
fn parse_prios_line(line: &str, stops: &mut FxHashMap<i32, Stop>) -> PResult<()> {
let (
_,
PriosLine {
stop_id,
exchange_priority,
name: _,
},
) = prios_combinator.parse(line)?;
let stop = stops
.get_mut(&stop_id)
.ok_or_else(|| ParsingError::UnknownId(format!("Unknown stop ID {stop_id}")))?;
stop.set_exchange_priority(exchange_priority);
Ok(())
}
fn parse_flags_line(line: &str, stops: &mut FxHashMap<i32, Stop>) -> PResult<()> {
let (
_,
FlagsLine {
stop_id,
exchange_flag,
},
) = flags_combinator.parse(line)?;
let stop = stops
.get_mut(&stop_id)
.ok_or_else(|| ParsingError::UnknownId(format!("Unknown stop ID {stop_id}")))?;
stop.set_exchange_flag(exchange_flag);
Ok(())
}
fn parse_times_line(line: &str, stops: &mut FxHashMap<i32, Stop>) -> PResult<Option<(i16, i16)>> {
let (
_,
TimesLines {
stop_id,
exchange_time_inter_city,
exchange_time_other,
},
) = times_combinator.parse(line)?;
let exchange_time = Some((exchange_time_inter_city, exchange_time_other));
if stop_id == 9999999 {
Ok(exchange_time)
} else {
let stop = stops
.get_mut(&stop_id)
.ok_or_else(|| ParsingError::UnknownId(format!("Unknown Stop ID {stop_id}")))?;
stop.set_exchange_time(exchange_time);
Ok(None)
}
}
pub fn parse(version: Version, path: &Path) -> HResult<StopStorageAndExchangeTimes> {
log::info!("Parsing BAHNHOF...");
let mut stops = FxHashMap::default();
let file = path.join("BAHNHOF");
read_lines(&file, 0)?
.into_iter()
.enumerate()
.filter(|(_, line)| !line.trim().is_empty())
.try_for_each(|(line_number, line)| {
parse_stop_line(&line, &mut stops).map_err(|e| HrdfError::Parsing {
error: e,
file: String::from(file.to_string_lossy()),
line,
line_number,
})
})?;
log::info!("Parsing BFKOORD_LV95...");
let file = path.join("BFKOORD_LV95");
read_lines(&file, 0)?
.into_iter()
.enumerate()
.filter(|(_, line)| !line.trim().is_empty())
.try_for_each(|(line_number, line)| {
parse_coord_line(&line, &mut stops, CoordinateSystem::LV95).map_err(|e| {
HrdfError::Parsing {
error: e,
file: String::from(file.to_string_lossy()),
line,
line_number,
}
})
})?;
let file = path.join("BFKOORD_WGS");
log::info!("Parsing BFKOORD_WGS...");
read_lines(&file, 0)?
.into_iter()
.enumerate()
.filter(|(_, line)| !line.trim().is_empty())
.try_for_each(|(line_number, line)| {
parse_coord_line(&line, &mut stops, CoordinateSystem::WGS84).map_err(|e| {
HrdfError::Parsing {
error: e,
file: String::from(file.to_string_lossy()),
line,
line_number,
}
})
})?;
log::info!("Parsing BFPRIOS...");
let file = path.join("BFPRIOS");
read_lines(&file, 0)?
.into_iter()
.enumerate()
.filter(|(_, line)| !line.trim().is_empty())
.try_for_each(|(line_number, line)| {
parse_prios_line(&line, &mut stops).map_err(|e| HrdfError::Parsing {
error: e,
file: String::from(file.to_string_lossy()),
line,
line_number,
})
})?;
log::info!("Parsing KMINFO...");
let file = path.join("KMINFO");
read_lines(&file, 0)?
.into_iter()
.enumerate()
.filter(|(_, line)| !line.trim().is_empty())
.try_for_each(|(line_number, line)| {
parse_flags_line(&line, &mut stops).map_err(|e| HrdfError::Parsing {
error: e,
file: String::from(file.to_string_lossy()),
line,
line_number,
})
})?;
log::info!("Parsing UMSTEIGB...");
let file = path.join("UMSTEIGB");
let default_exchange_time = read_lines(&file, 0)?
.into_iter()
.filter(|line| !line.trim().is_empty())
.map(|line| parse_times_line(&line, &mut stops))
.try_fold(None, |acc, curr| match (curr, acc) {
(Err(e), _) => Err(e),
(Ok(None), None) => Ok(None),
(_, Some(w)) => Ok(Some(w)),
(Ok(Some(v)), None) => Ok(Some(v)),
})
.map_err(|e| HrdfError::Parsing {
error: e,
file: String::from(file.to_string_lossy()),
line: String::default(),
line_number: 0,
})?
.ok_or(ParsingError::MissingDefaultExchangeTime)
.map_err(|e| HrdfError::Parsing {
error: e,
file: String::from(file.to_string_lossy()),
line: String::default(),
line_number: 0,
})?;
let bhfart = match version {
Version::V_5_40_41_2_0_4 | Version::V_5_40_41_2_0_5 | Version::V_5_40_41_2_0_6 => {
Ok("BHFART_60")
}
Version::V_5_40_41_2_0_7 => Ok("BHFART"),
_ => Err(HrdfError::SupportedVersion(version)),
}?;
log::info!("Parsing {bhfart}...");
let file = path.join(bhfart);
read_lines(&file, 0)?
.into_iter()
.enumerate()
.filter(|(_, line)| !line.trim().is_empty())
.try_for_each(|(line_number, line)| {
parse_description_line(&line, &mut stops).map_err(|e| HrdfError::Parsing {
error: e,
file: String::from(file.to_string_lossy()),
line,
line_number,
})
})?;
Ok((ResourceStorage::new(stops), default_exchange_time))
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_station_combinator_basic() {
let input = "8500010 Basel SBB$<1>";
let result = station_combinator(input);
assert!(result.is_ok());
let (_, stop_line) = result.unwrap();
assert_eq!(stop_line.stop_id, 8500010);
assert_eq!(stop_line.designation, "Basel SBB");
assert!(stop_line.long_name.is_none());
assert!(stop_line.abbreviation.is_none());
}
#[test]
fn test_station_combinator_with_abbreviation() {
let input = "8500010 Basel SBB$<1>$BS$<3>";
let result = station_combinator(input);
assert!(result.is_ok());
let (_, stop_line) = result.unwrap();
assert_eq!(stop_line.stop_id, 8500010);
assert_eq!(stop_line.designation, "Basel SBB");
assert_eq!(stop_line.abbreviation, Some("BS".to_string()));
}
#[test]
fn test_station_combinator_with_all_fields() {
let input = "8501212 Chavannes-R., UNIL-Mouline$<1>$Chavannes-près-Renens, UNIL-Mouline$<2>$MOUI$<3>";
let result = station_combinator(input);
assert!(result.is_ok());
let (_, stop_line) = result.unwrap();
assert_eq!(stop_line.stop_id, 8501212);
assert_eq!(stop_line.designation, "Chavannes-R., UNIL-Mouline");
assert_eq!(
stop_line.long_name,
Some("Chavannes-près-Renens, UNIL-Mouline".to_string())
);
assert_eq!(stop_line.abbreviation, Some("MOUI".to_string()));
}
#[test]
fn test_station_combinator_auxiliary_stop() {
let input = "0000022 Basel$<1>";
let result = station_combinator(input);
assert!(result.is_ok());
let (_, stop_line) = result.unwrap();
assert_eq!(stop_line.stop_id, 22);
assert_eq!(stop_line.designation, "Basel");
}
#[test]
fn test_coordinates_combinator_basic() {
let input = "8500010 2611363 1266310 0";
let result = coordinates_combinator(input);
assert!(result.is_ok());
let (_, coord_line) = result.unwrap();
assert_eq!(coord_line.stop_id, 8500010);
assert_eq!(coord_line.x, 2611363.0);
assert_eq!(coord_line.y, 1266310.0);
assert_eq!(coord_line.altitude, 0.0);
}
#[test]
fn test_coordinates_combinator_with_decimals() {
let input = "8500010 7.589563 47.547412 0";
let result = coordinates_combinator(input);
assert!(result.is_ok());
let (_, coord_line) = result.unwrap();
assert_eq!(coord_line.stop_id, 8500010);
assert_eq!(coord_line.x, 7.589563);
assert_eq!(coord_line.y, 47.547412);
}
#[test]
fn test_prios_combinator() {
let input = "8500010 4 Basel SBB";
let result = prios_combinator(input);
assert!(result.is_ok());
let (_, prios_line) = result.unwrap();
assert_eq!(prios_line.stop_id, 8500010);
assert_eq!(prios_line.exchange_priority, 4);
}
#[test]
fn test_prios_combinator_high_priority() {
let input = "8500009 16 Pregassona, Scuola Media";
let result = prios_combinator(input);
assert!(result.is_ok());
let (_, prios_line) = result.unwrap();
assert_eq!(prios_line.stop_id, 8500009);
assert_eq!(prios_line.exchange_priority, 16);
}
#[test]
fn test_flags_combinator() {
let input = "8500009 30 Pregassona, Scuola Media";
let result = flags_combinator(input);
assert!(result.is_ok());
let (_, flags_line) = result.unwrap();
assert_eq!(flags_line.stop_id, 8500009);
assert_eq!(flags_line.exchange_flag, 30);
}
#[test]
fn test_flags_combinator_large_value() {
let input = "8500010 5000 Basel SBB";
let result = flags_combinator(input);
assert!(result.is_ok());
let (_, flags_line) = result.unwrap();
assert_eq!(flags_line.stop_id, 8500010);
assert_eq!(flags_line.exchange_flag, 5000);
}
#[test]
fn test_times_combinator_standard() {
let input = "9999999 02 02 STANDARD";
let result = times_combinator(input);
assert!(result.is_ok());
let (_, times_line) = result.unwrap();
assert_eq!(times_line.stop_id, 9999999);
assert_eq!(times_line.exchange_time_inter_city, 2);
assert_eq!(times_line.exchange_time_other, 2);
}
#[test]
fn test_times_combinator_specific_stop() {
let input = "8500010 05 05 Basel SBB";
let result = times_combinator(input);
assert!(result.is_ok());
let (_, times_line) = result.unwrap();
assert_eq!(times_line.stop_id, 8500010);
assert_eq!(times_line.exchange_time_inter_city, 5);
assert_eq!(times_line.exchange_time_other, 5);
}
#[test]
fn test_comment_combinator() {
let input = "% This is a comment";
let result = comment_combinator(input);
assert!(result.is_ok());
let (_, desc_line) = result.unwrap();
assert!(matches!(desc_line, DescriptionLine::Comment));
}
#[test]
fn test_restriction_combinator() {
let input = "0000132 B 3";
let result = restriction_combinator(input);
assert!(result.is_ok());
let (_, desc_line) = result.unwrap();
match desc_line {
DescriptionLine::Restriction {
stop_id,
restrictions,
} => {
assert_eq!(stop_id, 132);
assert_eq!(restrictions, 3);
}
_ => panic!("Expected Restriction variant"),
}
}
#[test]
fn test_sloid_combinator() {
let input = "8500010 G A ch:1:sloid:10";
let result = sloid_combinator(input);
assert!(result.is_ok());
let (_, desc_line) = result.unwrap();
match desc_line {
DescriptionLine::Sloid { stop_id, sloid } => {
assert_eq!(stop_id, 8500010);
assert_eq!(sloid, "ch:1:sloid:10");
}
_ => panic!("Expected Sloid variant"),
}
}
#[test]
fn test_boarding_combinator() {
let input = "8500010 G a ch:1:sloid:10:3:5";
let result = boarding_combinator(input);
assert!(result.is_ok());
let (_, desc_line) = result.unwrap();
match desc_line {
DescriptionLine::Boarding { stop_id, sloid } => {
assert_eq!(stop_id, 8500010);
assert_eq!(sloid, "ch:1:sloid:10:3:5");
}
_ => panic!("Expected Boarding variant"),
}
}
#[test]
fn test_parse_stop_line_creates_stop() {
let mut stops = FxHashMap::default();
let result = parse_stop_line("8500010 Basel SBB$<1>", &mut stops);
assert!(result.is_ok());
assert_eq!(stops.len(), 1);
let stop = stops.get(&8500010).unwrap();
assert_eq!(stop.name(), "Basel SBB");
}
#[test]
fn test_parse_coord_line_sets_coordinates() {
let mut stops = FxHashMap::default();
stops.insert(
8500010,
Stop::new(8500010, "Basel SBB".to_string(), None, None, None),
);
let result = parse_coord_line(
"8500010 7.589563 47.547412 0",
&mut stops,
CoordinateSystem::WGS84,
);
assert!(result.is_ok());
let stop = stops.get(&8500010).unwrap();
assert!(stop.wgs84_coordinates().is_some());
}
#[test]
fn test_parse_prios_line_sets_priority() {
let mut stops = FxHashMap::default();
stops.insert(
8500010,
Stop::new(8500010, "Basel SBB".to_string(), None, None, None),
);
let result = parse_prios_line("8500010 4 Basel SBB", &mut stops);
assert!(result.is_ok());
}
#[test]
fn test_parse_flags_line_sets_flag() {
let mut stops = FxHashMap::default();
stops.insert(
8500010,
Stop::new(8500010, "Basel SBB".to_string(), None, None, None),
);
let result = parse_flags_line("8500010 5000 Basel SBB", &mut stops);
assert!(result.is_ok());
}
#[test]
fn test_parse_times_line_sets_exchange_time() {
let mut stops = FxHashMap::default();
stops.insert(
8500010,
Stop::new(8500010, "Basel SBB".to_string(), None, None, None),
);
let result = parse_times_line("8500010 05 05 Basel SBB", &mut stops);
assert!(result.is_ok());
let stop = stops.get(&8500010).unwrap();
assert_eq!(stop.exchange_time(), Some((5, 5)));
}
#[test]
fn test_parse_times_line_default_sets_none() {
let mut stops = FxHashMap::default();
let result = parse_times_line("9999999 02 02 STANDARD", &mut stops);
assert!(result.is_ok());
assert_eq!(stops.len(), 0);
}
}