timeout_macro_parse/
parse_duration.rs

1use std::time::Duration;
2
3pub fn parse_duration(dur: &str) -> Result<Duration, String> {
4    let dur = dur.trim_matches('"');
5    let mut it = dur.chars().enumerate();
6    let mut prev_ind = None;
7    let mut parsed_dur = Duration::ZERO;
8    let mut dirty = false;
9    loop {
10        let Some((ind, ch)) = it.next() else {
11            return if parsed_dur == Duration::ZERO {
12                Err(format!("parsing '{dur}' resulted in a zero duration"))
13            } else if dirty {
14                Err(format!(
15                    "parsing '{dur}' resulted in an unfinished calculation"
16                ))
17            } else {
18                Ok(parsed_dur)
19            };
20        };
21        parse_next(
22            &mut prev_ind,
23            ind,
24            ch,
25            dur,
26            &mut parsed_dur,
27            &mut it,
28            &mut dirty,
29        )?;
30    }
31}
32
33fn parse_next(
34    prev_ind: &mut Option<usize>,
35    ind: usize,
36    ch: char,
37    dur: &str,
38    cumulative_dur: &mut Duration,
39    iterator: &mut impl Iterator<Item = (usize, char)>,
40    dirty: &mut bool,
41) -> Result<(), String> {
42    if ch.is_alphabetic() {
43        let pi = prev_ind.unwrap_or_default();
44        let Some(prev) = dur.get(pi..ind) else {
45            return Err(format!("failed to parse duration from: '{dur}'"));
46        };
47        let num = parse_num(prev)?;
48        let (add_dur, rem, add) = create_duration(num, ch, iterator)?;
49        *cumulative_dur = cumulative_dur.saturating_add(add_dur);
50        *dirty = false;
51        *prev_ind = Some(ind + add);
52        if let Some((next_ind, ch)) = rem {
53            parse_next(prev_ind, next_ind, ch, dur, cumulative_dur, iterator, dirty)?;
54        }
55    } else {
56        *dirty = true;
57    }
58    Ok(())
59}
60
61fn parse_num(sect: &str) -> Result<u64, String> {
62    if sect.is_empty() {
63        return Err("failed to parse num, empty section".to_string());
64    };
65    sect.parse()
66        .map_err(|e| format!("failed to parse num from '{sect}': {e}"))
67}
68
69#[allow(clippy::type_complexity)]
70fn create_duration(
71    num: u64,
72    lead_char: char,
73    iterator: &mut impl Iterator<Item = (usize, char)>,
74) -> Result<(Duration, Option<(usize, char)>, usize), String> {
75    let (unit, rem, add) = parse_unit(lead_char, iterator)?;
76    let dur = match unit {
77        AcceptedUnits::Hour => Duration::from_secs(num * 60 * 60),
78        AcceptedUnits::Minute => Duration::from_secs(num * 60),
79        AcceptedUnits::Second => Duration::from_secs(num),
80        AcceptedUnits::Millisecond => Duration::from_millis(num),
81    };
82    Ok((dur, rem, add))
83}
84
85#[allow(clippy::type_complexity)]
86fn parse_unit(
87    start: char,
88    iterator: &mut impl Iterator<Item = (usize, char)>,
89) -> Result<(AcceptedUnits, Option<(usize, char)>, usize), String> {
90    match start {
91        'h' => Ok((AcceptedUnits::Hour, None, 1)),
92        'm' => {
93            let next = iterator.next();
94            if let Some((_, 's')) = next {
95                Ok((AcceptedUnits::Millisecond, None, 2))
96            } else {
97                Ok((AcceptedUnits::Minute, next, 1))
98            }
99        }
100        's' => Ok((AcceptedUnits::Second, None, 1)),
101        unk => Err(format!("unknown unit start: '{unk}'")),
102    }
103}
104
105enum AcceptedUnits {
106    Hour,
107    Minute,
108    Second,
109    Millisecond,
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn parse_reasonable_durations() {
118        let hours = parse_duration("3h").unwrap();
119        assert_eq!(hours, Duration::from_secs(3 * 3600));
120        let minutes = parse_duration("11m").unwrap();
121        assert_eq!(minutes, Duration::from_secs(11 * 60));
122        let seconds = parse_duration("55s").unwrap();
123        assert_eq!(seconds, Duration::from_secs(55));
124        let millis = parse_duration("100ms").unwrap();
125        assert_eq!(millis, Duration::from_millis(100));
126        let combined = "1h2m3s4ms";
127        let dur = parse_duration(combined).unwrap();
128        let expect = Duration::from_secs(3600)
129            + Duration::from_secs(120)
130            + Duration::from_secs(3)
131            + Duration::from_millis(4);
132        assert_eq!(dur, expect);
133    }
134
135    #[test]
136    fn parse_unreasonable_additive_durations() {
137        let dur = "1h1h1h1h";
138        let dur = parse_duration(dur).unwrap();
139        assert_eq!(Duration::from_secs(3600 * 4), dur);
140        let dur = "1m1m1m";
141        let dur = parse_duration(dur).unwrap();
142        assert_eq!(Duration::from_secs(60 * 3), dur);
143        let dur = "1s1s";
144        let dur = parse_duration(dur).unwrap();
145        assert_eq!(Duration::from_secs(2), dur);
146        let dur = "1ms1ms1ms1ms1ms1ms1ms";
147        let dur = parse_duration(dur).unwrap();
148        assert_eq!(Duration::from_millis(7), dur);
149        let dur = "5ms2s1h5ms1m1s";
150        let dur = parse_duration(dur).unwrap();
151        assert_eq!(
152            Duration::from_millis(10) + Duration::from_secs(63) + Duration::from_secs(3600),
153            dur
154        );
155    }
156}