Skip to main content

duration_flex/
lib.rs

1#![allow(clippy::tabs_in_doc_comments)]
2//! # Duration Flex
3//!
4//! Helper to make it easier to specify durations. Specially useful in configuration files.
5//! - Basic interoperability with [`chrono::DateTime`], allowing it to be added/subbed from it.
6//! - Can be built from [`chrono::Duration`].
7//! - Can be built from [`std::time::Duration`].
8//!
9//! **Example:**
10//! - 1 hour and 23 minutes: `1h23m`
11//! - 1 week, 6 days, 23 hours, 49 minutes and 50 seconds: `1w6d23h49m59s`
12//! - 1 year, 2 weeks and 3 days: `1y2w3d`
13//!
14//! **Supported Time Units**
15//! - Years: `1y` (1 year, equivalent to 365 days).
16//! - Weeks: `2w` (2 weeks).
17//! - Days: `3d` (3 days).
18//! - Hours: `15h` (15 hours).
19//! - Minutes: `5m` (5 minutes).
20//! - Seconds: `30s` (30 seconds).
21//!
22//! > **Note:** Months are not supported because they vary in the amount of days (28 to 31 days).
23//! > It is best to specify the desired duration in days instead (e.g. `30d`).
24//!
25//! ## Usage
26//!
27//! Simply call one of the `from` methods to create an instance:
28//! ```
29//! use duration_flex::DurationFlex;
30//!
31//! # pub fn main() {
32//! let df = DurationFlex::try_from("1w6d23h49m59s").unwrap();
33//! println!("{df}");
34//! # }
35//! ```
36//!
37//! ## Features
38//! - `clap`: enable clap support, so it can be used as application arguments.
39//! - `serde`: enable serde support.
40//! - `utoipa`: enable support for the [`utoipa`] crate, allowing it to be used with the `ToSchema` derivation.
41//! - `validator`: enable support for the [`validator`] crate, allowing it to be used with the `range` validator.
42//!
43//! ### Validator Example:
44//!
45//! You can specify the range using the fully qualified type (extended version):
46//! ```
47//! # #[cfg(feature = "validator")]
48//! # {
49//! use duration_flex::DurationFlex;
50//! use validator::Validate;
51//!
52//! #[derive(Validate)]
53//! struct Config {
54//! 	#[validate(range(
55//! 		min = "DurationFlex::try_from(\"1h\").unwrap()",
56//! 		max = "DurationFlex::try_from(\"2h\").unwrap()"
57//! 	))]
58//! 	timeout: DurationFlex,
59//! }
60//! # }
61//! ```
62//!
63//! Or using string literals (string version). Note the escaped inner quotes, which are required
64//! because the macro parses the arguments as Rust expressions:
65//! ```
66//! # #[cfg(feature = "validator")]
67//! # {
68//! use duration_flex::DurationFlex;
69//! use validator::Validate;
70//!
71//! #[derive(Validate)]
72//! struct Config {
73//! 	#[validate(range(min = "\"1h\"", max = "\"2h\""))]
74//! 	timeout: DurationFlex,
75//! }
76//! # }
77//! ```
78//!
79//! Or using numbers (number version), which represent the amount of seconds:
80//! ```
81//! # #[cfg(feature = "validator")]
82//! # {
83//! use duration_flex::DurationFlex;
84//! use validator::Validate;
85//!
86//! #[derive(Validate)]
87//! struct Config {
88//! 	#[validate(range(min = 3600, max = 7200))]
89//! 	timeout: DurationFlex,
90//! }
91//! # }
92//! ```
93
94use 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/// Errors returned by the different methods.
118#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
119pub enum DurationFlexError {
120	/// String format is not valid, e.g. `1x` (`x` is not supported).
121	InvalidFormat,
122
123	/// Value is out of range.
124	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/// Type to conveniently specify durations and interoperate with [`chrono::Duration`].
139///
140/// The correct way of building this, is through one of the `from` methods.
141///
142/// With the `clap` feature, can be used with [`clap`]:
143/// ```
144/// use clap::Args;
145/// use duration_flex::DurationFlex;
146///
147/// #[derive(Args)]
148/// pub struct Arguments {
149/// 	#[arg(long, default_value_t = Arguments::default().duration)]
150/// 	duration: DurationFlex,
151/// }
152///
153/// impl Default for Arguments {
154/// 	fn default() -> Self {
155/// 		Self { duration: DurationFlex::try_from("1w6d23h49m59s").unwrap() }
156/// 	}
157/// }
158/// ```
159#[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	/// A duration of zero seconds.
208	pub const ZERO: DurationFlex = DurationFlex { secs: 0, nanos: 0 };
209
210	/// Creates a new `DurationFlex` from seconds and nanoseconds.
211	pub const fn new(secs: i64, nanos: i32) -> Self {
212		DurationFlex { secs, nanos }
213	}
214
215	/// Creates a new `DurationFlex` from a whole number of seconds.
216	pub const fn from_secs(secs: i64) -> Self {
217		DurationFlex { secs, nanos: 0 }
218	}
219
220	/// Creates a new `DurationFlex` from a whole number of milliseconds.
221	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	/// Creates a new `DurationFlex` from a whole number of minutes.
228	pub const fn from_minutes(minutes: i64) -> Self {
229		DurationFlex { secs: minutes * SECS_PER_MINUTES, nanos: 0 }
230	}
231
232	/// Creates a new `DurationFlex` from a whole number of hours.
233	pub const fn from_hours(hours: i64) -> Self {
234		DurationFlex { secs: hours * SECS_PER_HOUR, nanos: 0 }
235	}
236
237	/// Creates a new `DurationFlex` from a whole number of days.
238	pub const fn from_days(days: i64) -> Self {
239		DurationFlex { secs: days * SECS_PER_DAY, nanos: 0 }
240	}
241
242	/// Creates a new `DurationFlex` from a whole number of weeks.
243	pub const fn from_weeks(weeks: i64) -> Self {
244		DurationFlex { secs: weeks * SECS_PER_WEEK, nanos: 0 }
245	}
246
247	/// Creates a new `DurationFlex` from a whole number of years (365 days each).
248	pub const fn from_years(years: i64) -> Self {
249		DurationFlex { secs: years * SECS_PER_YEAR, nanos: 0 }
250	}
251
252	/// Returns true if the duration is zero.
253	pub const fn is_zero(&self) -> bool {
254		self.secs == 0 && self.nanos == 0
255	}
256
257	/// Returns true if the duration is positive (> 0).
258	pub const fn is_positive(&self) -> bool {
259		self.secs > 0 || (self.secs == 0 && self.nanos > 0)
260	}
261
262	/// Returns true if the duration is negative (< 0).
263	pub const fn is_negative(&self) -> bool {
264		self.secs < 0 || (self.secs == 0 && self.nanos < 0)
265	}
266
267	/// Converts this duration into a [`std::time::Duration`] if non-negative.
268	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	/// Converts this duration into a [`chrono::Duration`].
277	pub fn to_chrono(&self) -> Duration {
278		Duration::from(*self)
279	}
280
281	/// Whole seconds.
282	pub fn secs(&self) -> i64 {
283		self.secs
284	}
285
286	/// Nano-seconds.
287	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}