ambient_time/
lib.rs

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
51/// Parses a duration in the format of `45` or `45s 1m`. Is overly relaxed and
52/// will ignore spaces and mispellings.
53pub 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        // Consume
78        s = tail;
79    }
80
81    // Anything without a suffix is considered as seconds
82    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}