use crate::error::{Error, Result};
pub fn parse_duration(s: &str) -> Result<i64> {
let s = s.trim();
if s.is_empty() {
return Err(Error::config("empty duration string"));
}
if let Ok(n) = s.parse::<i64>() {
return Ok(n);
}
let (digits, suffix) = s.split_at(s.len() - 1);
let value: i64 = digits
.trim()
.parse()
.map_err(|_| Error::config(format!("invalid duration: {s}")))?;
let multiplier: i64 = match suffix {
"s" => 1,
"m" => 60,
"h" => 3600,
"d" => 86400,
"w" => 604_800,
_ => return Err(Error::config(format!("unknown duration suffix: {suffix}"))),
};
value
.checked_mul(multiplier)
.ok_or_else(|| Error::config(format!("duration overflow: {s}")))
}
pub fn deserialize_duration<'de, D>(deserializer: D) -> std::result::Result<i64, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de;
struct DurationVisitor;
impl de::Visitor<'_> for DurationVisitor {
type Value = i64;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str("an integer (seconds) or a duration string like \"10m\", \"1h\"")
}
fn visit_i64<E: de::Error>(self, v: i64) -> std::result::Result<i64, E> {
Ok(v)
}
fn visit_u64<E: de::Error>(self, v: u64) -> std::result::Result<i64, E> {
i64::try_from(v).map_err(|_| E::custom("duration too large"))
}
fn visit_str<E: de::Error>(self, v: &str) -> std::result::Result<i64, E> {
parse_duration(v).map_err(|e| E::custom(e.to_string()))
}
}
deserializer.deserialize_any(DurationVisitor)
}
#[cfg(test)]
mod tests {
use crate::duration::parse_duration;
#[test]
fn plain_integers() {
assert_eq!(parse_duration("60").unwrap(), 60);
assert_eq!(parse_duration("3600").unwrap(), 3600);
assert_eq!(parse_duration("-1").unwrap(), -1);
assert_eq!(parse_duration("0").unwrap(), 0);
}
#[test]
fn seconds_suffix() {
assert_eq!(parse_duration("30s").unwrap(), 30);
assert_eq!(parse_duration("1s").unwrap(), 1);
}
#[test]
fn minutes_suffix() {
assert_eq!(parse_duration("10m").unwrap(), 600);
assert_eq!(parse_duration("1m").unwrap(), 60);
assert_eq!(parse_duration("30m").unwrap(), 1800);
}
#[test]
fn hours_suffix() {
assert_eq!(parse_duration("1h").unwrap(), 3600);
assert_eq!(parse_duration("24h").unwrap(), 86400);
}
#[test]
fn days_suffix() {
assert_eq!(parse_duration("1d").unwrap(), 86400);
assert_eq!(parse_duration("7d").unwrap(), 604_800);
}
#[test]
fn weeks_suffix() {
assert_eq!(parse_duration("1w").unwrap(), 604_800);
assert_eq!(parse_duration("2w").unwrap(), 1_209_600);
}
#[test]
fn whitespace_trimmed() {
assert_eq!(parse_duration(" 60 ").unwrap(), 60);
assert_eq!(parse_duration(" 10m ").unwrap(), 600);
}
#[test]
fn empty_string_error() {
assert!(parse_duration("").is_err());
assert!(parse_duration(" ").is_err());
}
#[test]
fn invalid_suffix_error() {
assert!(parse_duration("10x").is_err());
assert!(parse_duration("5y").is_err());
}
#[test]
fn invalid_number_error() {
assert!(parse_duration("abcm").is_err());
assert!(parse_duration("m").is_err());
}
}