1#![allow(clippy::tabs_in_doc_comments)]
2use std::fmt::{Display, Formatter};
89use std::ops::{Add, Sub};
90use std::str::FromStr;
91use std::time;
92
93use chrono::{DateTime, Duration, TimeZone};
94#[cfg(feature = "clap")]
95use clap::builder::OsStr;
96use once_cell::sync::Lazy;
97use regex::{Match, Regex};
98#[cfg(feature = "serde")]
99use serde::de::{Error, Unexpected, Visitor};
100#[cfg(feature = "serde")]
101use serde::{Deserialize, Deserializer, Serialize, Serializer};
102
103const SECS_PER_MINUTES: i64 = 60;
104const SECS_PER_HOUR: i64 = 60 * SECS_PER_MINUTES;
105const SECS_PER_DAY: i64 = 24 * SECS_PER_HOUR;
106const SECS_PER_WEEK: i64 = 7 * SECS_PER_DAY;
107
108#[derive(Copy, Clone, Debug)]
110pub enum DurationFlexError {
111 InvalidFormat,
113
114 OutOfRange,
116}
117
118#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
140pub struct DurationFlex {
141 secs: i64,
142 nanos: i32,
143}
144
145#[cfg(feature = "validator")]
146impl validator::ValidateRange<DurationFlex> for DurationFlex {
147 fn greater_than(&self, max: DurationFlex) -> Option<bool> {
148 Some(self > &max)
149 }
150
151 fn less_than(&self, min: DurationFlex) -> Option<bool> {
152 Some(self < &min)
153 }
154}
155
156#[cfg(feature = "validator")]
157impl validator::ValidateRange<&str> for DurationFlex {
158 fn greater_than(&self, max: &str) -> Option<bool> {
159 let max = DurationFlex::try_from(max).expect("invalid duration string in validator bounds");
160 Some(self > &max)
161 }
162
163 fn less_than(&self, min: &str) -> Option<bool> {
164 let min = DurationFlex::try_from(min).expect("invalid duration string in validator bounds");
165 Some(self < &min)
166 }
167}
168
169#[cfg(feature = "validator")]
170impl validator::ValidateRange<i64> for DurationFlex {
171 fn greater_than(&self, max: i64) -> Option<bool> {
172 Some(self.secs > max)
173 }
174
175 fn less_than(&self, min: i64) -> Option<bool> {
176 Some(self.secs < min)
177 }
178}
179
180static REGEX_STR: &str =
181 r"^((?P<weeks>\d+)w)?((?P<days>\d+)d)?((?P<hours>\d+)h)?((?P<minutes>\d+)m)?((?P<seconds>\d+)s)?$";
182
183static REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(REGEX_STR).unwrap());
184
185impl DurationFlex {
186 pub fn secs(&self) -> i64 {
188 self.secs
189 }
190
191 pub fn nanos(&self) -> i32 {
193 self.nanos
194 }
195
196 fn de_component(r#match: Match) -> i64 {
197 r#match.as_str().parse().unwrap()
198 }
199
200 fn ser_component(secs: &mut i64, component: &str, component_secs: i64, f: &mut Formatter<'_>) -> std::fmt::Result {
201 let value = *secs / component_secs;
202 *secs -= value * component_secs;
203
204 if value == 0 {
205 Ok(())
206 } else {
207 write!(f, "{}{}", value, component)
208 }
209 }
210}
211
212impl Sub<Duration> for DurationFlex {
213 type Output = Duration;
214
215 fn sub(self, rhs: Duration) -> Self::Output {
216 Duration::from(self) - rhs
217 }
218}
219
220impl Add<Duration> for DurationFlex {
221 type Output = Duration;
222
223 fn add(self, rhs: Duration) -> Self::Output {
224 Duration::from(self) + rhs
225 }
226}
227
228impl<T> Add<DateTime<T>> for DurationFlex
229where
230 T: TimeZone,
231{
232 type Output = DateTime<T>;
233
234 fn add(self, rhs: DateTime<T>) -> Self::Output {
235 rhs + Duration::from(self)
236 }
237}
238
239impl<T> Add<DurationFlex> for DateTime<T>
240where
241 T: TimeZone,
242{
243 type Output = DateTime<T>;
244
245 fn add(self, rhs: DurationFlex) -> Self::Output {
246 self + Duration::from(rhs)
247 }
248}
249
250impl TryFrom<&str> for DurationFlex {
251 type Error = DurationFlexError;
252
253 fn try_from(value: &str) -> Result<Self, Self::Error> {
254 let captures = REGEX.captures(value).ok_or(DurationFlexError::InvalidFormat)?;
255
256 let weeks = Duration::try_weeks(captures.name("weeks").map_or(0i64, Self::de_component))
257 .ok_or(DurationFlexError::OutOfRange)?;
258 let days = Duration::try_days(captures.name("days").map_or(0i64, Self::de_component))
259 .ok_or(DurationFlexError::OutOfRange)?;
260 let hours = Duration::try_hours(captures.name("hours").map_or(0i64, Self::de_component))
261 .ok_or(DurationFlexError::OutOfRange)?;
262 let minutes = Duration::try_minutes(captures.name("minutes").map_or(0i64, Self::de_component))
263 .ok_or(DurationFlexError::OutOfRange)?;
264 let seconds = Duration::try_seconds(captures.name("seconds").map_or(0i64, Self::de_component))
265 .ok_or(DurationFlexError::OutOfRange)?;
266
267 let duration = weeks + days + hours + minutes + seconds;
268
269 Ok(DurationFlex { secs: duration.num_seconds(), nanos: 0i32 })
270 }
271}
272
273impl From<String> for DurationFlex {
274 fn from(value: String) -> Self {
275 DurationFlex::try_from(value.as_str()).unwrap()
276 }
277}
278
279impl From<Duration> for DurationFlex {
280 fn from(value: Duration) -> Self {
281 DurationFlex { secs: value.num_seconds(), nanos: 0i32 }
282 }
283}
284
285impl From<DurationFlex> for Duration {
286 fn from(value: DurationFlex) -> Self {
287 Duration::try_seconds(value.secs()).unwrap() + Duration::nanoseconds(value.nanos() as i64)
288 }
289}
290
291impl From<time::Duration> for DurationFlex {
292 fn from(value: time::Duration) -> Self {
293 DurationFlex { secs: value.as_secs() as i64, nanos: 0i32 }
294 }
295}
296
297impl From<DurationFlex> for time::Duration {
298 fn from(value: DurationFlex) -> Self {
299 time::Duration::from_secs(value.secs as u64).add(time::Duration::from_nanos(value.nanos as u64))
300 }
301}
302
303impl Display for DurationFlex {
304 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
305 let mut secs = self.secs;
306
307 Self::ser_component(&mut secs, "w", SECS_PER_WEEK, f)?;
308 Self::ser_component(&mut secs, "d", SECS_PER_DAY, f)?;
309 Self::ser_component(&mut secs, "h", SECS_PER_HOUR, f)?;
310 Self::ser_component(&mut secs, "m", SECS_PER_MINUTES, f)?;
311 Self::ser_component(&mut secs, "s", 1, f)
312 }
313}
314
315#[cfg(feature = "serde")]
316impl<'de> Deserialize<'de> for DurationFlex {
317 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
318 where
319 D: Deserializer<'de>,
320 {
321 static REGEX_MSG: &str =
322 "a String with the format weeks (w), days (d), hours (h), minutes (m) and/or seconds (s), in order";
323
324 struct DurationFlexVisitor;
325
326 impl<'de> Visitor<'de> for DurationFlexVisitor {
327 type Value = DurationFlex;
328
329 fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
330 formatter.write_str(REGEX_MSG)
331 }
332
333 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
334 where
335 E: Error,
336 {
337 match DurationFlex::try_from(v) {
338 Ok(value) => Ok(value),
339 Err(DurationFlexError::InvalidFormat) => Err(Error::invalid_value(Unexpected::Str(v), &self)),
340 Err(DurationFlexError::OutOfRange) => Err(Error::invalid_value(Unexpected::Str(v), &self)),
341 }
342 }
343
344 fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E>
345 where
346 E: Error,
347 {
348 self.visit_str(v)
349 }
350
351 fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
352 where
353 E: Error,
354 {
355 match DurationFlex::try_from(v.as_str()) {
356 Ok(value) => Ok(value),
357 Err(DurationFlexError::InvalidFormat) => {
358 Err(Error::invalid_value(Unexpected::Str(v.as_str()), &self))
359 },
360 Err(DurationFlexError::OutOfRange) => Err(Error::invalid_value(Unexpected::Str(v.as_str()), &self)),
361 }
362 }
363 }
364
365 deserializer.deserialize_string(DurationFlexVisitor)
366 }
367}
368
369#[cfg(feature = "serde")]
370impl Serialize for DurationFlex {
371 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
372 where
373 S: Serializer,
374 {
375 serializer.serialize_str(format!("{}", self).as_str())
376 }
377}
378
379#[cfg(feature = "clap")]
380impl From<OsStr> for DurationFlex {
381 fn from(value: OsStr) -> Self {
382 DurationFlex::try_from(value.to_str().unwrap()).unwrap()
383 }
384}
385
386#[cfg(feature = "clap")]
387impl From<DurationFlex> for OsStr {
388 fn from(value: DurationFlex) -> Self {
389 format!("{}", value).into()
390 }
391}
392
393impl FromStr for DurationFlex {
394 type Err = DurationFlexError;
395
396 fn from_str(s: &str) -> Result<Self, Self::Err> {
397 DurationFlex::try_from(s)
398 }
399}
400
401#[cfg(test)]
402mod test {
403
404 use serde::{Deserialize, Serialize};
405 use serde_test::{assert_de_tokens, assert_ser_tokens, Token};
406
407 use super::*;
408
409 #[test]
410 fn de_string() {
411 let value = DurationFlex::try_from("1w2d").unwrap();
412 assert_eq!(value.secs(), 9 * SECS_PER_DAY);
413 assert_eq!(value.nanos(), 0);
414
415 let value = DurationFlex::try_from("1w2d3h4m5s").unwrap();
416 assert_eq!(value.secs(), 9 * SECS_PER_DAY + 3 * SECS_PER_HOUR + 4 * SECS_PER_MINUTES + 5);
417 assert_eq!(value.nanos(), 0);
418
419 let value = DurationFlex::try_from("5s").unwrap();
420 assert_eq!(value.secs(), 5);
421 assert_eq!(value.nanos(), 0);
422
423 let value = DurationFlex::try_from("5s5d");
424 assert!(value.is_err());
425 }
426
427 #[test]
428 fn ser_string() {
429 let value = DurationFlex::try_from("1w2d").unwrap().to_string();
430 assert_eq!(value, "1w2d");
431
432 let value = DurationFlex::try_from("1w2d3h4m5s").unwrap().to_string();
433 assert_eq!(value, "1w2d3h4m5s");
434
435 let value = DurationFlex::try_from("5s").unwrap().to_string();
436 assert_eq!(value, "5s");
437
438 let value = DurationFlex::try_from("1w8d3h4m5s").unwrap().to_string();
439 assert_eq!(value, "2w1d3h4m5s");
440
441 let value = DurationFlex::try_from("1w8d3h4m3605s").unwrap().to_string();
442 assert_eq!(value, "2w1d4h4m5s");
443 }
444
445 #[test]
446 fn deserialize_nums() {
447 let value = DurationFlex::try_from("1w2d").unwrap();
448 assert_de_tokens(&value, &[Token::Str("1w2d")]);
449
450 let value = DurationFlex::try_from("1w2d3h4m5s").unwrap();
451 assert_de_tokens(&value, &[Token::Str("1w2d3h4m5s")]);
452
453 let value = DurationFlex::try_from("5s").unwrap();
454 assert_de_tokens(&value, &[Token::Str("5s")]);
455
456 let value = DurationFlex::try_from("1w8d3h4m5s").unwrap();
457 assert_de_tokens(&value, &[Token::Str("2w1d3h4m5s")]);
458
459 let value = DurationFlex::try_from("1w8d3h4m3605s").unwrap();
460 assert_de_tokens(&value, &[Token::Str("2w1d4h4m5s")]);
461 }
462
463 #[test]
464 fn serialize() {
465 let value = DurationFlex::try_from("1w2d").unwrap();
466 assert_ser_tokens(&value, &[Token::Str("1w2d")]);
467
468 let value = DurationFlex::try_from("1w2d3h4m5s").unwrap();
469 assert_ser_tokens(&value, &[Token::Str("1w2d3h4m5s")]);
470
471 let value = DurationFlex::try_from("5s").unwrap();
472 assert_ser_tokens(&value, &[Token::Str("5s")]);
473
474 let value = DurationFlex::try_from("1w8d3h4m5s").unwrap();
475 assert_ser_tokens(&value, &[Token::Str("2w1d3h4m5s")]);
476
477 let value = DurationFlex::try_from("1w8d3h4m3605s").unwrap();
478 assert_ser_tokens(&value, &[Token::Str("2w1d4h4m5s")]);
479 }
480
481 #[test]
482 fn in_struct() {
483 #[derive(Serialize, Deserialize)]
484 struct SomeStruct {
485 duration: DurationFlex,
486 }
487
488 let value = SomeStruct { duration: Duration::try_weeks(1).unwrap().into() };
489
490 assert_ser_tokens(
491 &value,
492 &[Token::Struct { name: "SomeStruct", len: 1 }, Token::Str("duration"), Token::Str("1w"), Token::StructEnd],
493 );
494 }
495
496 #[cfg(feature = "validator")]
497 #[test]
498 fn validator() {
499 use validator::Validate;
500
501 #[derive(Validate)]
502 struct SomeStruct {
503 #[validate(range(
504 min = "DurationFlex::try_from(\"1h\").unwrap()",
505 max = "DurationFlex::try_from(\"2h\").unwrap()"
506 ))]
507 duration: DurationFlex,
508 }
509
510 let value = SomeStruct { duration: DurationFlex::try_from("1h30m").unwrap() };
511 assert!(value.validate().is_ok());
512
513 let value = SomeStruct { duration: DurationFlex::try_from("30m").unwrap() };
514 assert!(value.validate().is_err());
515
516 let value = SomeStruct { duration: DurationFlex::try_from("2h30m").unwrap() };
517 assert!(value.validate().is_err());
518 }
519
520 #[cfg(feature = "validator")]
521 #[test]
522 fn validator_str() {
523 use validator::Validate;
524
525 #[derive(Validate)]
526 struct SomeStruct {
527 #[validate(range(min = "\"1h\"", max = "\"2h\""))]
528 duration: DurationFlex,
529 }
530
531 let value = SomeStruct { duration: DurationFlex::try_from("1h30m").unwrap() };
532 assert!(value.validate().is_ok());
533
534 let value = SomeStruct { duration: DurationFlex::try_from("30m").unwrap() };
535 assert!(value.validate().is_err());
536
537 let value = SomeStruct { duration: DurationFlex::try_from("2h30m").unwrap() };
538 assert!(value.validate().is_err());
539 }
540
541 #[cfg(feature = "validator")]
542 #[test]
543 fn validator_int() {
544 use validator::Validate;
545
546 #[derive(Validate)]
547 struct SomeStruct {
548 #[validate(range(min = 3600, max = 7200))]
549 duration: DurationFlex,
550 }
551
552 let value = SomeStruct { duration: DurationFlex::try_from("1h30m").unwrap() };
553 assert!(value.validate().is_ok());
554
555 let value = SomeStruct { duration: DurationFlex::try_from("30m").unwrap() };
556 assert!(value.validate().is_err());
557
558 let value = SomeStruct { duration: DurationFlex::try_from("2h30m").unwrap() };
559 assert!(value.validate().is_err());
560 }
561}