use std::{ops::Add, time::Instant};
use lazy_regex::regex;
use crate::{DurationError, DurationHumanValidator};
type StdDuration = std::time::Duration;
#[derive(Clone, PartialEq, Eq, PartialOrd, Copy, Debug)]
pub struct DurationHuman {
inner: StdDuration,
}
impl DurationHuman {
pub const MICRO_SEC: u64 = 1_000;
pub const MILLI_SEC: u64 = 1_000 * Self::MICRO_SEC;
pub const SEC: u64 = 1_000 * Self::MILLI_SEC;
pub const MINUTE: u64 = 60 * Self::SEC;
pub const HOUR: u64 = 60 * Self::MINUTE;
pub const DAY: u64 = 24 * Self::HOUR;
pub const WEEK: u64 = 7 * Self::DAY;
pub const YEAR: u64 = 31_557_600 * Self::SEC;
pub const MONTH: u64 = Self::YEAR / 12;
pub const CENTURY: u64 = 100 * Self::YEAR;
pub const ONE_SECOND: Self = Self::new(Self::SEC);
pub const ONE_MILLISECOND: Self = Self::new(Self::MILLI_SEC);
#[must_use]
pub const fn new(nanos: u64) -> Self {
Self {
inner: std::time::Duration::from_nanos(nanos),
}
}
pub fn parse(human_readable: &str) -> Result<Self, DurationError> {
Self::try_from(human_readable)
}
#[must_use]
pub fn is_in(&self, range: &DurationHumanValidator) -> bool {
range.contains(self)
}
}
impl Default for DurationHuman {
fn default() -> Self {
Self {
inner: StdDuration::from_millis(Self::MINUTE),
}
}
}
impl Add<Instant> for DurationHuman {
type Output = Instant;
fn add(self, rhs: Instant) -> Self::Output {
rhs + self.inner
}
}
impl From<StdDuration> for DurationHuman {
fn from(inner: StdDuration) -> Self {
Self { inner }
}
}
impl From<&DurationHuman> for StdDuration {
fn from(duration: &DurationHuman) -> Self {
duration.inner
}
}
impl From<u64> for DurationHuman {
fn from(nanos: u64) -> Self {
Self::new(nanos)
}
}
impl TryFrom<&str> for DurationHuman {
type Error = DurationError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
let matcher = regex!(
r"^(?:(\d+)\s*(?:(century|centuries)|(year|month|week|day)(?:s?)|(h|min|s|ms|μs|ns))\s*)*$"
);
let splitter = regex!(
r"(\d+)\s*(?:(century|centuries)|(year|month|week|day)(?:s?)|(h|min|s|ms|μs|ns))"
);
if !matcher.is_match(value) {
return Err(DurationError::InvalidSyntax);
}
splitter
.captures_iter(value)
.map(|group| {
let value = group[1].parse::<u64>()?;
if value == 0 {
Ok(DurationPart::default())
} else {
let part: &str = group[0].as_ref();
#[allow(clippy::unwrap_used)] let unit = group
.get(2)
.or_else(|| group.get(3).or_else(|| group.get(4)))
.unwrap();
match unit.as_str() {
"century" | "centuries" => (part, value, Self::CENTURY).try_into(),
"year" => (part, value, Self::YEAR).try_into(),
"month" => (part, value, Self::MONTH).try_into(),
"week" => (part, value, Self::WEEK).try_into(),
"day" => (part, value, Self::DAY).try_into(),
"h" => (part, value, Self::HOUR).try_into(),
"min" => (part, value, Self::MINUTE).try_into(),
"s" => (part, value, Self::SEC).try_into(),
"ms" => (part, value, Self::MILLI_SEC).try_into(),
"μs" => (part, value, Self::MICRO_SEC).try_into(),
"ns" => (part, value, 1).try_into(),
sym => Err(DurationError::UnitMatchAndRegexNotInSync {
sym: sym.to_string(),
}),
}
}
})
.fold(Ok(0), |nanos_sum, part| {
nanos_sum.and_then(|nanos_sum| {
part.and_then(|duration_part| duration_part.add(nanos_sum))
})
})
.map(Self::from)
}
}
impl From<DurationHuman> for clap::builder::OsStr {
fn from(duration: DurationHuman) -> Self {
duration.to_string().into()
}
}
impl From<&DurationHuman> for u64 {
#[allow(clippy::cast_possible_truncation)] fn from(duration: &DurationHuman) -> Self {
duration.inner.as_nanos() as Self
}
}
#[derive(Default)]
struct DurationPart {
part: String,
nanos: u64,
}
impl TryFrom<(&str, u64, u64)> for DurationPart {
type Error = DurationError;
fn try_from((part, value, factor): (&str, u64, u64)) -> Result<Self, Self::Error> {
if factor < 1 {
return Ok(Self::default());
}
if value > u64::MAX / factor {
return Err(DurationError::IntegerOverflowAt {
duration: part.to_string(),
});
}
Ok(Self {
part: part.to_string(),
nanos: value * factor,
})
}
}
impl DurationPart {
fn add(&self, rhs: u64) -> Result<u64, DurationError> {
if self.nanos > u64::MAX - rhs {
return Err(DurationError::IntegerOverflowAt {
duration: self.part.to_string(),
});
}
Ok(self.nanos + rhs)
}
}