sleep_utils/
duration_parser.rs

1use crate::{Result, SleepError};
2use std::time::Duration;
3
4/// Parse sleep duration with support for multiple formats
5///
6/// Supports single units (e.g., "1s", "2m") and multiple units (e.g., "1m30s", "1h2m3s")
7pub fn parse_sleep_duration(input: &str) -> Result<Duration> {
8    let input = input.trim().to_lowercase();
9
10    if input.is_empty() {
11        return Ok(Duration::ZERO);
12    }
13
14    // Try to parse as plain number (default to milliseconds)
15    if let Ok(millis) = input.parse::<isize>() {
16        if millis <= 0 {
17            return Ok(Duration::ZERO);
18        }
19        return Ok(Duration::from_millis(millis as u64));
20    }
21
22    // Parse time with units (single or multiple)
23    if let Some(duration) = parse_duration_with_unit(&input)? {
24        Ok(duration)
25    } else {
26        Err(SleepError::InvalidDuration(format!(
27            "Invalid sleep duration format: '{}'",
28            input
29        )))
30    }
31}
32
33/// Parse duration with single or multiple time units
34fn parse_duration_with_unit(input: &str) -> Result<Option<Duration>> {
35    use lazy_static::lazy_static;
36    use regex::Regex;
37
38    lazy_static! {
39        // Patterns for single units (existing functionality)
40        static ref SINGLE_PATTERNS: Vec<(&'static str, f64)> = vec![
41            // Milliseconds
42            (r"^(\d+)\s*(ms|millis?|milliseconds?)$", 1.0),
43            // Seconds
44            (r"^(\d+)\s*(s|sec|seconds?)$", 1000.0),
45            // Minutes
46            (r"^(\d+)\s*(m|min|minutes?)$", 60_000.0),
47            // Hours
48            (r"^(\d+)\s*(h|hr|hours?)$", 3_600_000.0),
49            // Short format (no spaces)
50            (r"^(\d+)(ms)$", 1.0),
51            (r"^(\d+)(s)$", 1000.0),
52            (r"^(\d+)(m)$", 60_000.0),
53            (r"^(\d+)(h)$", 3_600_000.0),
54        ];
55
56        static ref FLOAT_PATTERNS: Vec<(&'static str, f64)> = vec![
57            (r"^(\d*\.?\d+)\s*(s|sec|seconds?)$", 1000.0),
58            (r"^(\d*\.?\d+)\s*(m|min|minutes?)$", 60_000.0),
59            (r"^(\d*\.?\d+)(s)$", 1000.0),
60            (r"^(\d*\.?\d+)(m)$", 60_000.0),
61        ];
62    }
63
64    // First, try single unit patterns (including float patterns)
65    for (pattern, multiplier) in SINGLE_PATTERNS.iter() {
66        let re = Regex::new(pattern).unwrap();
67        if let Some(caps) = re.captures(input) {
68            if let Ok(value) = caps[1].parse::<isize>() {
69                if value <= 0 {
70                    return Ok(Some(Duration::ZERO));
71                }
72                let millis = (value as f64 * multiplier) as u64;
73                return Ok(Some(Duration::from_millis(millis)));
74            }
75        }
76    }
77
78    // Then try float patterns (this handles "1.5s" correctly)
79    for (pattern, multiplier) in FLOAT_PATTERNS.iter() {
80        let re = Regex::new(pattern).unwrap();
81        if let Some(caps) = re.captures(input) {
82            if let Ok(value) = caps[1].parse::<f64>() {
83                if value <= 0.0 {
84                    return Ok(Some(Duration::ZERO));
85                }
86                let millis = (value * multiplier) as u64;
87                return Ok(Some(Duration::from_millis(millis)));
88            }
89        }
90    }
91
92    // Finally, try multiple units pattern (e.g., "1h2m3s")
93    // Only if no single unit pattern matched
94    if let Some(duration) = parse_multiple_units(input)? {
95        return Ok(Some(duration));
96    }
97
98    Ok(None)
99}
100
101/// Parse multiple time units in a single string
102fn parse_multiple_units(input: &str) -> Result<Option<Duration>> {
103    use lazy_static::lazy_static;
104    use regex::Regex;
105
106    lazy_static! {
107        // Improved pattern to avoid matching float numbers
108        static ref MULTI_UNIT_PATTERN: Regex = Regex::new(r"(?i)(\d+)\s*([a-z]+)").unwrap();
109    }
110
111    let mut total_millis: u64 = 0;
112    let mut found_any = false;
113    let mut has_positive_value = false;
114
115    for caps in MULTI_UNIT_PATTERN.captures_iter(input) {
116        let value: u64 = match caps[1].parse() {
117            Ok(v) => v,
118            Err(_) => continue,
119        };
120
121        let unit = &caps[2].to_lowercase();
122        let multiplier = match unit.as_str() {
123            "ms" | "milli" | "millis" | "millisecond" | "milliseconds" => 1,
124            "s" | "sec" | "second" | "seconds" => 1000,
125            "m" | "min" | "minute" | "minutes" => 60_000,
126            "h" | "hr" | "hour" | "hours" => 3_600_000,
127            _ => continue, // Skip unknown units
128        };
129
130        total_millis += value * multiplier;
131        found_any = true;
132        if value > 0 {
133            has_positive_value = true;
134        }
135    }
136
137    if found_any {
138        // Return Duration::ZERO for all-zero values like "0h0m0s"
139        if has_positive_value {
140            Ok(Some(Duration::from_millis(total_millis)))
141        } else {
142            Ok(Some(Duration::ZERO))
143        }
144    } else {
145        Ok(None)
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152
153    #[test]
154    fn test_parse_sleep_duration() -> Result<()> {
155        // Single unit tests (existing functionality)
156        assert_eq!(parse_sleep_duration("100")?, Duration::from_millis(100));
157        assert_eq!(parse_sleep_duration("100ms")?, Duration::from_millis(100));
158        assert_eq!(parse_sleep_duration("1s")?, Duration::from_secs(1));
159        assert_eq!(parse_sleep_duration("1.5s")?, Duration::from_millis(1500));
160        assert_eq!(parse_sleep_duration("0")?, Duration::ZERO);
161
162        // Multiple unit tests (new functionality)
163        assert_eq!(parse_sleep_duration("1m30s")?, Duration::from_millis(90000));
164        assert_eq!(
165            parse_sleep_duration("1h2m3s")?,
166            Duration::from_millis(3723000)
167        );
168        assert_eq!(
169            parse_sleep_duration("1h 2m 3s")?,
170            Duration::from_millis(3723000)
171        );
172        assert_eq!(
173            parse_sleep_duration("2s500ms")?,
174            Duration::from_millis(2500)
175        );
176        assert_eq!(
177            parse_sleep_duration("1m30s500ms")?,
178            Duration::from_millis(90500)
179        );
180
181        Ok(())
182    }
183
184    #[test]
185    fn test_multiple_units_edge_cases() -> Result<()> {
186        // Mixed formats
187        assert_eq!(parse_sleep_duration("1m30s")?, Duration::from_secs(90));
188        assert_eq!(parse_sleep_duration("1h30m")?, Duration::from_secs(5400));
189        assert_eq!(parse_sleep_duration("1h1s")?, Duration::from_secs(3601));
190
191        // With spaces
192        assert_eq!(parse_sleep_duration("1h 30m")?, Duration::from_secs(5400));
193        assert_eq!(parse_sleep_duration("2m 30s")?, Duration::from_secs(150));
194
195        // Zero values in multi-unit - these should return Duration::ZERO
196        assert_eq!(parse_sleep_duration("0h0m0s")?, Duration::ZERO);
197        assert_eq!(parse_sleep_duration("0s")?, Duration::ZERO);
198        assert_eq!(parse_sleep_duration("0ms")?, Duration::ZERO);
199
200        Ok(())
201    }
202
203    #[test]
204    fn test_float_numbers_not_matched_as_multi_units() -> Result<()> {
205        // These should be parsed as single units with float values, not multiple units
206        assert_eq!(parse_sleep_duration("1.5s")?, Duration::from_millis(1500));
207        assert_eq!(parse_sleep_duration("0.5m")?, Duration::from_millis(30000));
208        assert_eq!(parse_sleep_duration("2.5s")?, Duration::from_millis(2500));
209
210        // These should be parsed as multiple units
211        assert_eq!(
212            parse_sleep_duration("1s500ms")?,
213            Duration::from_millis(1500)
214        );
215        assert_eq!(parse_sleep_duration("1m30s")?, Duration::from_millis(90000));
216
217        Ok(())
218    }
219
220    #[test]
221    fn test_zero_values() -> Result<()> {
222        // All zero values should return Duration::ZERO
223        assert_eq!(parse_sleep_duration("0")?, Duration::ZERO);
224        assert_eq!(parse_sleep_duration("0ms")?, Duration::ZERO);
225        assert_eq!(parse_sleep_duration("0s")?, Duration::ZERO);
226        assert_eq!(parse_sleep_duration("0m")?, Duration::ZERO);
227        assert_eq!(parse_sleep_duration("0h")?, Duration::ZERO);
228        assert_eq!(parse_sleep_duration("0h0m0s")?, Duration::ZERO);
229        assert_eq!(parse_sleep_duration("0s0ms")?, Duration::ZERO);
230
231        Ok(())
232    }
233}