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