1use std::time::Duration;
2
3use anyhow::{Context, Result, bail};
4
5pub fn parse_duration(input: &str) -> Result<Duration> {
6 let trimmed = input.trim();
7 if trimmed.is_empty() {
8 bail!("duration must not be empty");
9 }
10 let split = trimmed
11 .char_indices()
12 .find(|(_, ch)| !ch.is_ascii_digit() && *ch != '.')
13 .map(|(i, _)| i)
14 .unwrap_or(trimmed.len());
15 let (number, unit) = trimmed.split_at(split);
16 if number.is_empty() {
17 bail!("duration {input:?} is missing a number");
18 }
19 let value: f64 = number
20 .parse()
21 .with_context(|| format!("invalid duration number in {input:?}"))?;
22 if !value.is_finite() || value < 0.0 {
23 bail!("duration {input:?} must be a finite non-negative number");
24 }
25 let factor = match unit.trim().to_ascii_lowercase().as_str() {
26 "" | "s" | "sec" | "secs" | "second" | "seconds" => 1.0,
27 "m" | "min" | "mins" | "minute" | "minutes" => 60.0,
28 "h" | "hr" | "hrs" | "hour" | "hours" => 3600.0,
29 "d" | "day" | "days" => 86400.0,
30 other => bail!("unknown duration unit {other:?} in {input:?}"),
31 };
32 Ok(Duration::from_secs_f64(value * factor))
33}
34
35pub fn parse_idle(input: Option<&str>) -> Result<Option<Duration>> {
36 input.map(parse_duration).transpose()
37}
38
39#[cfg(test)]
40#[path = "duration_tests.rs"]
41mod tests;