1pub const DEFAULT_INTERVAL_SECS: u64 = 60;
7
8pub const DEFAULT_DURATION_SECS: u64 = 3600;
10
11pub const UNTIL_CANCELLED: [&str; 2] = ["forever", "inf"];
13
14pub fn is_until_cancelled(s: &str) -> bool {
16 UNTIL_CANCELLED.contains(&s.trim().to_ascii_lowercase().as_str())
17}
18
19pub fn parse_duration(s: &str) -> Result<u64, String> {
22 let s = s.trim();
23 if s.is_empty() {
24 return Err("Empty duration".to_string());
25 }
26 let (num_str, unit) = s
27 .char_indices()
28 .find(|(_, c)| !c.is_ascii_digit() && *c != '.')
29 .map(|(i, _)| (&s[..i], s[i..].trim()))
30 .unwrap_or((s, "s"));
31
32 let num: f64 = num_str
33 .parse()
34 .map_err(|_| format!("Invalid duration number: {}", num_str))?;
35
36 if num <= 0.0 {
37 return Err("Duration must be positive".to_string());
38 }
39
40 let secs = match unit.to_lowercase().as_str() {
41 "s" | "sec" | "secs" | "second" | "seconds" => num,
42 "m" | "min" | "mins" | "minute" | "minutes" => num * 60.0,
43 "h" | "hr" | "hrs" | "hour" | "hours" => num * 3600.0,
44 "d" | "day" | "days" => num * 86400.0,
45 _ => return Err(format!("Unknown duration unit: {}", unit)),
46 };
47
48 Ok(secs as u64)
49}
50
51pub fn format_duration(secs: u64) -> String {
53 match secs {
54 s if s > 0 && s % 86400 == 0 => format!("{}d", s / 86400),
55 s if s > 0 && s % 3600 == 0 => format!("{}h", s / 3600),
56 s if s > 0 && s % 60 == 0 => format!("{}m", s / 60),
57 s => format!("{s}s"),
58 }
59}
60
61#[cfg(test)]
62mod tests {
63 use super::*;
64
65 #[test]
66 fn units_parse() {
67 assert_eq!(parse_duration("30s"), Ok(30));
68 assert_eq!(parse_duration("5m"), Ok(300));
69 assert_eq!(parse_duration("1h"), Ok(3600));
70 assert_eq!(parse_duration("2d"), Ok(172_800));
71 assert_eq!(parse_duration("45"), Ok(45));
72 assert_eq!(parse_duration("1.5m"), Ok(90));
73 assert!(parse_duration("0s").is_err());
74 assert!(parse_duration("").is_err());
75 assert!(
76 parse_duration("3w")
77 .unwrap_err()
78 .contains("Unknown duration unit")
79 );
80 }
81
82 #[test]
83 fn format_reads_back() {
84 for secs in [1, 30, 60, 90, 3600, 5400, 86400] {
85 assert_eq!(parse_duration(&format_duration(secs)), Ok(secs));
86 }
87 assert_eq!(format_duration(3600), "1h");
88 assert_eq!(format_duration(90), "90s");
89 }
90
91 #[test]
92 fn until_cancelled_words() {
93 assert!(is_until_cancelled("forever"));
94 assert!(is_until_cancelled(" INF "));
95 assert!(!is_until_cancelled("1h"));
96 }
97}