1#![allow(clippy::tabs_in_doc_comments)]
2use std::fmt::{Display, Formatter};
95use std::hash::Hash;
96use std::iter::Sum;
97use std::ops::{Add, Div, Mul, Sub};
98use std::str::FromStr;
99use std::time;
100
101use chrono::{DateTime, Duration, TimeZone};
102#[cfg(feature = "clap")]
103use clap::builder::OsStr;
104use once_cell::sync::Lazy;
105use regex::{Match, Regex};
106#[cfg(feature = "serde")]
107use serde::de::{Error, Unexpected, Visitor};
108#[cfg(feature = "serde")]
109use serde::{Deserialize, Deserializer, Serialize, Serializer};
110
111const SECS_PER_MINUTES: i64 = 60;
112const SECS_PER_HOUR: i64 = 60 * SECS_PER_MINUTES;
113const SECS_PER_DAY: i64 = 24 * SECS_PER_HOUR;
114const SECS_PER_WEEK: i64 = 7 * SECS_PER_DAY;
115const SECS_PER_YEAR: i64 = 365 * SECS_PER_DAY;
116
117#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
119pub enum DurationFlexError {
120 InvalidFormat,
122
123 OutOfRange,
125}
126
127impl Display for DurationFlexError {
128 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
129 match self {
130 DurationFlexError::InvalidFormat => write!(f, "invalid duration format"),
131 DurationFlexError::OutOfRange => write!(f, "duration value is out of range"),
132 }
133 }
134}
135
136impl std::error::Error for DurationFlexError {}
137
138#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Hash, PartialOrd, Ord)]
160#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
161#[cfg_attr(feature = "utoipa", schema(as = String, example = "1h30m"))]
162pub struct DurationFlex {
163 secs: i64,
164 nanos: i32,
165}
166
167#[cfg(feature = "validator")]
168impl validator::ValidateRange<DurationFlex> for DurationFlex {
169 fn greater_than(&self, max: DurationFlex) -> Option<bool> {
170 Some(self > &max)
171 }
172
173 fn less_than(&self, min: DurationFlex) -> Option<bool> {
174 Some(self < &min)
175 }
176}
177
178#[cfg(feature = "validator")]
179impl validator::ValidateRange<&str> for DurationFlex {
180 fn greater_than(&self, max: &str) -> Option<bool> {
181 let max = DurationFlex::try_from(max).expect("invalid duration string in validator bounds");
182 Some(self > &max)
183 }
184
185 fn less_than(&self, min: &str) -> Option<bool> {
186 let min = DurationFlex::try_from(min).expect("invalid duration string in validator bounds");
187 Some(self < &min)
188 }
189}
190
191#[cfg(feature = "validator")]
192impl validator::ValidateRange<i64> for DurationFlex {
193 fn greater_than(&self, max: i64) -> Option<bool> {
194 Some(self.secs > max)
195 }
196
197 fn less_than(&self, min: i64) -> Option<bool> {
198 Some(self.secs < min)
199 }
200}
201
202static REGEX_STR: &str = r"^((?P<years>\d+)y)?((?P<weeks>\d+)w)?((?P<days>\d+)d)?((?P<hours>\d+)h)?((?P<minutes>\d+)m)?((?P<seconds>\d+)s)?$";
203
204static REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(REGEX_STR).unwrap());
205
206impl DurationFlex {
207 pub const ZERO: DurationFlex = DurationFlex { secs: 0, nanos: 0 };
209
210 pub const fn new(secs: i64, nanos: i32) -> Self {
212 DurationFlex { secs, nanos }
213 }
214
215 pub const fn from_secs(secs: i64) -> Self {
217 DurationFlex { secs, nanos: 0 }
218 }
219
220 pub const fn from_millis(millis: i64) -> Self {
222 let secs = millis / 1000;
223 let extra_millis = millis % 1000;
224 DurationFlex { secs, nanos: (extra_millis * 1_000_000) as i32 }
225 }
226
227 pub const fn from_minutes(minutes: i64) -> Self {
229 DurationFlex { secs: minutes * SECS_PER_MINUTES, nanos: 0 }
230 }
231
232 pub const fn from_hours(hours: i64) -> Self {
234 DurationFlex { secs: hours * SECS_PER_HOUR, nanos: 0 }
235 }
236
237 pub const fn from_days(days: i64) -> Self {
239 DurationFlex { secs: days * SECS_PER_DAY, nanos: 0 }
240 }
241
242 pub const fn from_weeks(weeks: i64) -> Self {
244 DurationFlex { secs: weeks * SECS_PER_WEEK, nanos: 0 }
245 }
246
247 pub const fn from_years(years: i64) -> Self {
249 DurationFlex { secs: years * SECS_PER_YEAR, nanos: 0 }
250 }
251
252 pub const fn is_zero(&self) -> bool {
254 self.secs == 0 && self.nanos == 0
255 }
256
257 pub const fn is_positive(&self) -> bool {
259 self.secs > 0 || (self.secs == 0 && self.nanos > 0)
260 }
261
262 pub const fn is_negative(&self) -> bool {
264 self.secs < 0 || (self.secs == 0 && self.nanos < 0)
265 }
266
267 pub fn to_std(&self) -> Option<time::Duration> {
269 if self.secs < 0 || (self.secs == 0 && self.nanos < 0) {
270 None
271 } else {
272 Some(time::Duration::new(self.secs as u64, self.nanos as u32))
273 }
274 }
275
276 pub fn to_chrono(&self) -> Duration {
278 Duration::from(*self)
279 }
280
281 pub fn secs(&self) -> i64 {
283 self.secs
284 }
285
286 pub fn nanos(&self) -> i32 {
288 self.nanos
289 }
290
291 fn de_component(r#match: Match) -> i64 {
292 r#match.as_str().parse().unwrap()
293 }
294
295 fn ser_component(secs: &mut i64, component: &str, component_secs: i64, f: &mut Formatter<'_>) -> std::fmt::Result {
296 let value = *secs / component_secs;
297 *secs -= value * component_secs;
298
299 if value == 0 {
300 Ok(())
301 } else {
302 write!(f, "{}{}", value, component)
303 }
304 }
305}
306
307impl Sub<Duration> for DurationFlex {
308 type Output = Duration;
309
310 fn sub(self, rhs: Duration) -> Self::Output {
311 Duration::from(self) - rhs
312 }
313}
314
315impl Add<Duration> for DurationFlex {
316 type Output = Duration;
317
318 fn add(self, rhs: Duration) -> Self::Output {
319 Duration::from(self) + rhs
320 }
321}
322
323impl Sub<DurationFlex> for DurationFlex {
324 type Output = DurationFlex;
325
326 fn sub(self, rhs: DurationFlex) -> Self::Output {
327 DurationFlex { secs: self.secs - rhs.secs, nanos: self.nanos - rhs.nanos }
328 }
329}
330
331impl Add<DurationFlex> for DurationFlex {
332 type Output = DurationFlex;
333
334 fn add(self, rhs: DurationFlex) -> Self::Output {
335 DurationFlex { secs: self.secs + rhs.secs, nanos: self.nanos + rhs.nanos }
336 }
337}
338
339impl Mul<u32> for DurationFlex {
340 type Output = DurationFlex;
341
342 fn mul(self, rhs: u32) -> Self::Output {
343 DurationFlex { secs: self.secs * rhs as i64, nanos: self.nanos * rhs as i32 }
344 }
345}
346
347impl Mul<i64> for DurationFlex {
348 type Output = DurationFlex;
349
350 fn mul(self, rhs: i64) -> Self::Output {
351 DurationFlex { secs: self.secs * rhs, nanos: (self.nanos as i64 * rhs) as i32 }
352 }
353}
354
355impl Div<u32> for DurationFlex {
356 type Output = DurationFlex;
357
358 fn div(self, rhs: u32) -> Self::Output {
359 DurationFlex { secs: self.secs / rhs as i64, nanos: self.nanos / rhs as i32 }
360 }
361}
362
363impl Div<i64> for DurationFlex {
364 type Output = DurationFlex;
365
366 fn div(self, rhs: i64) -> Self::Output {
367 DurationFlex { secs: self.secs / rhs, nanos: (self.nanos as i64 / rhs) as i32 }
368 }
369}
370
371impl Sum for DurationFlex {
372 fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
373 iter.fold(DurationFlex::default(), |acc, x| acc + x)
374 }
375}
376
377impl<'a> Sum<&'a DurationFlex> for DurationFlex {
378 fn sum<I: Iterator<Item = &'a Self>>(iter: I) -> Self {
379 iter.fold(DurationFlex::default(), |acc, x| acc + *x)
380 }
381}
382
383impl<T> Sub<DurationFlex> for DateTime<T>
384where
385 T: TimeZone,
386{
387 type Output = DateTime<T>;
388
389 fn sub(self, rhs: DurationFlex) -> Self::Output {
390 self - Duration::from(rhs)
391 }
392}
393
394impl<T> Add<DateTime<T>> for DurationFlex
395where
396 T: TimeZone,
397{
398 type Output = DateTime<T>;
399
400 fn add(self, rhs: DateTime<T>) -> Self::Output {
401 rhs + Duration::from(self)
402 }
403}
404
405impl<T> Add<DurationFlex> for DateTime<T>
406where
407 T: TimeZone,
408{
409 type Output = DateTime<T>;
410
411 fn add(self, rhs: DurationFlex) -> Self::Output {
412 self + Duration::from(rhs)
413 }
414}
415
416impl TryFrom<&str> for DurationFlex {
417 type Error = DurationFlexError;
418
419 fn try_from(value: &str) -> Result<Self, Self::Error> {
420 let captures = REGEX.captures(value).ok_or(DurationFlexError::InvalidFormat)?;
421
422 let years = Duration::try_days(captures.name("years").map_or(0i64, Self::de_component) * 365)
423 .ok_or(DurationFlexError::OutOfRange)?;
424 let weeks = Duration::try_weeks(captures.name("weeks").map_or(0i64, Self::de_component))
425 .ok_or(DurationFlexError::OutOfRange)?;
426 let days = Duration::try_days(captures.name("days").map_or(0i64, Self::de_component))
427 .ok_or(DurationFlexError::OutOfRange)?;
428 let hours = Duration::try_hours(captures.name("hours").map_or(0i64, Self::de_component))
429 .ok_or(DurationFlexError::OutOfRange)?;
430 let minutes = Duration::try_minutes(captures.name("minutes").map_or(0i64, Self::de_component))
431 .ok_or(DurationFlexError::OutOfRange)?;
432 let seconds = Duration::try_seconds(captures.name("seconds").map_or(0i64, Self::de_component))
433 .ok_or(DurationFlexError::OutOfRange)?;
434
435 let duration = years + weeks + days + hours + minutes + seconds;
436
437 Ok(DurationFlex { secs: duration.num_seconds(), nanos: 0i32 })
438 }
439}
440
441impl From<String> for DurationFlex {
442 fn from(value: String) -> Self {
443 DurationFlex::try_from(value.as_str()).unwrap()
444 }
445}
446
447impl From<Duration> for DurationFlex {
448 fn from(value: Duration) -> Self {
449 DurationFlex { secs: value.num_seconds(), nanos: 0i32 }
450 }
451}
452
453impl From<DurationFlex> for Duration {
454 fn from(value: DurationFlex) -> Self {
455 Duration::try_seconds(value.secs()).unwrap() + Duration::nanoseconds(value.nanos() as i64)
456 }
457}
458
459impl From<time::Duration> for DurationFlex {
460 fn from(value: time::Duration) -> Self {
461 DurationFlex { secs: value.as_secs() as i64, nanos: 0i32 }
462 }
463}
464
465impl From<DurationFlex> for time::Duration {
466 fn from(value: DurationFlex) -> Self {
467 time::Duration::from_secs(value.secs as u64).add(time::Duration::from_nanos(value.nanos as u64))
468 }
469}
470
471impl Display for DurationFlex {
472 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
473 let mut secs = self.secs;
474
475 Self::ser_component(&mut secs, "y", SECS_PER_YEAR, f)?;
476 Self::ser_component(&mut secs, "w", SECS_PER_WEEK, f)?;
477 Self::ser_component(&mut secs, "d", SECS_PER_DAY, f)?;
478 Self::ser_component(&mut secs, "h", SECS_PER_HOUR, f)?;
479 Self::ser_component(&mut secs, "m", SECS_PER_MINUTES, f)?;
480 Self::ser_component(&mut secs, "s", 1, f)
481 }
482}
483
484#[cfg(feature = "serde")]
485impl<'de> Deserialize<'de> for DurationFlex {
486 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
487 where
488 D: Deserializer<'de>,
489 {
490 static REGEX_MSG: &str = "a String with the format years (y), weeks (w), days (d), hours (h), minutes (m) \
491 and/or seconds (s), in order";
492
493 struct DurationFlexVisitor;
494
495 impl<'de> Visitor<'de> for DurationFlexVisitor {
496 type Value = DurationFlex;
497
498 fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
499 formatter.write_str(REGEX_MSG)
500 }
501
502 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
503 where
504 E: Error,
505 {
506 match DurationFlex::try_from(v) {
507 Ok(value) => Ok(value),
508 Err(DurationFlexError::InvalidFormat) => Err(Error::invalid_value(Unexpected::Str(v), &self)),
509 Err(DurationFlexError::OutOfRange) => Err(Error::invalid_value(Unexpected::Str(v), &self)),
510 }
511 }
512
513 fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E>
514 where
515 E: Error,
516 {
517 self.visit_str(v)
518 }
519
520 fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
521 where
522 E: Error,
523 {
524 match DurationFlex::try_from(v.as_str()) {
525 Ok(value) => Ok(value),
526 Err(DurationFlexError::InvalidFormat) => {
527 Err(Error::invalid_value(Unexpected::Str(v.as_str()), &self))
528 },
529 Err(DurationFlexError::OutOfRange) => Err(Error::invalid_value(Unexpected::Str(v.as_str()), &self)),
530 }
531 }
532 }
533
534 deserializer.deserialize_string(DurationFlexVisitor)
535 }
536}
537
538#[cfg(feature = "serde")]
539impl Serialize for DurationFlex {
540 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
541 where
542 S: Serializer,
543 {
544 serializer.serialize_str(format!("{}", self).as_str())
545 }
546}
547
548#[cfg(feature = "clap")]
549impl From<OsStr> for DurationFlex {
550 fn from(value: OsStr) -> Self {
551 DurationFlex::try_from(value.to_str().unwrap()).unwrap()
552 }
553}
554
555#[cfg(feature = "clap")]
556impl From<DurationFlex> for OsStr {
557 fn from(value: DurationFlex) -> Self {
558 format!("{}", value).into()
559 }
560}
561
562impl FromStr for DurationFlex {
563 type Err = DurationFlexError;
564
565 fn from_str(s: &str) -> Result<Self, Self::Err> {
566 DurationFlex::try_from(s)
567 }
568}
569
570#[cfg(test)]
571mod test {
572
573 use serde::{Deserialize, Serialize};
574 use serde_test::{assert_de_tokens, assert_ser_tokens, Token};
575
576 use super::*;
577
578 #[test]
579 fn de_string() {
580 let value = DurationFlex::try_from("1y").unwrap();
581 assert_eq!(value.secs(), SECS_PER_YEAR);
582 assert_eq!(value.nanos(), 0);
583
584 let value = DurationFlex::try_from("1y2w3d4h5m6s").unwrap();
585 assert_eq!(
586 value.secs(),
587 SECS_PER_YEAR + 2 * SECS_PER_WEEK + 3 * SECS_PER_DAY + 4 * SECS_PER_HOUR + 5 * SECS_PER_MINUTES + 6
588 );
589 assert_eq!(value.nanos(), 0);
590
591 let value = DurationFlex::try_from("1w2d").unwrap();
592 assert_eq!(value.secs(), 9 * SECS_PER_DAY);
593 assert_eq!(value.nanos(), 0);
594
595 let value = DurationFlex::try_from("1w2d3h4m5s").unwrap();
596 assert_eq!(value.secs(), 9 * SECS_PER_DAY + 3 * SECS_PER_HOUR + 4 * SECS_PER_MINUTES + 5);
597 assert_eq!(value.nanos(), 0);
598
599 let value = DurationFlex::try_from("5s").unwrap();
600 assert_eq!(value.secs(), 5);
601 assert_eq!(value.nanos(), 0);
602
603 let value = DurationFlex::try_from("5s5d");
604 assert!(value.is_err());
605
606 let value = DurationFlex::try_from("1w1y");
607 assert!(value.is_err());
608 }
609
610 #[test]
611 fn ser_string() {
612 let value = DurationFlex::try_from("1y").unwrap().to_string();
613 assert_eq!(value, "1y");
614
615 let value = DurationFlex::try_from("1y2w3d4h5m6s").unwrap().to_string();
616 assert_eq!(value, "1y2w3d4h5m6s");
617
618 let value = DurationFlex::try_from("365d").unwrap().to_string();
619 assert_eq!(value, "1y");
620
621 let value = DurationFlex::try_from("372d").unwrap().to_string();
622 assert_eq!(value, "1y1w");
623
624 let value = DurationFlex::try_from("53w").unwrap().to_string();
625 assert_eq!(value, "1y6d");
626
627 let value = DurationFlex::try_from("1w2d").unwrap().to_string();
628 assert_eq!(value, "1w2d");
629
630 let value = DurationFlex::try_from("1w2d3h4m5s").unwrap().to_string();
631 assert_eq!(value, "1w2d3h4m5s");
632
633 let value = DurationFlex::try_from("5s").unwrap().to_string();
634 assert_eq!(value, "5s");
635
636 let value = DurationFlex::try_from("1w8d3h4m5s").unwrap().to_string();
637 assert_eq!(value, "2w1d3h4m5s");
638
639 let value = DurationFlex::try_from("1w8d3h4m3605s").unwrap().to_string();
640 assert_eq!(value, "2w1d4h4m5s");
641 }
642
643 #[test]
644 fn deserialize_nums() {
645 let value = DurationFlex::try_from("1y").unwrap();
646 assert_de_tokens(&value, &[Token::Str("1y")]);
647
648 let value = DurationFlex::try_from("1y2w3d4h5m6s").unwrap();
649 assert_de_tokens(&value, &[Token::Str("1y2w3d4h5m6s")]);
650
651 let value = DurationFlex::try_from("1w2d").unwrap();
652 assert_de_tokens(&value, &[Token::Str("1w2d")]);
653
654 let value = DurationFlex::try_from("1w2d3h4m5s").unwrap();
655 assert_de_tokens(&value, &[Token::Str("1w2d3h4m5s")]);
656
657 let value = DurationFlex::try_from("5s").unwrap();
658 assert_de_tokens(&value, &[Token::Str("5s")]);
659
660 let value = DurationFlex::try_from("1w8d3h4m5s").unwrap();
661 assert_de_tokens(&value, &[Token::Str("2w1d3h4m5s")]);
662
663 let value = DurationFlex::try_from("1w8d3h4m3605s").unwrap();
664 assert_de_tokens(&value, &[Token::Str("2w1d4h4m5s")]);
665 }
666
667 #[test]
668 fn serialize() {
669 let value = DurationFlex::try_from("1y").unwrap();
670 assert_ser_tokens(&value, &[Token::Str("1y")]);
671
672 let value = DurationFlex::try_from("1y2w3d4h5m6s").unwrap();
673 assert_ser_tokens(&value, &[Token::Str("1y2w3d4h5m6s")]);
674
675 let value = DurationFlex::try_from("1w2d").unwrap();
676 assert_ser_tokens(&value, &[Token::Str("1w2d")]);
677
678 let value = DurationFlex::try_from("1w2d3h4m5s").unwrap();
679 assert_ser_tokens(&value, &[Token::Str("1w2d3h4m5s")]);
680
681 let value = DurationFlex::try_from("5s").unwrap();
682 assert_ser_tokens(&value, &[Token::Str("5s")]);
683
684 let value = DurationFlex::try_from("1w8d3h4m5s").unwrap();
685 assert_ser_tokens(&value, &[Token::Str("2w1d3h4m5s")]);
686
687 let value = DurationFlex::try_from("1w8d3h4m3605s").unwrap();
688 assert_ser_tokens(&value, &[Token::Str("2w1d4h4m5s")]);
689 }
690
691 #[test]
692 fn in_struct() {
693 #[derive(Serialize, Deserialize)]
694 struct SomeStruct {
695 duration: DurationFlex,
696 }
697
698 let value = SomeStruct { duration: Duration::try_weeks(1).unwrap().into() };
699
700 assert_ser_tokens(
701 &value,
702 &[Token::Struct { name: "SomeStruct", len: 1 }, Token::Str("duration"), Token::Str("1w"), Token::StructEnd],
703 );
704
705 let value_year = SomeStruct { duration: DurationFlex::try_from("1y").unwrap() };
706
707 assert_ser_tokens(
708 &value_year,
709 &[Token::Struct { name: "SomeStruct", len: 1 }, Token::Str("duration"), Token::Str("1y"), Token::StructEnd],
710 );
711 }
712
713 #[cfg(feature = "validator")]
714 #[test]
715 fn validator() {
716 use validator::Validate;
717
718 #[derive(Validate)]
719 struct SomeStruct {
720 #[validate(range(
721 min = "DurationFlex::try_from(\"1h\").unwrap()",
722 max = "DurationFlex::try_from(\"2h\").unwrap()"
723 ))]
724 duration: DurationFlex,
725 }
726
727 let value = SomeStruct { duration: DurationFlex::try_from("1h30m").unwrap() };
728 assert!(value.validate().is_ok());
729
730 let value = SomeStruct { duration: DurationFlex::try_from("30m").unwrap() };
731 assert!(value.validate().is_err());
732
733 let value = SomeStruct { duration: DurationFlex::try_from("2h30m").unwrap() };
734 assert!(value.validate().is_err());
735
736 #[derive(Validate)]
737 struct YearStruct {
738 #[validate(range(
739 min = "DurationFlex::try_from(\"1y\").unwrap()",
740 max = "DurationFlex::try_from(\"2y\").unwrap()"
741 ))]
742 duration: DurationFlex,
743 }
744
745 let value = YearStruct { duration: DurationFlex::try_from("1y6w").unwrap() };
746 assert!(value.validate().is_ok());
747
748 let value = YearStruct { duration: DurationFlex::try_from("300d").unwrap() };
749 assert!(value.validate().is_err());
750
751 let value = YearStruct { duration: DurationFlex::try_from("2y1d").unwrap() };
752 assert!(value.validate().is_err());
753 }
754
755 #[cfg(feature = "validator")]
756 #[test]
757 fn validator_str() {
758 use validator::Validate;
759
760 #[derive(Validate)]
761 struct SomeStruct {
762 #[validate(range(min = "\"1h\"", max = "\"2h\""))]
763 duration: DurationFlex,
764 }
765
766 let value = SomeStruct { duration: DurationFlex::try_from("1h30m").unwrap() };
767 assert!(value.validate().is_ok());
768
769 let value = SomeStruct { duration: DurationFlex::try_from("30m").unwrap() };
770 assert!(value.validate().is_err());
771
772 let value = SomeStruct { duration: DurationFlex::try_from("2h30m").unwrap() };
773 assert!(value.validate().is_err());
774
775 #[derive(Validate)]
776 struct YearStruct {
777 #[validate(range(min = "\"1y\"", max = "\"2y\""))]
778 duration: DurationFlex,
779 }
780
781 let value = YearStruct { duration: DurationFlex::try_from("1y6w").unwrap() };
782 assert!(value.validate().is_ok());
783
784 let value = YearStruct { duration: DurationFlex::try_from("300d").unwrap() };
785 assert!(value.validate().is_err());
786
787 let value = YearStruct { duration: DurationFlex::try_from("2y1d").unwrap() };
788 assert!(value.validate().is_err());
789 }
790
791 #[cfg(feature = "validator")]
792 #[test]
793 fn validator_int() {
794 use validator::Validate;
795
796 #[derive(Validate)]
797 struct SomeStruct {
798 #[validate(range(min = 3600, max = 7200))]
799 duration: DurationFlex,
800 }
801
802 let value = SomeStruct { duration: DurationFlex::try_from("1h30m").unwrap() };
803 assert!(value.validate().is_ok());
804
805 let value = SomeStruct { duration: DurationFlex::try_from("30m").unwrap() };
806 assert!(value.validate().is_err());
807
808 let value = SomeStruct { duration: DurationFlex::try_from("2h30m").unwrap() };
809 assert!(value.validate().is_err());
810 }
811
812 #[test]
813 fn default_and_hash() {
814 use std::collections::HashSet;
815
816 let default_val = DurationFlex::default();
817 assert_eq!(default_val.secs(), 0);
818 assert_eq!(default_val.nanos(), 0);
819
820 let mut set = HashSet::new();
821 set.insert(DurationFlex::try_from("1h").unwrap());
822 set.insert(DurationFlex::try_from("60m").unwrap());
823 assert_eq!(set.len(), 1);
824
825 let mut err_set = HashSet::new();
826 err_set.insert(DurationFlexError::InvalidFormat);
827 err_set.insert(DurationFlexError::OutOfRange);
828 assert_eq!(err_set.len(), 2);
829 }
830
831 #[test]
832 fn error_traits() {
833 use std::error::Error;
834
835 let err = DurationFlexError::InvalidFormat;
836 assert_eq!(err.to_string(), "invalid duration format");
837 let err_source: &dyn Error = &err;
838 assert!(err_source.source().is_none());
839
840 let err_oor = DurationFlexError::OutOfRange;
841 assert_eq!(err_oor.to_string(), "duration value is out of range");
842 }
843
844 #[test]
845 fn arithmetic_operations() {
846 let a = DurationFlex::try_from("1h").unwrap();
847 let b = DurationFlex::try_from("30m").unwrap();
848
849 assert_eq!((a + b).secs(), 90 * 60);
850 assert_eq!((a - b).secs(), 30 * 60);
851 assert_eq!((b * 2u32).secs(), 3600);
852 assert_eq!((b * 3i64).secs(), 5400);
853 assert_eq!((a / 2u32).secs(), 1800);
854 assert_eq!((a / 4i64).secs(), 900);
855
856 let list = vec![
857 DurationFlex::try_from("1h").unwrap(),
858 DurationFlex::try_from("30m").unwrap(),
859 DurationFlex::try_from("15m").unwrap(),
860 ];
861 let total: DurationFlex = list.iter().sum();
862 assert_eq!(total.secs(), 105 * 60);
863
864 let total_owned: DurationFlex = list.into_iter().sum();
865 assert_eq!(total_owned.secs(), 105 * 60);
866 }
867
868 #[test]
869 fn datetime_subtraction() {
870 use chrono::Utc;
871
872 let now = Utc::now();
873 let duration = DurationFlex::try_from("1h").unwrap();
874 let earlier = now - duration;
875 assert_eq!(earlier + duration, now);
876 }
877
878 #[test]
879 fn constructors_and_inspectors() {
880 assert_eq!(DurationFlex::ZERO, DurationFlex::new(0, 0));
881 assert!(DurationFlex::ZERO.is_zero());
882 assert!(!DurationFlex::ZERO.is_positive());
883 assert!(!DurationFlex::ZERO.is_negative());
884
885 let s = DurationFlex::from_secs(10);
886 assert_eq!(s.secs(), 10);
887 assert_eq!(s.nanos(), 0);
888 assert!(s.is_positive());
889 assert!(!s.is_negative());
890
891 let ms = DurationFlex::from_millis(1500);
892 assert_eq!(ms.secs(), 1);
893 assert_eq!(ms.nanos(), 500_000_000);
894
895 let m = DurationFlex::from_minutes(2);
896 assert_eq!(m.secs(), 120);
897
898 let h = DurationFlex::from_hours(3);
899 assert_eq!(h.secs(), 3 * 3600);
900
901 let d = DurationFlex::from_days(4);
902 assert_eq!(d.secs(), 4 * 86400);
903
904 let w = DurationFlex::from_weeks(2);
905 assert_eq!(w.secs(), 14 * 86400);
906
907 let y = DurationFlex::from_years(1);
908 assert_eq!(y.secs(), 365 * 86400);
909
910 let neg = DurationFlex::new(-5, 0);
911 assert!(neg.is_negative());
912 assert!(!neg.is_positive());
913 assert_eq!(neg.to_std(), None);
914
915 let pos = DurationFlex::from_secs(5);
916 assert_eq!(pos.to_std(), Some(time::Duration::from_secs(5)));
917 assert_eq!(pos.to_chrono(), Duration::try_seconds(5).unwrap());
918 }
919}