1use std::time::Duration;
2
3use itertools::{Itertools, PeekingNext};
4use thiserror::Error;
5
6#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Error)]
7pub enum DurationParseError {
8 #[error("Missing number for suffix {0}")]
9 MissingIntegral(String),
10 #[error("Duplicate suffix")]
11 DoubleSuffix,
12 #[error("Duplicate number without identifier")]
13 DoubleIntegral,
14 #[error("Malformed integral")]
15 MalformedIntegral(String),
16 #[error("Malformed suffix")]
17 MalformedSuffix(String),
18}
19
20#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
21pub enum DurationScale {
22 Milliseconds,
23 Seconds,
24 Minutes,
25 Hours,
26}
27
28impl DurationScale {
29 pub fn to_duration(&self, time: f64) -> Duration {
30 let scale = match self {
31 DurationScale::Milliseconds => 1e-3,
32 DurationScale::Seconds => 1.0,
33 DurationScale::Minutes => 60.0,
34 DurationScale::Hours => 3600.0,
35 };
36
37 Duration::from_secs_f64(time * scale)
38 }
39
40 pub fn parse(s: &str) -> Option<Self> {
41 match s {
42 "ms" | "millis" | "millisecond" | "milliseconds" => Some(Self::Milliseconds),
43 "s" | "sec" | "second" | "seconds" => Some(Self::Seconds),
44 "m" | "min" | "minute" | "minutes" => Some(Self::Minutes),
45 "h" | "hour" | "hours" => Some(Self::Hours),
46 _ => None,
47 }
48 }
49}
50
51pub fn parse_duration(mut s: &str) -> Result<Duration, DurationParseError> {
54 let mut num: Option<f64> = None;
55
56 let mut dur = Duration::ZERO;
57 while let Some((kind, head, tail)) = tok(s) {
58 match (kind, num) {
59 (TokenKind::Integral, None) => {
60 num = Some(
61 head.parse()
62 .map_err(|_| DurationParseError::MalformedIntegral(head.to_string()))?,
63 )
64 }
65 (TokenKind::Integral, Some(_)) => return Err(DurationParseError::DoubleIntegral),
66 (TokenKind::Identifier, None) => {
67 return Err(DurationParseError::MissingIntegral(head.to_string()))
68 }
69 (TokenKind::Identifier, Some(n)) => {
70 let scale = DurationScale::parse(head)
71 .ok_or_else(|| DurationParseError::MalformedSuffix(head.to_string()))?;
72 dur += scale.to_duration(n);
73 num = None;
74 }
75 (TokenKind::WhiteSpace, _) => {}
76 }
77 s = tail;
79 }
80
81 if let Some(num) = num {
83 dur += DurationScale::Seconds.to_duration(num);
84 }
85
86 Ok(dur)
87}
88
89#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
90enum TokenKind {
91 Integral,
92 Identifier,
93 WhiteSpace,
94}
95
96fn consume_integral(
97 iter: &mut impl PeekingNext<Item = (usize, char)>,
98) -> Option<(TokenKind, usize)> {
99 iter.peeking_take_while(|(_, c)| c.is_ascii_digit() || *c == ',' || *c == '.')
100 .last()
101 .map(|(i, c)| (TokenKind::Integral, i + c.len_utf8()))
102}
103
104fn consume_ident(iter: &mut impl PeekingNext<Item = (usize, char)>) -> Option<(TokenKind, usize)> {
105 iter.peeking_take_while(|(_, c)| c.is_alphabetic())
106 .last()
107 .map(|(i, c)| (TokenKind::Identifier, i + c.len_utf8()))
108}
109
110fn consume_whitespace(
111 iter: &mut impl PeekingNext<Item = (usize, char)>,
112) -> Option<(TokenKind, usize)> {
113 iter.peeking_take_while(|(_, c)| c.is_whitespace() || matches!(*c, ',' | '.' | ':'))
114 .last()
115 .map(|(i, c)| (TokenKind::WhiteSpace, i + c.len_utf8()))
116}
117
118fn tok(s: &str) -> Option<(TokenKind, &str, &str)> {
119 let mut iter = s.char_indices();
120 let tok = consume_integral(&mut iter)
121 .or_else(|| consume_ident(&mut iter))
122 .or_else(|| consume_whitespace(&mut iter));
123
124 if let Some((kind, tok)) = tok {
125 let (head, tail) = s.split_at(tok);
126 Some((kind, head, tail))
127 } else {
128 None
129 }
130}
131
132#[cfg(test)]
133mod test {
134 use super::*;
135
136 #[test]
137 fn parse_duration() {
138 let input = ["", "1s", "4m", "5m2s"];
139 let output = input.into_iter().map(super::parse_duration).collect_vec();
140 let expected = [
141 Ok(Duration::ZERO),
142 Ok(Duration::from_secs(1)),
143 Ok(Duration::from_secs(240)),
144 Ok(Duration::from_secs(302)),
145 ];
146 assert_eq!(output, expected);
147 }
148}