1pub fn parse_time_ms(s: &str) -> Result<u64, String> {
25 let s = s.trim();
26 if s.is_empty() {
27 return Err("empty time value".into());
28 }
29 if let Ok(n) = s.parse::<f64>() {
31 if !n.is_finite() || n < 0.0 {
32 return Err(format!("time value {s:?}: must be a non-negative number"));
33 }
34 return Ok((n * 1000.0).round() as u64);
35 }
36 let bytes = s.as_bytes();
38 let mut i = 0;
39 let mut total_ms: f64 = 0.0;
40 while i < bytes.len() {
41 let start = i;
42 while i < bytes.len() && (bytes[i].is_ascii_digit() || bytes[i] == b'.') {
43 i += 1;
44 }
45 if i == start {
46 return Err(format!(
47 "time value {s:?}: expected a number before the unit"
48 ));
49 }
50 let n: f64 = s[start..i]
51 .parse()
52 .map_err(|e| format!("time value {s:?}: bad number: {e}"))?;
53 if !n.is_finite() || n < 0.0 {
54 return Err(format!("time value {s:?}: must be non-negative"));
55 }
56 let unit_start = i;
57 while i < bytes.len() && bytes[i].is_ascii_alphabetic() {
58 i += 1;
59 }
60 let unit = &s[unit_start..i];
61 let mult = match unit {
62 "ms" => 1.0,
63 "s" => 1_000.0,
64 "m" => 60_000.0,
65 "h" => 3_600_000.0,
66 "d" => 86_400_000.0,
67 "" => {
68 return Err(format!(
69 "time value {s:?}: missing unit (use ms/s/m/h/d, \
70 or a bare number for seconds)"
71 ));
72 }
73 other => {
74 return Err(format!(
75 "time value {s:?}: unknown unit {other:?} (use ms/s/m/h/d)"
76 ));
77 }
78 };
79 total_ms += n * mult;
80 }
81 Ok(total_ms.round() as u64)
82}
83
84#[cfg(test)]
85mod tests {
86 use super::parse_time_ms;
87
88 #[test]
89 fn suffix_forms() {
90 assert_eq!(parse_time_ms("60s").unwrap(), 60_000);
91 assert_eq!(parse_time_ms("500ms").unwrap(), 500);
92 assert_eq!(parse_time_ms("2.5s").unwrap(), 2_500);
93 assert_eq!(parse_time_ms("1h30m").unwrap(), 5_400_000);
94 assert_eq!(parse_time_ms("1d").unwrap(), 86_400_000);
95 }
96
97 #[test]
98 fn bare_number_is_fractional_seconds() {
99 assert_eq!(parse_time_ms("60").unwrap(), 60_000);
100 assert_eq!(parse_time_ms("60.5").unwrap(), 60_500);
101 assert_eq!(parse_time_ms("0.25").unwrap(), 250);
102 assert_eq!(parse_time_ms("0").unwrap(), 0);
103 }
104
105 #[test]
106 fn rejects_garbage() {
107 assert!(parse_time_ms("abc").is_err());
108 assert!(parse_time_ms("10x").is_err());
109 assert!(parse_time_ms("").is_err());
110 assert!(parse_time_ms("-5").is_err());
111 assert!(parse_time_ms("10 s foo").is_err());
112 }
113}