timeout_macro_parse/
parse_duration.rs1use 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}