1use std::{fmt, str::FromStr, time::Duration};
2
3use serde::{Deserialize, Serialize};
4
5#[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 #[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 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), 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()); assert!("-10s".parse::<Interval>().is_err()); assert!("10x".parse::<Interval>().is_err()); assert!("10s5".parse::<Interval>().is_err()); assert!("10.5s".parse::<Interval>().is_err()); }
293
294 #[test]
295 fn test_overflow_protection() {
296 assert!(
298 "9999999999999999999999999999999999999w"
299 .parse::<Interval>()
300 .is_err()
301 );
302 assert!("18446744073709551616s".parse::<Interval>().is_err());
304 }
305}