use lazy_static::lazy_static;
use regex::Regex;
use std::fmt::Write;
use std::time::Duration;
lazy_static! {
static ref DURATION_RE: Regex = Regex::new(
r"(?x)
^
((?P<y>[0-9]+)y)?
((?P<w>[0-9]+)w)?
((?P<d>[0-9]+)d)?
((?P<h>[0-9]+)h)?
((?P<m>[0-9]+)m)?
((?P<s>[0-9]+)s)?
((?P<ms>[0-9]+)ms)?
$",
)
.unwrap();
}
pub const MILLI_DURATION: Duration = Duration::from_millis(1);
pub const SECOND_DURATION: Duration = Duration::from_secs(1);
pub const MINUTE_DURATION: Duration = Duration::from_secs(60);
pub const HOUR_DURATION: Duration = Duration::from_secs(60 * 60);
pub const DAY_DURATION: Duration = Duration::from_secs(60 * 60 * 24);
pub const WEEK_DURATION: Duration = Duration::from_secs(60 * 60 * 24 * 7);
pub const YEAR_DURATION: Duration = Duration::from_secs(60 * 60 * 24 * 365);
const ALL_CAPS: [(&str, Duration); 7] = [
("y", YEAR_DURATION),
("w", WEEK_DURATION),
("d", DAY_DURATION),
("h", HOUR_DURATION),
("m", MINUTE_DURATION),
("s", SECOND_DURATION),
("ms", MILLI_DURATION),
];
pub fn parse_duration(ds: &str) -> Result<Duration, String> {
if ds.is_empty() {
return Err("empty duration string".into());
}
if ds == "0" {
return Err("duration must be greater than 0".into());
}
if let Ok(float_duration) = ds.parse::<f64>() {
return Ok(Duration::from_secs_f64(float_duration));
}
if !DURATION_RE.is_match(ds) {
return Err(format!("not a valid duration string: {ds}"));
}
let caps = DURATION_RE.captures(ds).unwrap();
let dur = ALL_CAPS
.into_iter()
.map(|(title, duration)| {
caps.name(title)
.and_then(|cap| cap.as_str().parse::<u32>().ok())
.and_then(|v| duration.checked_mul(v))
})
.try_fold(Duration::ZERO, |acc, x| {
acc.checked_add(x.unwrap_or(Duration::ZERO))
.ok_or_else(|| "duration overflowed".into())
});
if matches!(dur, Ok(d) if d == Duration::ZERO) {
Err("duration must be greater than 0".into())
} else {
dur
}
}
pub fn display_duration(duration: &Duration) -> String {
if duration.is_zero() {
return "0s".into();
}
let mut ms = duration.as_millis();
let mut ss = String::new();
let mut f = |unit: &str, mult: u128, exact: bool| {
if exact && !ms.is_multiple_of(mult) {
return;
}
let v = ms / mult;
if v > 0 {
write!(ss, "{v}{unit}").unwrap();
ms -= v * mult
}
};
f("y", 1000 * 60 * 60 * 24 * 365, true);
f("w", 1000 * 60 * 60 * 24 * 7, true);
f("d", 1000 * 60 * 60 * 24, false);
f("h", 1000 * 60 * 60, false);
f("m", 1000 * 60, false);
f("s", 1000, false);
f("ms", 1, false);
ss
}
#[cfg(feature = "ser")]
pub(crate) fn serialize_duration<S>(dur: &Duration, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let duration_millis = dur.as_millis();
serializer.serialize_u128(duration_millis)
}
#[cfg(feature = "ser")]
pub(crate) fn serialize_duration_opt<S>(
dur: &Option<Duration>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
if let Some(dur) = dur {
serialize_duration(dur, serializer)
} else {
serializer.serialize_none()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_re() {
let res = vec![
"1y", "2w", "3d", "4h", "5m", "6s", "7ms", "1y2w3d", "4h30m", "3600ms",
];
for re in res {
assert!(DURATION_RE.is_match(re), "{re} failed.")
}
let res = vec!["1", "1y1m1d", "-1w", "1.5d", "d"];
for re in res {
assert!(!DURATION_RE.is_match(re), "{re} failed.")
}
}
#[test]
fn test_valid_duration() {
let ds = vec![
("324ms", Duration::from_millis(324)),
("3s", Duration::from_secs(3)),
("5m", MINUTE_DURATION * 5),
("1h", HOUR_DURATION),
("4d", DAY_DURATION * 4),
("4d1h", DAY_DURATION * 4 + HOUR_DURATION),
("14d", DAY_DURATION * 14),
("3w", WEEK_DURATION * 3),
("3w2d1h", WEEK_DURATION * 3 + HOUR_DURATION * 49),
("10y", YEAR_DURATION * 10),
("0.5", Duration::from_secs_f64(0.5)),
(".5", Duration::from_secs_f64(0.5)),
("1", Duration::from_secs_f64(1.)),
("11.2345", Duration::from_secs_f64(11.2345)),
];
for (s, expect) in ds {
let d = parse_duration(s);
assert!(d.is_ok());
assert_eq!(expect, d.unwrap(), "{s} and {expect:?} not matched");
}
}
#[test]
fn test_diff_with_promql() {
let ds = vec![
("294y", YEAR_DURATION * 294),
("200y10400w", YEAR_DURATION * 200 + WEEK_DURATION * 10400),
("107675d", DAY_DURATION * 107675),
("2584200h", HOUR_DURATION * 2584200),
];
for (s, expect) in ds {
let d = parse_duration(s);
assert!(d.is_ok());
assert_eq!(expect, d.unwrap(), "{s} and {expect:?} not matched");
}
}
#[test]
fn test_invalid_duration() {
let ds = vec!["1y1m1d", "-1w", "1.5d", "d", "", "0", "0w", "0s"];
for d in ds {
assert!(parse_duration(d).is_err(), "{d} is invalid duration!");
}
}
#[test]
fn test_display_duration() {
let ds = vec![
(Duration::ZERO, "0s"),
(Duration::from_millis(324), "324ms"),
(Duration::from_secs(3), "3s"),
(MINUTE_DURATION * 5, "5m"),
(MINUTE_DURATION * 5 + MILLI_DURATION * 500, "5m500ms"),
(HOUR_DURATION, "1h"),
(DAY_DURATION * 4, "4d"),
(DAY_DURATION * 4 + HOUR_DURATION, "4d1h"),
(
DAY_DURATION * 4 + HOUR_DURATION * 2 + MINUTE_DURATION * 10,
"4d2h10m",
),
(DAY_DURATION * 14, "2w"),
(WEEK_DURATION * 3, "3w"),
(WEEK_DURATION * 3 + HOUR_DURATION * 49, "23d1h"),
(YEAR_DURATION * 10, "10y"),
];
for (d, expect) in ds {
let s = display_duration(&d);
assert_eq!(expect, s, "{s} and {expect:?} not matched");
}
}
}