use std::ops::Deref;
use chrono::Duration;
use once_cell::sync::Lazy;
use regex::bytes::Regex;
use thiserror::Error;
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct PositiveDuration(Duration);
#[derive(Error, Debug)]
pub enum DurationError {
#[error("Negative values are not allowed for durations")]
NegativeDuration,
#[error("Duration would exceed maximum allowed value")]
ExceedsMaximumDuration,
#[error("Input string couldn't be parsed into a PositiveDuration")]
InvalidInput,
}
impl PositiveDuration {
#[allow(clippy::expect_used)]
#[allow(clippy::unwrap_in_result)]
pub fn parse_from_str(s: &str) -> Result<Self, DurationError> {
let bytes = s.as_bytes();
static RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"^[0-9]{1,12} h$")
.expect("It wasn't possible to compile a hardcoded regex. This is a bug.")
});
if RE.is_match(bytes) {
let hours = s.split(' ').next().expect("Expecting to retrieve the hours from the string after matching the regex. This is a bug.").parse::<i64>().expect("Expecting to convert the hours to an i64. This is a bug.");
if hours > MAX_DURATION {
Err(DurationError::ExceedsMaximumDuration)
} else {
Ok(PositiveDuration(Duration::hours(hours)))
}
} else {
Err(DurationError::InvalidInput)
}
}
}
pub const MAX_DURATION: i64 = 999_999_999_999;
impl TryFrom<Duration> for PositiveDuration {
type Error = DurationError;
fn try_from(value: Duration) -> Result<Self, Self::Error> {
if value < Duration::milliseconds(0) {
Err(DurationError::NegativeDuration)
} else if value > Duration::milliseconds(MAX_DURATION) {
Err(DurationError::ExceedsMaximumDuration)
} else {
Ok(PositiveDuration(value))
}
}
}
impl Deref for PositiveDuration {
type Target = Duration;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[cfg(test)]
pub mod test_utils {
use proptest::prelude::Strategy;
pub fn duration_string() -> impl Strategy<Value = String> {
r"[0-9]{1,12} h".prop_map(|s: String| s.to_owned())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::duration::test_utils::duration_string;
use proptest::prelude::*;
proptest! {
#[test]
fn parse_from_str_works(s in duration_string()) {
let hours = s.split(' ').next().unwrap().parse::<i64>().unwrap();
if !(0..=MAX_DURATION).contains(&hours) {
assert!(PositiveDuration::parse_from_str(&s).is_err());
} else {
let duration = PositiveDuration::parse_from_str(&s).unwrap();
assert_eq!(duration.num_hours(), hours);
}
}
#[test]
fn parse_from_str_fails_with_invalid_input(s in "\\PC*") {
let bytes = s.as_bytes();
static RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"^[0-9]{1,12} h$")
.expect("It wasn't possible to compile a hardcoded regex. This is a bug.")
});
if !RE.is_match(bytes) {
assert!(PositiveDuration::parse_from_str(&s).is_err())
}
}
}
}