1use std::fmt::{Display, Formatter};
36use std::ops::{Add, Sub};
37use std::str::FromStr;
38use std::time;
39
40use chrono::{DateTime, Duration, TimeZone};
41#[cfg(feature = "clap")]
42use clap::builder::OsStr;
43use once_cell::sync::Lazy;
44use regex::{Match, Regex};
45#[cfg(feature = "serde")]
46use serde::de::{Error, Unexpected, Visitor};
47#[cfg(feature = "serde")]
48use serde::{Deserialize, Deserializer, Serialize, Serializer};
49
50const SECS_PER_MINUTES: i64 = 60;
51const SECS_PER_HOUR: i64 = 60 * SECS_PER_MINUTES;
52const SECS_PER_DAY: i64 = 24 * SECS_PER_HOUR;
53const SECS_PER_WEEK: i64 = 7 * SECS_PER_DAY;
54
55#[derive(Copy, Clone, Debug)]
57pub enum DurationFlexError {
58 InvalidFormat,
60
61 OutOfRange,
63}
64
65#[allow(clippy::tabs_in_doc_comments)]
66#[derive(Copy, Clone, Debug, Eq, PartialEq)]
88pub struct DurationFlex {
89 secs: i64,
90 nanos: i32,
91}
92
93static REGEX_STR: &str =
94 r"^((?P<weeks>\d+)w)?((?P<days>\d+)d)?((?P<hours>\d+)h)?((?P<minutes>\d+)m)?((?P<seconds>\d+)s)?$";
95
96static REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(REGEX_STR).unwrap());
97
98impl DurationFlex {
99 pub fn secs(&self) -> i64 {
101 self.secs
102 }
103
104 pub fn nanos(&self) -> i32 {
106 self.nanos
107 }
108
109 fn de_component(r#match: Match) -> i64 {
110 r#match.as_str().parse().unwrap()
111 }
112
113 fn ser_component(secs: &mut i64, component: &str, component_secs: i64, f: &mut Formatter<'_>) -> std::fmt::Result {
114 let value = *secs / component_secs;
115 *secs -= value * component_secs;
116
117 if value == 0 {
118 Ok(())
119 } else {
120 write!(f, "{}{}", value, component)
121 }
122 }
123}
124
125impl Sub<Duration> for DurationFlex {
126 type Output = Duration;
127
128 fn sub(self, rhs: Duration) -> Self::Output {
129 Duration::from(self) - rhs
130 }
131}
132
133impl Add<Duration> for DurationFlex {
134 type Output = Duration;
135
136 fn add(self, rhs: Duration) -> Self::Output {
137 Duration::from(self) + rhs
138 }
139}
140
141impl<T> Add<DateTime<T>> for DurationFlex
142where
143 T: TimeZone,
144{
145 type Output = DateTime<T>;
146
147 fn add(self, rhs: DateTime<T>) -> Self::Output {
148 rhs + Duration::from(self)
149 }
150}
151
152impl<T> Add<DurationFlex> for DateTime<T>
153where
154 T: TimeZone,
155{
156 type Output = DateTime<T>;
157
158 fn add(self, rhs: DurationFlex) -> Self::Output {
159 self + Duration::from(rhs)
160 }
161}
162
163impl TryFrom<&str> for DurationFlex {
164 type Error = DurationFlexError;
165
166 fn try_from(value: &str) -> Result<Self, Self::Error> {
167 let captures = REGEX.captures(value).ok_or(DurationFlexError::InvalidFormat)?;
168
169 let weeks = Duration::try_weeks(captures.name("weeks").map_or(0i64, Self::de_component))
170 .ok_or(DurationFlexError::OutOfRange)?;
171 let days = Duration::try_days(captures.name("days").map_or(0i64, Self::de_component))
172 .ok_or(DurationFlexError::OutOfRange)?;
173 let hours = Duration::try_hours(captures.name("hours").map_or(0i64, Self::de_component))
174 .ok_or(DurationFlexError::OutOfRange)?;
175 let minutes = Duration::try_minutes(captures.name("minutes").map_or(0i64, Self::de_component))
176 .ok_or(DurationFlexError::OutOfRange)?;
177 let seconds = Duration::try_seconds(captures.name("seconds").map_or(0i64, Self::de_component))
178 .ok_or(DurationFlexError::OutOfRange)?;
179
180 let duration = weeks + days + hours + minutes + seconds;
181
182 Ok(DurationFlex { secs: duration.num_seconds(), nanos: 0i32 })
183 }
184}
185
186impl From<String> for DurationFlex {
187 fn from(value: String) -> Self {
188 DurationFlex::try_from(value.as_str()).unwrap()
189 }
190}
191
192impl From<Duration> for DurationFlex {
193 fn from(value: Duration) -> Self {
194 DurationFlex { secs: value.num_seconds(), nanos: 0i32 }
195 }
196}
197
198impl From<DurationFlex> for Duration {
199 fn from(value: DurationFlex) -> Self {
200 Duration::try_seconds(value.secs()).unwrap() + Duration::nanoseconds(value.nanos() as i64)
201 }
202}
203
204impl From<time::Duration> for DurationFlex {
205 fn from(value: time::Duration) -> Self {
206 DurationFlex { secs: value.as_secs() as i64, nanos: 0i32 }
207 }
208}
209
210impl From<DurationFlex> for time::Duration {
211 fn from(value: DurationFlex) -> Self {
212 time::Duration::from_secs(value.secs as u64).add(time::Duration::from_nanos(value.nanos as u64))
213 }
214}
215
216impl Display for DurationFlex {
217 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
218 let mut secs = self.secs;
219
220 Self::ser_component(&mut secs, "w", SECS_PER_WEEK, f)?;
221 Self::ser_component(&mut secs, "d", SECS_PER_DAY, f)?;
222 Self::ser_component(&mut secs, "h", SECS_PER_HOUR, f)?;
223 Self::ser_component(&mut secs, "m", SECS_PER_MINUTES, f)?;
224 Self::ser_component(&mut secs, "s", 1, f)
225 }
226}
227
228#[cfg(feature = "serde")]
229impl<'de> Deserialize<'de> for DurationFlex {
230 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
231 where
232 D: Deserializer<'de>,
233 {
234 static REGEX_MSG: &str =
235 "a String with the format weeks (w), days (d), hours (h), minutes (m) and/or seconds (s), in order";
236
237 struct DurationFlexVisitor;
238
239 impl<'de> Visitor<'de> for DurationFlexVisitor {
240 type Value = DurationFlex;
241
242 fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
243 formatter.write_str(REGEX_MSG)
244 }
245
246 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
247 where
248 E: Error,
249 {
250 match DurationFlex::try_from(v) {
251 Ok(value) => Ok(value),
252 Err(DurationFlexError::InvalidFormat) => Err(Error::invalid_value(Unexpected::Str(v), &self)),
253 Err(DurationFlexError::OutOfRange) => Err(Error::invalid_value(Unexpected::Str(v), &self)),
254 }
255 }
256
257 fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E>
258 where
259 E: Error,
260 {
261 self.visit_str(v)
262 }
263
264 fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
265 where
266 E: Error,
267 {
268 match DurationFlex::try_from(v.as_str()) {
269 Ok(value) => Ok(value),
270 Err(DurationFlexError::InvalidFormat) => {
271 Err(Error::invalid_value(Unexpected::Str(v.as_str()), &self))
272 },
273 Err(DurationFlexError::OutOfRange) => Err(Error::invalid_value(Unexpected::Str(v.as_str()), &self)),
274 }
275 }
276 }
277
278 deserializer.deserialize_string(DurationFlexVisitor)
279 }
280}
281
282#[cfg(feature = "serde")]
283impl Serialize for DurationFlex {
284 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
285 where
286 S: Serializer,
287 {
288 serializer.serialize_str(format!("{}", self).as_str())
289 }
290}
291
292#[cfg(feature = "clap")]
293impl From<OsStr> for DurationFlex {
294 fn from(value: OsStr) -> Self {
295 DurationFlex::try_from(value.to_str().unwrap()).unwrap()
296 }
297}
298
299#[cfg(feature = "clap")]
300impl From<DurationFlex> for OsStr {
301 fn from(value: DurationFlex) -> Self {
302 format!("{}", value).into()
303 }
304}
305
306impl FromStr for DurationFlex {
307 type Err = DurationFlexError;
308
309 fn from_str(s: &str) -> Result<Self, Self::Err> {
310 DurationFlex::try_from(s)
311 }
312}
313
314#[cfg(test)]
315mod test {
316
317 use serde::{Deserialize, Serialize};
318 use serde_test::{assert_de_tokens, assert_ser_tokens, Token};
319
320 use super::*;
321
322 #[test]
323 fn de_string() {
324 let value = DurationFlex::try_from("1w2d").unwrap();
325 assert_eq!(value.secs(), 9 * SECS_PER_DAY);
326 assert_eq!(value.nanos(), 0);
327
328 let value = DurationFlex::try_from("1w2d3h4m5s").unwrap();
329 assert_eq!(value.secs(), 9 * SECS_PER_DAY + 3 * SECS_PER_HOUR + 4 * SECS_PER_MINUTES + 5);
330 assert_eq!(value.nanos(), 0);
331
332 let value = DurationFlex::try_from("5s").unwrap();
333 assert_eq!(value.secs(), 5);
334 assert_eq!(value.nanos(), 0);
335
336 let value = DurationFlex::try_from("5s5d");
337 assert!(value.is_err());
338 }
339
340 #[test]
341 fn ser_string() {
342 let value = DurationFlex::try_from("1w2d").unwrap().to_string();
343 assert_eq!(value, "1w2d");
344
345 let value = DurationFlex::try_from("1w2d3h4m5s").unwrap().to_string();
346 assert_eq!(value, "1w2d3h4m5s");
347
348 let value = DurationFlex::try_from("5s").unwrap().to_string();
349 assert_eq!(value, "5s");
350
351 let value = DurationFlex::try_from("1w8d3h4m5s").unwrap().to_string();
352 assert_eq!(value, "2w1d3h4m5s");
353
354 let value = DurationFlex::try_from("1w8d3h4m3605s").unwrap().to_string();
355 assert_eq!(value, "2w1d4h4m5s");
356 }
357
358 #[test]
359 fn deserialize_nums() {
360 let value = DurationFlex::try_from("1w2d").unwrap();
361 assert_de_tokens(&value, &[Token::Str("1w2d")]);
362
363 let value = DurationFlex::try_from("1w2d3h4m5s").unwrap();
364 assert_de_tokens(&value, &[Token::Str("1w2d3h4m5s")]);
365
366 let value = DurationFlex::try_from("5s").unwrap();
367 assert_de_tokens(&value, &[Token::Str("5s")]);
368
369 let value = DurationFlex::try_from("1w8d3h4m5s").unwrap();
370 assert_de_tokens(&value, &[Token::Str("2w1d3h4m5s")]);
371
372 let value = DurationFlex::try_from("1w8d3h4m3605s").unwrap();
373 assert_de_tokens(&value, &[Token::Str("2w1d4h4m5s")]);
374 }
375
376 #[test]
377 fn serialize() {
378 let value = DurationFlex::try_from("1w2d").unwrap();
379 assert_ser_tokens(&value, &[Token::Str("1w2d")]);
380
381 let value = DurationFlex::try_from("1w2d3h4m5s").unwrap();
382 assert_ser_tokens(&value, &[Token::Str("1w2d3h4m5s")]);
383
384 let value = DurationFlex::try_from("5s").unwrap();
385 assert_ser_tokens(&value, &[Token::Str("5s")]);
386
387 let value = DurationFlex::try_from("1w8d3h4m5s").unwrap();
388 assert_ser_tokens(&value, &[Token::Str("2w1d3h4m5s")]);
389
390 let value = DurationFlex::try_from("1w8d3h4m3605s").unwrap();
391 assert_ser_tokens(&value, &[Token::Str("2w1d4h4m5s")]);
392 }
393
394 #[test]
395 fn in_struct() {
396 #[derive(Serialize, Deserialize)]
397 struct SomeStruct {
398 duration: DurationFlex,
399 }
400
401 let value = SomeStruct { duration: Duration::try_weeks(1).unwrap().into() };
402
403 assert_ser_tokens(
404 &value,
405 &[Token::Struct { name: "SomeStruct", len: 1 }, Token::Str("duration"), Token::Str("1w"), Token::StructEnd],
406 );
407 }
408}