use std::str::FromStr;
use std::time::Duration;
use super::{BusinessCalendar, SlaError};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub struct BusinessDuration {
days: u32,
secs: u64,
}
impl BusinessDuration {
pub const ZERO: Self = Self { days: 0, secs: 0 };
#[must_use]
pub const fn days(n: u32) -> Self {
Self { days: n, secs: 0 }
}
#[must_use]
pub const fn hours(n: u32) -> Self {
Self {
days: 0,
secs: (n as u64).saturating_mul(3_600),
}
}
#[must_use]
pub const fn minutes(n: u32) -> Self {
Self {
days: 0,
secs: (n as u64).saturating_mul(60),
}
}
#[must_use]
pub const fn from_parts(days: u32, secs: u64) -> Self {
Self { days, secs }
}
#[must_use]
pub const fn business_days(&self) -> u32 {
self.days
}
#[must_use]
pub const fn working_secs(&self) -> u64 {
self.secs
}
#[must_use]
pub fn resolve(&self, calendar: &BusinessCalendar) -> Duration {
calendar
.day_length()
.saturating_mul(self.days)
.saturating_add(Duration::from_secs(self.secs))
}
}
impl FromStr for BusinessDuration {
type Err = SlaError;
fn from_str(text: &str) -> Result<Self, Self::Err> {
parse(text).ok_or_else(|| SlaError::InvalidDuration(text.to_owned()))
}
}
impl serde::Serialize for BusinessDuration {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.collect_str(self)
}
}
impl<'de> serde::Deserialize<'de> for BusinessDuration {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let text = String::deserialize(deserializer)?;
text.parse().map_err(serde::de::Error::custom)
}
}
impl std::fmt::Display for BusinessDuration {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let parts = [
(u64::from(self.days), "day"),
(self.secs / 3_600, "hour"),
(self.secs % 3_600 / 60, "minute"),
(self.secs % 60, "second"),
];
let mut first = true;
for (count, unit) in parts {
if count == 0 {
continue;
}
if !first {
f.write_str(", ")?;
}
first = false;
let plural = if count == 1 { "" } else { "s" };
write!(f, "{count} business {unit}{plural}")?;
}
if first {
f.write_str("0 business minutes")?;
}
Ok(())
}
}
fn parse(text: &str) -> Option<BusinessDuration> {
let lower = text.to_ascii_lowercase().replace(',', " , ");
let mut words = lower.split_whitespace().peekable();
let mut total = BusinessDuration::ZERO;
loop {
let count = words.next()?;
if !count.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
let count: u64 = count.parse().ok()?;
words.next_if_eq(&"business");
match words.next()? {
"day" | "days" => {
total.days = total.days.checked_add(u32::try_from(count).ok()?)?;
}
unit => {
let scale = match unit {
"hour" | "hours" => 3_600,
"minute" | "minutes" => 60,
"second" | "seconds" => 1,
_ => return None,
};
total.secs = total.secs.checked_add(count.checked_mul(scale)?)?;
}
}
if words.peek().is_none() {
return Some(total);
}
let comma = words.next_if_eq(&",").is_some();
let and = words.next_if_eq(&"and").is_some();
if !comma && !and {
return None;
}
}
}