use crate::{Error, Result};
pub(crate) fn parse_finite(raw: &str, context: &'static str) -> Result<f64> {
let value: f64 = raw.trim().parse().map_err(|_| Error::InvalidNumber {
context,
value: raw.to_owned(),
})?;
if !value.is_finite() {
return Err(Error::NonFiniteNumber {
context,
value: raw.to_owned(),
});
}
Ok(value)
}
#[cfg(feature = "write")]
pub(crate) fn format_finite(value: f64) -> String {
value.to_string()
}
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub struct Point {
pub x: f64,
pub y: f64,
pub z: f64,
}
impl Point {
pub(crate) fn parse(raw: &str, context: &'static str) -> Result<Self> {
let mut parts = raw.split(',');
let mut next_coordinate = || -> Result<f64> {
let part = parts.next().ok_or(Error::IncompletePosition {
context,
value: raw.to_owned(),
})?;
parse_finite(part, context)
};
let x = next_coordinate()?;
let y = next_coordinate()?;
let z = match parts.next() {
Some(z) => parse_finite(z, context)?,
None => 0.0,
};
if parts.next().is_some() {
return Err(Error::TooManyCoordinates {
context,
value: raw.to_owned(),
});
}
Ok(Point { x, y, z })
}
#[cfg(feature = "write")]
pub(crate) fn format(&self) -> String {
if self.z == 0.0 {
format!("{},{}", format_finite(self.x), format_finite(self.y))
} else {
format!(
"{},{},{}",
format_finite(self.x),
format_finite(self.y),
format_finite(self.z)
)
}
}
}
impl TryFrom<&str> for Point {
type Error = Error;
fn try_from(raw: &str) -> Result<Self> {
Point::parse(raw, "SUMO position")
}
}
pub(crate) fn split_ids<T: From<String>>(raw: &str) -> Vec<T> {
raw.split_whitespace()
.map(|id| T::from(id.to_owned()))
.collect()
}
pub(crate) fn split_ids_opt<T: From<String>>(raw: Option<&str>) -> Vec<T> {
raw.map(split_ids).unwrap_or_default()
}
#[cfg(feature = "write")]
pub(crate) fn join_ids<T: AsRef<str>>(ids: &[T]) -> String {
ids.iter().map(AsRef::as_ref).collect::<Vec<_>>().join(" ")
}
#[cfg(feature = "write")]
pub(crate) fn join_ids_opt<T: AsRef<str>>(ids: &[T]) -> Option<String> {
(!ids.is_empty()).then(|| join_ids(ids))
}
pub(crate) fn parse_bool(value: crate::schema::BoolType) -> Result<bool> {
use crate::schema::BoolType as S;
match value {
S::true_ | S::True | S::yes | S::on | S::_1 => Ok(true),
S::false_ | S::False | S::no | S::off | S::_0 => Ok(false),
S::x | S::Dash => Err(Error::AmbiguousBool),
}
}
pub(crate) fn parse_bool_opt(value: Option<crate::schema::BoolType>) -> Result<Option<bool>> {
value.map(parse_bool).transpose()
}
#[cfg(feature = "write")]
pub(crate) fn format_bool(value: bool) -> crate::schema::BoolType {
if value {
crate::schema::BoolType::true_
} else {
crate::schema::BoolType::false_
}
}
#[cfg(feature = "write")]
pub(crate) fn format_bool_opt(value: Option<bool>) -> Option<crate::schema::BoolType> {
value.map(format_bool)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum VClass {
Aircraft,
Army,
Authority,
Bicycle,
Bus,
CableCar,
Coach,
Container,
Custom1,
Custom2,
Delivery,
Drone,
Emergency,
Evehicle,
Hov,
Ignoring,
Moped,
Motorcycle,
Passenger,
Pedestrian,
Private,
Rail,
RailElectric,
RailFast,
RailUrban,
Scooter,
Ship,
Subway,
Taxi,
Trailer,
Tram,
Truck,
Vip,
Wheelchair,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnknownVClass(pub String);
impl std::str::FromStr for VClass {
type Err = UnknownVClass;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
"aircraft" => Self::Aircraft,
"army" => Self::Army,
"authority" => Self::Authority,
"bicycle" => Self::Bicycle,
"bus" => Self::Bus,
"cable_car" => Self::CableCar,
"coach" => Self::Coach,
"container" => Self::Container,
"custom1" => Self::Custom1,
"custom2" => Self::Custom2,
"delivery" => Self::Delivery,
"drone" => Self::Drone,
"emergency" => Self::Emergency,
"evehicle" => Self::Evehicle,
"hov" => Self::Hov,
"ignoring" => Self::Ignoring,
"moped" => Self::Moped,
"motorcycle" => Self::Motorcycle,
"passenger" => Self::Passenger,
"pedestrian" => Self::Pedestrian,
"private" => Self::Private,
"rail" => Self::Rail,
"rail_electric" => Self::RailElectric,
"rail_fast" => Self::RailFast,
"rail_urban" => Self::RailUrban,
"scooter" => Self::Scooter,
"ship" => Self::Ship,
"subway" => Self::Subway,
"taxi" => Self::Taxi,
"trailer" => Self::Trailer,
"tram" => Self::Tram,
"truck" => Self::Truck,
"vip" => Self::Vip,
"wheelchair" => Self::Wheelchair,
other => return Err(UnknownVClass(other.to_string())),
})
}
}
impl VClass {
#[must_use]
pub fn permitted_on(self, allow: &[String], disallow: &[String]) -> bool {
let names_self = |list: &[String]| {
list.iter()
.any(|token| token == "all" || token.parse::<VClass>() == Ok(self))
};
if allow.is_empty() {
!names_self(disallow)
} else {
names_self(allow)
}
}
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Aircraft => "aircraft",
Self::Army => "army",
Self::Authority => "authority",
Self::Bicycle => "bicycle",
Self::Bus => "bus",
Self::CableCar => "cable_car",
Self::Coach => "coach",
Self::Container => "container",
Self::Custom1 => "custom1",
Self::Custom2 => "custom2",
Self::Delivery => "delivery",
Self::Drone => "drone",
Self::Emergency => "emergency",
Self::Evehicle => "evehicle",
Self::Hov => "hov",
Self::Ignoring => "ignoring",
Self::Moped => "moped",
Self::Motorcycle => "motorcycle",
Self::Passenger => "passenger",
Self::Pedestrian => "pedestrian",
Self::Private => "private",
Self::Rail => "rail",
Self::RailElectric => "rail_electric",
Self::RailFast => "rail_fast",
Self::RailUrban => "rail_urban",
Self::Scooter => "scooter",
Self::Ship => "ship",
Self::Subway => "subway",
Self::Taxi => "taxi",
Self::Trailer => "trailer",
Self::Tram => "tram",
Self::Truck => "truck",
Self::Vip => "vip",
Self::Wheelchair => "wheelchair",
}
}
}
impl std::fmt::Display for VClass {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::schema::BoolType;
#[test]
fn rejects_values_that_parse_as_numbers_but_are_not_finite() {
assert!(parse_finite("NaN", "test").is_err());
assert!(parse_finite("inf", "test").is_err());
assert!(parse_finite("-inf", "test").is_err());
assert!(parse_finite("not-a-number", "test").is_err());
}
#[test]
fn accepts_the_shapes_sumo_actually_writes() {
assert_eq!(parse_finite("12.50", "test").unwrap(), 12.5);
assert_eq!(parse_finite("-3", "test").unwrap(), -3.0);
assert_eq!(parse_finite(" 7.0 ", "test").unwrap(), 7.0);
}
#[test]
fn parses_positions_with_and_without_z() {
assert_eq!(
Point::parse("42,7", "test").unwrap(),
Point {
x: 42.0,
y: 7.0,
z: 0.0
}
);
assert_eq!(
Point::parse("42,7,1.5", "test").unwrap(),
Point {
x: 42.0,
y: 7.0,
z: 1.5
}
);
assert_eq!(
Point::parse("-1.00,-1.00", "test").unwrap(),
Point {
x: -1.0,
y: -1.0,
z: 0.0
}
);
}
#[test]
fn rejects_positions_of_the_wrong_shape_or_with_poisoned_coordinates() {
assert!(Point::parse("42", "test").is_err());
assert!(Point::parse("42,7,1,9", "test").is_err());
assert!(Point::parse("NaN,7", "test").is_err());
assert!(Point::parse("42,inf", "test").is_err());
}
#[test]
fn splits_on_any_run_of_whitespace() {
#[derive(Debug, PartialEq)]
struct Id(String);
impl From<String> for Id {
fn from(s: String) -> Self {
Id(s)
}
}
let ids: Vec<Id> = split_ids("a b\tc");
assert_eq!(ids, vec![Id("a".into()), Id("b".into()), Id("c".into())]);
assert!(split_ids::<Id>("").is_empty());
assert!(split_ids_opt::<Id>(None).is_empty());
}
#[test]
fn distinguishes_case_sensitive_bool_spellings() {
assert!(parse_bool(BoolType::True).unwrap());
assert!(parse_bool(BoolType::true_).unwrap());
assert!(!parse_bool(BoolType::False).unwrap());
assert!(!parse_bool(BoolType::false_).unwrap());
}
#[test]
fn rejects_bool_without_binary_meaning() {
assert!(parse_bool(BoolType::x).is_err());
assert!(parse_bool(BoolType::Dash).is_err());
assert_eq!(parse_bool_opt(None).unwrap(), None);
}
#[cfg(feature = "write")]
mod write_tests {
use super::*;
#[test]
fn formats_a_point_omitting_z_only_when_it_is_zero() {
assert_eq!(
Point {
x: 1.5,
y: -2.0,
z: 0.0
}
.format(),
"1.5,-2"
);
assert_eq!(
Point {
x: 1.5,
y: -2.0,
z: 3.0
}
.format(),
"1.5,-2,3"
);
}
#[test]
fn every_parsed_position_formats_back_to_an_equal_point() {
for raw in ["0,0", "42,7,1.5", "-1.00,-1.00"] {
let point = Point::parse(raw, "test").unwrap();
assert_eq!(Point::parse(&point.format(), "test").unwrap(), point);
}
}
#[test]
fn joins_and_splits_are_inverse_on_a_nonempty_list() {
let ids = ["a", "b", "c"];
assert_eq!(join_ids(&ids), "a b c");
assert_eq!(split_ids::<String>(&join_ids(&ids)), vec!["a", "b", "c"]);
}
#[test]
fn join_ids_opt_omits_the_attribute_for_an_empty_list() {
assert_eq!(join_ids_opt::<String>(&[]), None);
assert_eq!(join_ids_opt(&["a".to_string()]), Some("a".to_string()));
}
#[test]
fn formats_the_canonical_bool_spelling() {
assert!(parse_bool(format_bool(true)).unwrap());
assert!(!parse_bool(format_bool(false)).unwrap());
assert!(format_bool_opt(None).is_none());
}
}
}