Skip to main content

tocat_api/
interval.rs

1use std::{fmt, str::FromStr, time::Duration};
2
3use serde::{Deserialize, Serialize};
4
5/// A duration.
6///
7/// Accepts a plain number (seconds) or a suffix: `1`, `1s`, `1ms`, `1m1us`.
8#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
9pub struct Interval(Duration);
10
11impl Interval {
12    #[must_use]
13    pub fn duration(self) -> Duration {
14        self.0
15    }
16}
17
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct ParseIntervalError(String);
20
21impl fmt::Display for ParseIntervalError {
22    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23        f.write_str(&self.0)
24    }
25}
26
27impl std::error::Error for ParseIntervalError {}
28
29const MICROS: u128 = 1000;
30const MILLIS: u128 = 1000 * MICROS;
31const SECONDS: u128 = 1000 * MILLIS;
32const MINUTES: u128 = 60 * SECONDS;
33const HOURS: u128 = 60 * MINUTES;
34const DAYS: u128 = 24 * HOURS;
35const WEEKS: u128 = 7 * DAYS;
36
37impl fmt::Display for Interval {
38    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39        const UNITS: [(u128, &str); 7] = [
40            (WEEKS, "w"),
41            (DAYS, "d"),
42            (HOURS, "h"),
43            (MINUTES, "m"),
44            (SECONDS, "s"),
45            (MILLIS, "ms"),
46            (MICROS, "us"),
47        ];
48
49        let nanos = self.0.as_nanos();
50
51        let (mut res, rem) =
52            UNITS
53                .iter()
54                .fold((String::new(), nanos), |(mut s, rem), (scale, suffix)| {
55                    let count = rem / scale;
56                    if count > 0 {
57                        s.push_str(&format!("{count}{suffix}"));
58                    }
59
60                    (s, rem % scale)
61                });
62
63        if rem > 0 {
64            res.push_str(&format!("{rem}ns"));
65        }
66
67        if res.is_empty() {
68            write!(f, "0s")
69        } else {
70            write!(f, "{res}")
71        }
72    }
73}
74
75impl FromStr for Interval {
76    type Err = ParseIntervalError;
77
78    fn from_str(s: &str) -> Result<Self, Self::Err> {
79        let mut rest = s.trim();
80        let mut total_nanos = 0u128;
81        let mut seen_units = [false; 8];
82
83        while !rest.is_empty() {
84            let digit_len = rest
85                .chars()
86                .take_while(|c| c.is_ascii_digit())
87                .map(|c| c.len_utf8())
88                .sum::<usize>();
89
90            if digit_len == 0 {
91                return Err(ParseIntervalError("Invalid interval".to_string()));
92            }
93
94            let (num_str, tail) = rest.split_at(digit_len);
95            let count: u128 = num_str
96                .parse()
97                .map_err(|_| ParseIntervalError("Failed to parse duration".to_string()))?;
98
99            let unit_len = tail
100                .chars()
101                .take_while(|c| c.is_ascii_alphabetic())
102                .map(|c| c.len_utf8())
103                .sum::<usize>();
104
105            let (unit_str, next_rest) = if unit_len == 0 {
106                ("s", tail)
107            } else {
108                tail.split_at(unit_len)
109            };
110
111            let (multiplier, idx) = match unit_str.to_lowercase().as_str() {
112                "ns" => (1, 0),
113                "us" => (MICROS, 1),
114                "ms" => (MILLIS, 2),
115                "s" => (SECONDS, 3),
116                "m" => (MINUTES, 4),
117                "h" => (HOURS, 5),
118                "d" => (DAYS, 6),
119                "w" => (WEEKS, 7),
120                unit => return Err(ParseIntervalError(format!("Invalid unit: {unit}"))),
121            };
122
123            if seen_units[idx] {
124                return Err(ParseIntervalError("Duplicate units".to_string()));
125            }
126
127            seen_units[idx] = true;
128
129            total_nanos = total_nanos
130                .checked_add(
131                    count
132                        .checked_mul(multiplier)
133                        .ok_or_else(|| ParseIntervalError("Duration overflow".to_string()))?,
134                )
135                .ok_or_else(|| ParseIntervalError("Duration overflow".to_string()))?;
136
137            rest = next_rest;
138        }
139
140        let max = Duration::MAX.as_nanos();
141
142        if total_nanos > max {
143            return Err(ParseIntervalError("Duration too long".to_string()));
144        }
145
146        Ok(Interval(Duration::from_nanos_u128(total_nanos)))
147    }
148}
149
150impl Serialize for Interval {
151    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
152        serializer.serialize_str(&self.to_string())
153    }
154}
155
156impl<'de> Deserialize<'de> for Interval {
157    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
158        use serde::de::Error as _;
159
160        // Both `interval = 10` and `interval = "10s"` are natural
161        // things to write, so accept either.
162        #[derive(Deserialize)]
163        #[serde(untagged)]
164        enum Raw {
165            Secs(u64),
166            Text(String),
167        }
168
169        match Raw::deserialize(deserializer)? {
170            Raw::Secs(s) => Ok(Interval(Duration::from_secs(s))),
171            Raw::Text(s) => s.parse().map_err(D::Error::custom),
172        }
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use std::time::Duration;
179
180    use super::*;
181
182    #[test]
183    fn test_display_single_units() {
184        assert_eq!(Interval(Duration::from_nanos(500)).to_string(), "500ns");
185        assert_eq!(Interval(Duration::from_micros(5)).to_string(), "5us");
186        assert_eq!(Interval(Duration::from_millis(10)).to_string(), "10ms");
187        assert_eq!(Interval(Duration::from_secs(45)).to_string(), "45s");
188        assert_eq!(Interval(Duration::from_secs(120)).to_string(), "2m");
189        assert_eq!(Interval(Duration::from_secs(3600)).to_string(), "1h");
190        assert_eq!(Interval(Duration::from_secs(86400)).to_string(), "1d");
191        assert_eq!(Interval(Duration::from_secs(604800)).to_string(), "1w");
192    }
193
194    #[test]
195    fn test_display_combined_units() {
196        // 1 week + 2 days + 3 hours + 4 mins + 5 secs + 6 ms + 7 us + 8 ns
197        let nanos = (7 * 86400 + 2 * 86400 + 3 * 3600 + 4 * 60 + 5) * 1_000_000_000u128
198            + 6 * 1_000_000
199            + 7 * 1_000
200            + 8;
201
202        let interval = Interval(Duration::from_nanos(nanos as u64));
203        assert_eq!(interval.to_string(), "1w2d3h4m5s6ms7us8ns");
204    }
205
206    #[test]
207    fn test_display_zero() {
208        assert_eq!(Interval(Duration::ZERO).to_string(), "0s");
209    }
210
211    #[test]
212    fn test_from_str_basic() {
213        assert_eq!("".parse::<Interval>(), Ok(Interval(Duration::ZERO)));
214        assert_eq!("0".parse::<Interval>(), Ok(Interval(Duration::ZERO)));
215        assert_eq!(
216            "10".parse::<Interval>(),
217            Ok(Interval(Duration::from_secs(10)))
218        );
219        assert_eq!(
220            "500ns".parse::<Interval>(),
221            Ok(Interval(Duration::from_nanos(500)))
222        );
223        assert_eq!(
224            "5us".parse::<Interval>(),
225            Ok(Interval(Duration::from_micros(5)))
226        );
227        assert_eq!(
228            "10ms".parse::<Interval>(),
229            Ok(Interval(Duration::from_millis(10)))
230        );
231        assert_eq!(
232            "45s".parse::<Interval>(),
233            Ok(Interval(Duration::from_secs(45)))
234        );
235        assert_eq!(
236            "2m".parse::<Interval>(),
237            Ok(Interval(Duration::from_secs(120)))
238        );
239        assert_eq!(
240            "1h".parse::<Interval>(),
241            Ok(Interval(Duration::from_secs(3600)))
242        );
243        assert_eq!(
244            "1d".parse::<Interval>(),
245            Ok(Interval(Duration::from_secs(86400)))
246        );
247        assert_eq!(
248            "1w".parse::<Interval>(),
249            Ok(Interval(Duration::from_secs(604800)))
250        );
251    }
252
253    #[test]
254    fn test_from_str_combined() {
255        let expected_secs = (7 * 86400) + (2 * 86400) + (3 * 3600) + (4 * 60) + 5;
256        let expected_nanos = (6 * 1_000_000) + (7 * 1_000) + 8;
257
258        let parsed: Interval = "1w2d3h4m5s6ms7us8ns".parse().unwrap();
259        assert_eq!(
260            parsed,
261            Interval(Duration::new(expected_secs, expected_nanos as u32))
262        );
263    }
264
265    #[test]
266    fn test_round_trip() {
267        let original_durations = vec![
268            Duration::ZERO,
269            Duration::from_nanos(1),
270            Duration::from_micros(999),
271            Duration::from_millis(123456789),
272            Duration::from_secs(31536000), // ~1 year in seconds
273            Duration::new(123456, 789012345),
274        ];
275
276        for dur in original_durations {
277            let interval = Interval(dur);
278            let formatted = interval.to_string();
279            let parsed: Interval = formatted.parse().expect("Failed to parse formatted string");
280            assert_eq!(interval, parsed, "Round trip failed for {formatted}");
281        }
282    }
283
284    #[test]
285    fn test_invalid_formats() {
286        assert!("abc".parse::<Interval>().is_err());
287        assert!("s".parse::<Interval>().is_err()); // Missing quantity
288        assert!("-10s".parse::<Interval>().is_err()); // Negative duration
289        assert!("10x".parse::<Interval>().is_err()); // Invalid unit
290        assert!("10s5".parse::<Interval>().is_err()); // Trailing unitless number
291        assert!("10.5s".parse::<Interval>().is_err()); // Decimals not supported
292    }
293
294    #[test]
295    fn test_overflow_protection() {
296        // u128 overflow during scalar multiplication
297        assert!(
298            "9999999999999999999999999999999999999w"
299                .parse::<Interval>()
300                .is_err()
301        );
302        // Value exceeding Duration::MAX (~u64 MAX seconds)
303        assert!("18446744073709551616s".parse::<Interval>().is_err());
304    }
305}