use crate::osm::primitives::opening_hours::{OpeningHours, OpeningHoursParser};
use alloc::fmt;
use core::fmt::{Display, Formatter};
use core::str::FromStr;
use serde::Serialize;
use strum::{Display, EnumIter, EnumString};
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct Condition {
pub condition_type: ConditionType,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub enum ConditionType {
TimeDate(TimeDateCondition),
Season(SeasonCondition),
RoadCondition(RoadCondition),
VehicleProperty(VehiclePropertyCondition),
VehicleUsage(VehicleUsageCondition),
UserGroup(UserGroupCondition),
Purpose(PurposeCondition),
StayDuration(StayDurationCondition),
Combined(CombinedCondition),
Raw(String),
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct TimeDateCondition {
pub opening_hours: OpeningHours,
pub comment: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Display, EnumString, EnumIter, Serialize)]
#[strum(serialize_all = "lowercase")]
pub enum SeasonCondition {
Winter,
Summer,
Spring,
Autumn,
}
#[derive(Debug, Clone, PartialEq, Display, EnumString, EnumIter, Serialize)]
#[strum(serialize_all = "lowercase")]
pub enum RoadCondition {
Wet,
Dry,
Snow,
Ice,
Rain,
Fog,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct VehiclePropertyCondition {
pub property: VehicleProperty,
pub operator: ComparisonOperator,
pub value: f64,
pub unit: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Display, EnumString, EnumIter, Serialize)]
#[strum(serialize_all = "lowercase")]
pub enum VehicleProperty {
Weight,
Axleload,
Length,
Width,
Height,
Wheels,
Draught,
}
#[derive(Debug, Clone, PartialEq, Display, Serialize)]
pub enum ComparisonOperator {
#[strum(serialize = "<")]
LessThan,
#[strum(serialize = ">")]
GreaterThan,
#[strum(serialize = "=")]
Equal,
#[strum(serialize = "<=")]
LessThanOrEqual,
#[strum(serialize = ">=")]
GreaterThanOrEqual,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub enum VehicleUsageCondition {
Occupants {
operator: ComparisonOperator,
count: u32,
},
Hazmat,
Load(String),
}
#[derive(Debug, Clone, PartialEq, Display, EnumString, EnumIter, Serialize)]
#[strum(serialize_all = "lowercase")]
pub enum UserGroupCondition {
Doctor,
Disabled,
Emergency,
Female,
Residents,
Permit,
Staff,
Customers,
}
#[derive(Debug, Clone, PartialEq, Display, EnumString, EnumIter, Serialize)]
#[strum(serialize_all = "lowercase")]
pub enum PurposeCondition {
Destination,
Delivery,
Customers,
Forestry,
Agricultural,
Private,
Permit,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct StayDurationCondition {
pub operator: ComparisonOperator,
pub duration: Duration,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct Duration {
pub value: u32,
pub unit: DurationUnit,
}
#[derive(Debug, Clone, PartialEq, Display, EnumString, EnumIter, Serialize)]
#[strum(serialize_all = "lowercase")]
pub enum DurationUnit {
Minutes,
Hours,
Days,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct CombinedCondition {
pub left: Box<ConditionType>,
pub operator: LogicalOperator,
pub right: Box<ConditionType>,
}
#[derive(Debug, Clone, PartialEq, Display, EnumString, Serialize)]
#[strum(serialize_all = "UPPERCASE")]
pub enum LogicalOperator {
And,
Or,
}
impl FromStr for ComparisonOperator {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"<" => Ok(ComparisonOperator::LessThan),
">" => Ok(ComparisonOperator::GreaterThan),
"=" => Ok(ComparisonOperator::Equal),
"<=" => Ok(ComparisonOperator::LessThanOrEqual),
">=" => Ok(ComparisonOperator::GreaterThanOrEqual),
_ => Err(format!("Unknown comparison operator: {s}")),
}
}
}
impl Display for Condition {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match &self.condition_type {
ConditionType::TimeDate(td) => {
if let Some(comment) = &td.comment {
write!(f, "{} \"{}\"", td.opening_hours, comment)
} else {
write!(f, "{}", td.opening_hours)
}
}
ConditionType::Season(season) => write!(f, "{season}"),
ConditionType::RoadCondition(road) => write!(f, "{road}"),
ConditionType::VehicleProperty(vp) => {
let unit_str = vp.unit.as_deref().unwrap_or("");
write!(f, "{}{}{}{}", vp.property, vp.operator, vp.value, unit_str)
}
ConditionType::VehicleUsage(vu) => match vu {
VehicleUsageCondition::Occupants { operator, count } => {
write!(f, "occupants{operator}{count}")
}
VehicleUsageCondition::Hazmat => write!(f, "hazmat"),
VehicleUsageCondition::Load(load) => write!(f, "{load}"),
},
ConditionType::UserGroup(ug) => write!(f, "{ug}"),
ConditionType::Purpose(purpose) => write!(f, "{purpose}"),
ConditionType::StayDuration(sd) => {
write!(
f,
"stay {} {} {}",
sd.operator, sd.duration.value, sd.duration.unit
)
}
ConditionType::Combined(combined) => {
write!(
f,
"{} {} {}",
Condition {
condition_type: *combined.left.clone()
},
combined.operator,
Condition {
condition_type: *combined.right.clone()
}
)
}
ConditionType::Raw(raw) => write!(f, "{raw}"),
}
}
}
impl Condition {
pub fn parse(condition_str: &str) -> Result<Self, ParseError> {
let trimmed = condition_str.trim();
let cleaned = if trimmed.starts_with('(') && trimmed.ends_with(')') {
&trimmed[1..trimmed.len() - 1]
} else {
trimmed
};
if let Ok(combined) = Self::parse_combined_condition(cleaned) {
return Ok(Condition {
condition_type: combined,
});
}
if let Ok(time_date) = Self::parse_time_date(cleaned) {
return Ok(Condition {
condition_type: ConditionType::TimeDate(time_date),
});
}
if let Ok(season) = Self::parse_season(cleaned) {
return Ok(Condition {
condition_type: ConditionType::Season(season),
});
}
if let Ok(road_condition) = Self::parse_road_condition(cleaned) {
return Ok(Condition {
condition_type: ConditionType::RoadCondition(road_condition),
});
}
if let Ok(vehicle_prop) = Self::parse_vehicle_property(cleaned) {
return Ok(Condition {
condition_type: ConditionType::VehicleProperty(vehicle_prop),
});
}
if let Ok(vehicle_usage) = Self::parse_vehicle_usage(cleaned) {
return Ok(Condition {
condition_type: ConditionType::VehicleUsage(vehicle_usage),
});
}
if let Ok(user_group) = Self::parse_user_group(cleaned) {
return Ok(Condition {
condition_type: ConditionType::UserGroup(user_group),
});
}
if let Ok(purpose) = Self::parse_purpose(cleaned) {
return Ok(Condition {
condition_type: ConditionType::Purpose(purpose),
});
}
if let Ok(stay_duration) = Self::parse_stay_duration(cleaned) {
return Ok(Condition {
condition_type: ConditionType::StayDuration(stay_duration),
});
}
Ok(Condition {
condition_type: ConditionType::Raw(cleaned.to_string()),
})
}
fn parse_combined_condition(s: &str) -> Result<ConditionType, ParseError> {
let s_upper = s.to_uppercase();
if let Some(and_pos) = s_upper.find(" AND ") {
let left_str = &s[..and_pos].trim();
let right_str = &s[and_pos + 5..].trim();
let left_condition = Self::parse(left_str)?.condition_type;
let right_condition = Self::parse(right_str)?.condition_type;
return Ok(ConditionType::Combined(CombinedCondition {
left: Box::new(left_condition),
operator: LogicalOperator::And,
right: Box::new(right_condition),
}));
}
if let Some(or_pos) = s_upper.find(" OR ") {
let left_str = &s[..or_pos].trim();
let right_str = &s[or_pos + 4..].trim();
let left_condition = Self::parse(left_str)?.condition_type;
let right_condition = Self::parse(right_str)?.condition_type;
return Ok(ConditionType::Combined(CombinedCondition {
left: Box::new(left_condition),
operator: LogicalOperator::Or,
right: Box::new(right_condition),
}));
}
Err(ParseError::NotCombinedCondition)
}
fn parse_time_date(s: &str) -> Result<TimeDateCondition, ParseError> {
let (comment, hours) = if let Some(quote_start) = s.find('"') {
let opening_hours = s[..quote_start].trim().to_string();
let comment_end = s.rfind('"').unwrap_or(s.len());
let comment = s[quote_start + 1..comment_end].to_string();
(Some(comment), opening_hours)
} else {
(None, s.to_string())
};
Ok(TimeDateCondition {
comment,
opening_hours: OpeningHoursParser::parse(&hours)
.map_err(|_| ParseError::NotTimeDate)?,
})
}
fn parse_season(s: &str) -> Result<SeasonCondition, ParseError> {
SeasonCondition::from_str(s).map_err(|_| ParseError::NotSeason)
}
fn parse_road_condition(s: &str) -> Result<RoadCondition, ParseError> {
RoadCondition::from_str(s).map_err(|_| ParseError::NotRoadCondition)
}
fn parse_vehicle_property(s: &str) -> Result<VehiclePropertyCondition, ParseError> {
let operators = ["<=", ">=", "<", ">", "="];
for op_str in &operators {
if let Some(op_pos) = s.find(op_str) {
let property_str = s[..op_pos].trim();
let value_str = s[op_pos + op_str.len()..].trim();
let property = VehicleProperty::from_str(property_str)
.map_err(|_| ParseError::UnknownVehicleProperty)?;
let operator = ComparisonOperator::from_str(op_str)
.map_err(|_| ParseError::UnknownOperator)?;
let (value, unit) = Self::parse_value_with_unit(value_str)?;
return Ok(VehiclePropertyCondition {
property,
operator,
value,
unit,
});
}
}
Err(ParseError::NotVehicleProperty)
}
fn parse_vehicle_usage(s: &str) -> Result<VehicleUsageCondition, ParseError> {
if s == "hazmat" {
return Ok(VehicleUsageCondition::Hazmat);
}
if let Some(rest) = s.strip_prefix("occupants") {
let operators = ["<=", ">=", "<", ">", "="];
for op_str in &operators {
if let Some(op_pos) = rest.find(op_str) {
let operator = ComparisonOperator::from_str(op_str)
.map_err(|_| ParseError::UnknownOperator)?;
let count_str = rest[op_pos + op_str.len()..].trim();
let count = count_str
.parse::<u32>()
.map_err(|_| ParseError::InvalidNumber)?;
return Ok(VehicleUsageCondition::Occupants { operator, count });
}
}
}
Ok(VehicleUsageCondition::Load(s.to_string()))
}
fn parse_user_group(s: &str) -> Result<UserGroupCondition, ParseError> {
UserGroupCondition::from_str(s).map_err(|_| ParseError::NotUserGroup)
}
fn parse_purpose(s: &str) -> Result<PurposeCondition, ParseError> {
PurposeCondition::from_str(s).map_err(|_| ParseError::NotPurpose)
}
fn parse_stay_duration(s: &str) -> Result<StayDurationCondition, ParseError> {
if !s.starts_with("stay") {
return Err(ParseError::NotStayDuration);
}
let rest = s[4..].trim(); let operators = ["<=", ">=", "<", ">", "="];
for op_str in &operators {
if let Some(op_pos) = rest.find(op_str) {
let operator = ComparisonOperator::from_str(op_str)
.map_err(|_| ParseError::UnknownOperator)?;
let duration_str = rest[op_pos + op_str.len()..].trim();
let duration = Self::parse_duration(duration_str)?;
return Ok(StayDurationCondition { operator, duration });
}
}
Err(ParseError::NotStayDuration)
}
fn parse_duration(s: &str) -> Result<Duration, ParseError> {
let parts: Vec<&str> = s.split_whitespace().collect();
if parts.len() != 2 {
return Err(ParseError::InvalidDuration);
}
let value = parts[0]
.parse::<u32>()
.map_err(|_| ParseError::InvalidNumber)?;
let unit_str = parts[1].to_lowercase();
let unit = match unit_str.as_str() {
"minute" | "minutes" => DurationUnit::Minutes,
"hour" | "hours" => DurationUnit::Hours,
"day" | "days" => DurationUnit::Days,
_ => return Err(ParseError::InvalidDurationUnit),
};
Ok(Duration { value, unit })
}
fn parse_value_with_unit(s: &str) -> Result<(f64, Option<String>), ParseError> {
if let Ok(value) = s.parse::<f64>() {
return Ok((value, None));
}
let mut number_end = 0;
for (i, c) in s.chars().enumerate() {
if c.is_numeric() || c == '.' {
number_end = i + 1;
} else {
break;
}
}
if number_end > 0 {
let number_str = &s[..number_end];
let unit_str = &s[number_end..].trim();
let value = number_str
.parse::<f64>()
.map_err(|_| ParseError::InvalidNumber)?;
let unit = if unit_str.is_empty() {
None
} else {
Some(unit_str.to_string())
};
Ok((value, unit))
} else {
Err(ParseError::InvalidNumber)
}
}
}
#[derive(Debug, PartialEq)]
pub enum ParseError {
NotTimeDate,
NotSeason,
NotRoadCondition,
NotVehicleProperty,
NotVehicleUsage,
NotUserGroup,
NotPurpose,
NotStayDuration,
NotCombinedCondition,
UnknownVehicleProperty,
UnknownOperator,
InvalidNumber,
InvalidDuration,
InvalidDurationUnit,
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ParseError::NotTimeDate => write!(f, "Not a time/date condition"),
ParseError::NotSeason => write!(f, "Not a season condition"),
ParseError::NotRoadCondition => write!(f, "Not a road condition"),
ParseError::NotVehicleProperty => write!(f, "Not a vehicle property condition"),
ParseError::NotVehicleUsage => write!(f, "Not a vehicle usage condition"),
ParseError::NotUserGroup => write!(f, "Not a user group condition"),
ParseError::NotPurpose => write!(f, "Not a purpose condition"),
ParseError::NotStayDuration => write!(f, "Not a stay duration condition"),
ParseError::NotCombinedCondition => write!(f, "Not a combined condition"),
ParseError::UnknownVehicleProperty => write!(f, "Unknown vehicle property"),
ParseError::UnknownOperator => write!(f, "Unknown comparison operator"),
ParseError::InvalidNumber => write!(f, "Invalid number format"),
ParseError::InvalidDuration => write!(f, "Invalid duration format"),
ParseError::InvalidDurationUnit => write!(f, "Invalid duration unit"),
}
}
}
impl core::error::Error for ParseError {}
#[cfg(test)]
mod tests {
use super::*;
use crate::osm::primitives::opening_hours::{
OpeningRule, Time, TimeRange, Weekday, WeekdayRange,
};
#[test]
fn test_parse_time_date() {
let condition = Condition::parse("Tu-Fr 00:00-24:00").unwrap();
if let ConditionType::TimeDate(td) = condition.condition_type {
assert_eq!(
td.opening_hours.rules[0],
OpeningRule {
weekdays: Some(WeekdayRange::Range(Weekday::Tuesday, Weekday::Friday)),
times: vec![TimeRange {
start: Time { hour: 0, minute: 0 },
end: Time {
hour: 24,
minute: 0
}
}],
closed: false
}
);
assert_eq!(td.comment, None);
} else {
panic!("Expected TimeDate condition");
}
}
#[test]
fn test_parse_season() {
let condition = Condition::parse("winter").unwrap();
if let ConditionType::Season(season) = condition.condition_type {
assert_eq!(season, SeasonCondition::Winter);
} else {
panic!("Expected Season condition");
}
}
#[test]
fn test_parse_vehicle_property() {
let condition = Condition::parse("weight < 7.5").unwrap();
if let ConditionType::VehicleProperty(vp) = condition.condition_type {
assert_eq!(vp.property, VehicleProperty::Weight);
assert_eq!(vp.operator, ComparisonOperator::LessThan);
assert_eq!(vp.value, 7.5);
} else {
panic!("Expected VehicleProperty condition");
}
}
#[test]
fn test_parse_road_condition() {
let condition = Condition::parse("snow").unwrap();
if let ConditionType::RoadCondition(road) = condition.condition_type {
assert_eq!(road, RoadCondition::Snow);
} else {
panic!("Expected RoadCondition");
}
}
#[test]
fn test_to_string_roundtrip() {
let original = "weight < 7.5";
let condition = Condition::parse(original).unwrap();
let regenerated = condition.to_string();
assert_eq!(regenerated, "weight<7.5"); }
}