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)
}
#[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());
}
}
}