fancy_duration/
lib.rs

1//!
2//! A "fancy duration" is a text description of the duration. For example, "1h 20m 30s" which might
3//! be read as "one hour, twenty minutes and thirty seconds". Expression in a duration type is
4//! transparent through a variety of means; chrono and the time crate are supported, as well as
5//! serialization into and from string types with serde. Time support starts in years and funnels
6//! down to nanoseconds.
7//!
8//! Feature matrix:
9//!   - serde: enables serde support including serialization and deseralization from strings
10//!   - time: enables traits that implement fancy duration features for the `time` crate
11//!   - chrono: enables traits that implement fancy duration features for the `chrono` crate
12//!
13//! What follows are some usage examples. You can either wrap your duration-like type in a
14//! FancyDuration struct, or use types which allow for monkeypatched methods that allow you to work
15//! directly on the target type. For example, use AsFancyDuration to inject fancy_duration calls to
16//! perform the construction (which can be formatted or converted to string) and ParseFancyDuration
17//! to inject parse_fancy_duration constructors to accept strings into your favorite type.
18//! std::time::Duration, time::Duration, and chrono::Duration are all supported (some features may
19//! need to be required) and you can make more types eligible by implementing the AsTimes trait.
20//!
21//! ```
22//! use std::time::Duration;
23//! use fancy_duration::FancyDuration;
24//!
25//! pub fn main() {
26//!     // use struct-wrapped or monkeypatched approaches with fancy_duration::AsFancyDuration;
27//!     assert_eq!(FancyDuration(Duration::new(20, 0)).to_string(), "20s");
28//!     assert_eq!(FancyDuration(Duration::new(600, 0)).to_string(), "10m");
29//!     assert_eq!(FancyDuration(Duration::new(120, 0)).to_string(), "2m");
30//!     assert_eq!(FancyDuration(Duration::new(185, 0)).to_string(), "3m 5s");
31//!     assert_eq!(FancyDuration::<Duration>::parse("3m 5s").unwrap().duration(), Duration::new(185, 0));
32//!     assert_eq!(FancyDuration(Duration::new(185, 0)).to_string(), "3m 5s");
33//!
34//!     // these traits are also implemented for chrono and time
35//!     use fancy_duration::{ParseFancyDuration, AsFancyDuration};
36//!     assert_eq!(Duration::new(20, 0).fancy_duration().to_string(), "20s");
37//!     assert_eq!(Duration::new(600, 0).fancy_duration().to_string(), "10m");
38//!     assert_eq!(Duration::new(120, 0).fancy_duration().to_string(), "2m");
39//!     assert_eq!(Duration::new(185, 0).fancy_duration().to_string(), "3m 5s");
40//!     assert_eq!(Duration::parse_fancy_duration("3m 5s".to_string()).unwrap(), Duration::new(185, 0));
41//!     assert_eq!(Duration::new(185, 0).fancy_duration().to_string(), "3m 5s");
42//!
43//!     #[cfg(feature = "time")]
44//!     {
45//!         // also works with time::Duration from the `time` crate
46//!         assert_eq!(FancyDuration(time::Duration::new(20, 0)).to_string(), "20s");
47//!         assert_eq!(FancyDuration(time::Duration::new(600, 0)).to_string(), "10m");
48//!         assert_eq!(FancyDuration(time::Duration::new(120, 0)).to_string(), "2m");
49//!         assert_eq!(FancyDuration(time::Duration::new(185, 0)).to_string(), "3m 5s");
50//!         assert_eq!(FancyDuration::<time::Duration>::parse("3m 5s").unwrap().duration(), time::Duration::new(185, 0));
51//!         assert_eq!(FancyDuration(time::Duration::new(185, 0)).to_string(), "3m 5s");
52//!     }
53//!
54//!     #[cfg(feature = "chrono")]
55//!     {
56//!         // also works with chrono!
57//!         assert_eq!(FancyDuration(chrono::TimeDelta::try_seconds(20).unwrap_or_default()).to_string(), "20s");
58//!         assert_eq!(FancyDuration(chrono::TimeDelta::try_seconds(600).unwrap_or_default()).to_string(), "10m");
59//!         assert_eq!(FancyDuration(chrono::TimeDelta::try_seconds(120).unwrap_or_default()).to_string(), "2m");
60//!         assert_eq!(FancyDuration(chrono::TimeDelta::try_seconds(185).unwrap_or_default()).to_string(), "3m 5s");
61//!         assert_eq!(FancyDuration::<chrono::Duration>::parse("3m 5s").unwrap().duration(), chrono::TimeDelta::try_seconds(185).unwrap_or_default());
62//!         assert_eq!(FancyDuration(chrono::TimeDelta::try_seconds(185).unwrap_or_default()).to_string(), "3m 5s");
63//!     }
64//! }
65//! ```
66
67lazy_static::lazy_static! {
68    static ref FANCY_FORMAT: regex::Regex = regex::Regex::new(r#"([0-9]+)([a-zA-Z]{1,2})\s*"#).unwrap();
69}
70
71#[cfg(feature = "serde")]
72use serde::{de::Visitor, Deserialize, Serialize};
73#[cfg(feature = "serde")]
74use std::marker::PhantomData;
75use std::time::Duration;
76
77/// Implement AsFancyDuration for your Duration type, it will annotate those types with the
78/// `fancy_duration` function which allows trivial and explicit conversion into a fancy duration.
79pub trait AsFancyDuration<T>
80where
81    Self: Sized,
82    T: AsTimes + Clone,
83{
84    /// Convert T to a fancy_duration, which can be converted to a string representation of the
85    /// duration.
86    fn fancy_duration(&self) -> FancyDuration<T>;
87}
88
89/// Implement ParseFancyDuration for your Duration type to implement parsing constructors for your
90/// Duration. A more generic `parse` implementation for String and &str may come in a future
91/// version.
92pub trait ParseFancyDuration<T>
93where
94    Self: Sized,
95    T: AsTimes + Clone,
96{
97    /// Parse T from String, which allows the construction of a T from the fancy duration specified
98    /// in the string.
99    fn parse_fancy_duration(s: String) -> Result<Self, anyhow::Error>;
100}
101
102impl ParseFancyDuration<Duration> for Duration {
103    fn parse_fancy_duration(s: String) -> Result<Self, anyhow::Error> {
104        Ok(FancyDuration::<Duration>::parse(&s)?.duration())
105    }
106}
107
108impl AsFancyDuration<Duration> for Duration {
109    fn fancy_duration(&self) -> FancyDuration<Duration> {
110        FancyDuration::new(self.clone())
111    }
112}
113
114impl<D> std::str::FromStr for FancyDuration<D>
115where
116    D: AsTimes + Clone,
117{
118    type Err = anyhow::Error;
119
120    fn from_str(s: &str) -> Result<Self, Self::Err> {
121        Self::parse(s)
122    }
123}
124
125#[cfg(feature = "time")]
126impl ParseFancyDuration<time::Duration> for time::Duration {
127    fn parse_fancy_duration(s: String) -> Result<Self, anyhow::Error> {
128        Ok(FancyDuration::<time::Duration>::parse(&s)?.duration())
129    }
130}
131
132#[cfg(feature = "time")]
133impl AsFancyDuration<time::Duration> for time::Duration {
134    fn fancy_duration(&self) -> FancyDuration<time::Duration> {
135        FancyDuration::new(self.clone())
136    }
137}
138
139#[cfg(feature = "chrono")]
140impl ParseFancyDuration<chrono::Duration> for chrono::Duration {
141    fn parse_fancy_duration(s: String) -> Result<Self, anyhow::Error> {
142        Ok(FancyDuration::<chrono::Duration>::parse(&s)?.duration())
143    }
144}
145
146#[cfg(feature = "chrono")]
147impl AsFancyDuration<chrono::Duration> for chrono::Duration {
148    fn fancy_duration(&self) -> FancyDuration<chrono::Duration> {
149        FancyDuration::new(self.clone())
150    }
151}
152
153/// AsTimes is the trait that allows [FancyDuration] to represent durations. Implementing these
154/// methods will allow any compatible type to work with FancyDuration.
155pub trait AsTimes: Sized {
156    /// To implement a fancier duration, just have your duration return the seconds and nanoseconds (in
157    /// a tuple) as a part of the following method call, as well as a method to handle parsing. The
158    /// nanoseconds value should just represent the subsecond count, not the seconds.
159    fn as_times(&self) -> (u64, u64);
160    /// This function implements parsing to return the inner duration. [FancyDuration::parse_to_ns]
161    /// is the standard parser and provides you with data to construct most duration types.
162    fn parse_to_duration(s: &str) -> Result<Self, anyhow::Error>;
163    /// Yield one of this implementing duration from a pair of (seconds, nanoseconds).
164    fn from_times(&self, s: u64, ns: u64) -> Self;
165}
166
167impl AsTimes for Duration {
168    fn as_times(&self) -> (u64, u64) {
169        let secs = self.as_secs();
170        let nanos = self.as_nanos();
171
172        (
173            secs,
174            (nanos - (nanos / 1e9 as u128) * 1e9 as u128)
175                .try_into()
176                .unwrap(),
177        )
178    }
179
180    fn parse_to_duration(s: &str) -> Result<Self, anyhow::Error> {
181        let ns = FancyDuration::<Duration>::parse_to_ns(s)?;
182        Ok(Duration::new(ns.0, ns.1.try_into()?))
183    }
184
185    fn from_times(&self, s: u64, ns: u64) -> Self {
186        Duration::new(s, ns.try_into().unwrap())
187    }
188}
189
190#[cfg(feature = "chrono")]
191impl AsTimes for chrono::Duration {
192    fn as_times(&self) -> (u64, u64) {
193        let secs = self.num_seconds();
194        let nanos = self.num_nanoseconds().unwrap();
195
196        (
197            secs.try_into().unwrap(),
198            (nanos - (nanos / 1e9 as i64) * 1e9 as i64)
199                .try_into()
200                .unwrap(),
201        )
202    }
203
204    fn parse_to_duration(s: &str) -> Result<Self, anyhow::Error> {
205        let ns = FancyDuration::<chrono::Duration>::parse_to_ns(s)?;
206
207        Ok(
208            chrono::TimeDelta::try_seconds(ns.0.try_into()?).unwrap_or_default()
209                + chrono::Duration::nanoseconds(ns.1.try_into()?),
210        )
211    }
212
213    fn from_times(&self, s: u64, ns: u64) -> Self {
214        chrono::TimeDelta::try_seconds(s.try_into().unwrap()).unwrap_or_default()
215            + chrono::Duration::nanoseconds(ns.try_into().unwrap())
216    }
217}
218
219#[cfg(feature = "time")]
220impl AsTimes for time::Duration {
221    fn as_times(&self) -> (u64, u64) {
222        (
223            self.as_seconds_f64() as u64,
224            self.subsec_nanoseconds() as u64,
225        )
226    }
227
228    fn parse_to_duration(s: &str) -> Result<Self, anyhow::Error> {
229        let ns = FancyDuration::<Duration>::parse_to_ns(s)?;
230        Ok(time::Duration::new(ns.0.try_into()?, ns.1.try_into()?))
231    }
232
233    fn from_times(&self, s: u64, ns: u64) -> Self {
234        time::Duration::new(s.try_into().unwrap(), ns.try_into().unwrap())
235    }
236}
237
238#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
239pub enum DurationPart {
240    Years,
241    Months,
242    Weeks,
243    Days,
244    Hours,
245    Minutes,
246    Seconds,
247    Milliseconds,
248    Microseconds,
249    Nanoseconds,
250}
251
252#[derive(Debug, Clone)]
253pub(crate) struct DurationBreakdown {
254    pub(crate) years: u64,
255    pub(crate) months: u64,
256    pub(crate) weeks: u64,
257    pub(crate) days: u64,
258    pub(crate) hours: u64,
259    pub(crate) minutes: u64,
260    pub(crate) seconds: u64,
261    pub(crate) milliseconds: u64,
262    pub(crate) microseconds: u64,
263    pub(crate) nanoseconds: u64,
264}
265
266const YEAR: u64 = 12 * 30 * 24 * 60 * 60;
267const MONTH: u64 = 30 * 24 * 60 * 60;
268const WEEK: u64 = 7 * 24 * 60 * 60;
269const DAY: u64 = 24 * 60 * 60;
270const HOUR: u64 = 60 * 60;
271const MINUTE: u64 = 60;
272
273impl DurationBreakdown {
274    pub(crate) fn new(mut s: u64, mut ns: u64) -> Self {
275        let years = s / YEAR;
276        s -= years * YEAR;
277        let months = s / MONTH;
278        s -= months * MONTH;
279        let weeks = s / WEEK;
280        s -= weeks * WEEK;
281        let days = s / DAY;
282        s -= days * DAY;
283        let hours = s / HOUR;
284        s -= hours * HOUR;
285        let minutes = s / MINUTE;
286        s -= minutes * MINUTE;
287
288        let ms = ns / 1e6 as u64;
289        ns -= ms * 1e6 as u64;
290        let us = ns / 1e3 as u64;
291        ns -= us * 1e3 as u64;
292
293        Self {
294            years,
295            months,
296            weeks,
297            days,
298            hours,
299            minutes,
300            seconds: s,
301            milliseconds: ms,
302            microseconds: us,
303            nanoseconds: ns,
304        }
305    }
306
307    pub(crate) fn truncate(&self, mut limit: usize) -> Self {
308        let mut obj = self.clone();
309        let mut limit_started = false;
310
311        for val in [
312            &mut obj.years,
313            &mut obj.months,
314            &mut obj.weeks,
315            &mut obj.days,
316            &mut obj.hours,
317            &mut obj.minutes,
318            &mut obj.seconds,
319            &mut obj.milliseconds,
320            &mut obj.microseconds,
321            &mut obj.nanoseconds,
322        ] {
323            if limit_started || *val > 0 {
324                limit_started = true;
325
326                if limit == 0 {
327                    *val = 0
328                }
329
330                if limit != 0 {
331                    limit -= 1;
332                }
333            }
334        }
335
336        obj
337    }
338
339    pub fn filter(&self, filter: &[DurationPart]) -> Self {
340        let mut obj = self.clone();
341
342        let all = &[
343            DurationPart::Years,
344            DurationPart::Months,
345            DurationPart::Weeks,
346            DurationPart::Days,
347            DurationPart::Hours,
348            DurationPart::Minutes,
349            DurationPart::Seconds,
350            DurationPart::Milliseconds,
351            DurationPart::Microseconds,
352            DurationPart::Nanoseconds,
353        ];
354
355        for part in all {
356            if !filter.contains(part) {
357                match part {
358                    DurationPart::Years => obj.years = 0,
359                    DurationPart::Months => obj.months = 0,
360                    DurationPart::Weeks => obj.weeks = 0,
361                    DurationPart::Days => obj.days = 0,
362                    DurationPart::Hours => obj.hours = 0,
363                    DurationPart::Minutes => obj.minutes = 0,
364                    DurationPart::Seconds => obj.seconds = 0,
365                    DurationPart::Milliseconds => obj.milliseconds = 0,
366                    DurationPart::Microseconds => obj.microseconds = 0,
367                    DurationPart::Nanoseconds => obj.nanoseconds = 0,
368                }
369            }
370        }
371
372        obj
373    }
374
375    pub fn as_times(&self) -> (u64, u64) {
376        let mut s = 0;
377        let mut ns = 0;
378
379        s += self.years * 12 * 30 * 24 * 60 * 60
380            + self.months * 30 * 24 * 60 * 60
381            + self.weeks * 7 * 24 * 60 * 60
382            + self.days * 24 * 60 * 60
383            + self.hours * 60 * 60
384            + self.minutes * 60
385            + self.seconds;
386        ns += self.milliseconds * 1e6 as u64 + self.microseconds * 1e3 as u64 + self.nanoseconds;
387
388        (s, ns)
389    }
390}
391
392/// A [FancyDuration] contains a duration of type that implements [AsTimes]. It is capable of that
393/// point at parsing strings as well as returning the duration value encapsulated. If included in a
394/// serde serializing or deserializing workflow, it will automatically construct the appropriate
395/// duration as a part of the process.
396///
397/// Support for [time] and [chrono] are available as a part of this library via feature flags.
398///
399/// A duration is "human-readable" when it follows the following format:
400///
401/// ```ignore
402/// <count><timespec>...
403/// ```
404///
405/// This pattern repeats in an expected and prescribed order of precedence based on what duration
406/// is supplied. Certain durations are order-dependent (like months and minutes), but most are not;
407/// that said it should be desired to represent your durations in precedence order. If you express
408/// standard formatting, each unit is separated by whitespace, such as "2m 5s 30ms", compact
409/// formatting removes the whitespace: "2m5s30ms".
410///
411/// `count` is simply an integer value with no leading zero-padding. `timespec` is a one or two
412/// character identifier that specifies the unit of time the count represents. The following
413/// timespecs are supported, and more may be added in the future based on demand.
414///
415/// The order here is precedence-order. So to express this properly, one might say "5y2d30m" which
416/// means "5 years, 2 days and 30 minutes", but "5y30m2d" means "5 years, 30 months, and 2 days".
417///
418/// - y: years
419/// - m: months (must appear before `minutes`)
420/// - w: weeks
421/// - d: days
422/// - h: hours
423/// - m: minutes (must appear after `months`)
424/// - s: seconds
425/// - ms: milliseconds
426/// - us: microseconds
427/// - ns: nanoseconds
428///
429/// Simplifications:
430///
431/// Some time units have been simplified:
432///
433/// - Years is 365 days
434/// - Months is 30 days
435///
436/// These durations do not account for variations in the potential unit based on the current time.
437/// Perhaps in a future release.
438///
439#[derive(Clone, Debug, PartialEq)]
440pub struct FancyDuration<D: AsTimes + Clone>(pub D);
441
442impl<D> FancyDuration<D>
443where
444    D: AsTimes + Clone,
445{
446    /// Construct a fancier duration!
447    ///
448    /// Accept input of a Duration type that implements [AsTimes]. From here, strings containing
449    /// human-friendly durations can be constructed, or the inner duration can be retrieved.
450    pub fn new(d: D) -> Self {
451        Self(d)
452    }
453
454    /// Retrieve the inner duration.
455    pub fn duration(&self) -> D
456    where
457        D: Clone,
458    {
459        self.0.clone()
460    }
461
462    /// Supply a filter of allowed time values, others will be zeroed out and the time recalculated
463    /// as if they didn't exist.
464    pub fn filter(&self, filter: &[DurationPart]) -> Self {
465        let mut obj = self.clone();
466        let times = self.0.as_times();
467        let filtered = DurationBreakdown::new(times.0, times.1)
468            .filter(filter)
469            .as_times();
470        obj.0 = self.0.from_times(filtered.0, filtered.1);
471        obj
472    }
473
474    /// Truncate to the most significant consecutive values. This will take a number like "1y 2m 3w
475    /// 4d" and with a value of 2 reduce it to "1y 2m". Since it works consecutively, minor values
476    /// will also be dropped, such as "1h 2m 30us", truncated to 3, would still produce "1h 2m"
477    /// because "30us" is below the seconds value, which is more significant and would have been
478    /// counted. "1h 2m 3s" would truncate to 3 with "1h 2m 3s".
479    pub fn truncate(&self, limit: usize) -> Self {
480        let mut obj = self.clone();
481        let times = self.0.as_times();
482        let truncated = DurationBreakdown::new(times.0, times.1)
483            .truncate(limit)
484            .as_times();
485        obj.0 = self.0.from_times(truncated.0, truncated.1);
486        obj
487    }
488
489    /// Parse a string that contains a human-readable duration. See [FancyDuration] for more
490    /// information on how times are represented.
491    pub fn parse(s: &str) -> Result<Self, anyhow::Error> {
492        Ok(FancyDuration::new(D::parse_to_duration(s)?))
493    }
494
495    /// Supply the standard formatted human-readable representation of the duration. This format
496    /// contains whitespace.
497    pub fn format(&self) -> String {
498        self.format_internal(true)
499    }
500
501    /// Supply the compact formatted human-readable representation of the duration. This format
502    /// does not contain whitespace.
503    pub fn format_compact(&self) -> String {
504        self.format_internal(false)
505    }
506
507    fn format_internal(&self, pad: bool) -> String {
508        let times = self.0.as_times();
509
510        if times.0 == 0 && times.1 == 0 {
511            return "0".to_string();
512        }
513
514        let breakdown = DurationBreakdown::new(times.0, times.1);
515
516        let mut s = String::new();
517
518        let spad = if pad { " " } else { "" };
519
520        if breakdown.years > 0 {
521            s += &format!("{}y{}", breakdown.years, spad)
522        }
523
524        if breakdown.months > 0 {
525            s += &format!("{}m{}", breakdown.months, spad)
526        }
527
528        if breakdown.weeks > 0 {
529            s += &format!("{}w{}", breakdown.weeks, spad)
530        }
531
532        if breakdown.days > 0 {
533            s += &format!("{}d{}", breakdown.days, spad)
534        }
535
536        if breakdown.hours > 0 {
537            s += &format!("{}h{}", breakdown.hours, spad)
538        }
539
540        if breakdown.minutes > 0 {
541            s += &format!("{}m{}", breakdown.minutes, spad)
542        }
543
544        if breakdown.seconds > 0 {
545            s += &format!("{}s{}", breakdown.seconds, spad)
546        }
547
548        if breakdown.milliseconds > 0 {
549            s += &format!("{}ms{}", breakdown.milliseconds, spad)
550        }
551
552        if breakdown.microseconds > 0 {
553            s += &format!("{}us{}", breakdown.microseconds, spad)
554        }
555
556        if breakdown.nanoseconds > 0 {
557            s += &format!("{}ns{}", breakdown.nanoseconds, spad)
558        }
559
560        if pad {
561            s.truncate(s.len() - 1);
562        }
563
564        s
565    }
566
567    /// Parse a string in fancy duration format to a tuple of (seconds, nanoseconds). Nanoseconds
568    /// is simply a subsecond count and does not contain the seconds represented as nanoseconds. If
569    /// a parsing error occurs that will appear in the result.
570    pub fn parse_to_ns(s: &str) -> Result<(u64, u64), anyhow::Error> {
571        let mut subseconds: u64 = 0;
572        let mut seconds: u64 = 0;
573        let mut past_minutes = false;
574
575        let mut list: Vec<(&str, &str)> = Vec::new();
576
577        for item in FANCY_FORMAT.captures_iter(s) {
578            list.push((item.get(1).unwrap().as_str(), item.get(2).unwrap().as_str()));
579        }
580
581        for (value, suffix) in list.iter().rev() {
582            match *suffix {
583                "ns" => {
584                    let result: u64 = value.parse()?;
585                    subseconds += result;
586                }
587                "ms" => {
588                    let result: u64 = value.parse()?;
589                    subseconds += result * 1e6 as u64;
590                }
591                "us" => {
592                    let result: u64 = value.parse()?;
593                    subseconds += result * 1e3 as u64;
594                }
595                "s" => {
596                    let result: u64 = value.parse()?;
597                    seconds += result;
598                }
599                "m" => {
600                    let result: u64 = value.parse()?;
601                    seconds += if past_minutes {
602                        result * 60 * 60 * 24 * 30
603                    } else {
604                        past_minutes = true;
605                        result * 60
606                    }
607                }
608                "h" => {
609                    past_minutes = true;
610                    let result: u64 = value.parse()?;
611                    seconds += result * 60 * 60
612                }
613                "d" => {
614                    past_minutes = true;
615                    let result: u64 = value.parse()?;
616                    seconds += result * 60 * 60 * 24
617                }
618                "w" => {
619                    past_minutes = true;
620                    let result: u64 = value.parse()?;
621                    seconds += result * 60 * 60 * 24 * 7
622                }
623                "y" => {
624                    past_minutes = true;
625                    let result: u64 = value.parse()?;
626                    seconds += result * 12 * 30 * 60 * 60 * 24
627                }
628                _ => {}
629            }
630        }
631
632        Ok((seconds, subseconds))
633    }
634}
635
636impl<D> std::fmt::Display for FancyDuration<D>
637where
638    D: AsTimes + Clone,
639{
640    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
641        f.write_str(&self.format())
642    }
643}
644
645#[cfg(feature = "serde")]
646impl<D> Serialize for FancyDuration<D>
647where
648    D: AsTimes + Clone,
649{
650    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
651    where
652        S: serde::Serializer,
653    {
654        serializer.serialize_str(&self.to_string())
655    }
656}
657
658#[cfg(feature = "serde")]
659struct FancyDurationVisitor<D: AsTimes>(PhantomData<D>);
660
661#[cfg(feature = "serde")]
662impl<D> Visitor<'_> for FancyDurationVisitor<D>
663where
664    D: AsTimes + Clone,
665{
666    type Value = FancyDuration<D>;
667
668    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
669        formatter.write_str("expecting a duration in 'fancy' format")
670    }
671
672    fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
673    where
674        E: serde::de::Error,
675    {
676        match FancyDuration::parse(v) {
677            Ok(res) => Ok(res),
678            Err(e) => Err(serde::de::Error::custom(e)),
679        }
680    }
681}
682
683#[cfg(feature = "serde")]
684impl<'de, T> Deserialize<'de> for FancyDuration<T>
685where
686    T: AsTimes + Clone,
687{
688    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
689    where
690        D: serde::Deserializer<'de>,
691    {
692        deserializer.deserialize_str(FancyDurationVisitor(PhantomData::default()))
693    }
694}
695
696#[cfg(test)]
697mod tests {
698    use std::time::Duration;
699
700    use crate::FancyDuration;
701
702    #[test]
703    fn test_fancy_duration_call() {
704        use super::{AsFancyDuration, ParseFancyDuration};
705
706        assert_eq!(Duration::new(0, 600).fancy_duration().to_string(), "600ns");
707        #[cfg(feature = "time")]
708        assert_eq!(
709            time::Duration::new(0, 600).fancy_duration().to_string(),
710            "600ns"
711        );
712        #[cfg(feature = "chrono")]
713        assert_eq!(
714            chrono::Duration::nanoseconds(600)
715                .fancy_duration()
716                .to_string(),
717            "600ns"
718        );
719        assert_eq!(
720            Duration::parse_fancy_duration("600ns".to_string()).unwrap(),
721            Duration::new(0, 600)
722        );
723        #[cfg(feature = "time")]
724        assert_eq!(
725            time::Duration::parse_fancy_duration("600ns".to_string()).unwrap(),
726            time::Duration::new(0, 600)
727        );
728        #[cfg(feature = "chrono")]
729        assert_eq!(
730            chrono::Duration::parse_fancy_duration("600ns".to_string()).unwrap(),
731            chrono::Duration::nanoseconds(600)
732        );
733    }
734
735    #[test]
736    fn test_duration_to_string() {
737        assert_eq!(FancyDuration(Duration::new(0, 600)).to_string(), "600ns");
738        assert_eq!(FancyDuration(Duration::new(0, 600000)).to_string(), "600us");
739        assert_eq!(
740            FancyDuration(Duration::new(0, 600000000)).to_string(),
741            "600ms"
742        );
743        assert_eq!(FancyDuration(Duration::new(600, 0)).to_string(), "10m");
744        assert_eq!(FancyDuration(Duration::new(120, 0)).to_string(), "2m");
745        assert_eq!(FancyDuration(Duration::new(185, 0)).to_string(), "3m 5s");
746        assert_eq!(
747            FancyDuration(Duration::new(24 * 60 * 60, 0)).to_string(),
748            "1d"
749        );
750        assert_eq!(FancyDuration(Duration::new(324, 0)).to_string(), "5m 24s");
751        assert_eq!(
752            FancyDuration(Duration::new(24 * 60 * 60 + 324, 0)).to_string(),
753            "1d 5m 24s"
754        );
755        assert_eq!(
756            FancyDuration(Duration::new(27 * 24 * 60 * 60 + 324, 0)).to_string(),
757            "3w 6d 5m 24s"
758        );
759        assert_eq!(
760            FancyDuration(Duration::new(99 * 24 * 60 * 60 + 324, 0)).to_string(),
761            "3m 1w 2d 5m 24s"
762        );
763
764        assert_eq!(
765            FancyDuration(Duration::new(12 * 30 * 24 * 60 * 60 + 10 * 24 * 60 * 60, 0)).to_string(),
766            "1y 1w 3d"
767        );
768
769        assert_eq!(
770            FancyDuration(Duration::new(12 * 30 * 24 * 60 * 60 + 10 * 24 * 60 * 60, 0))
771                .format_compact(),
772            "1y1w3d"
773        );
774
775        assert_eq!(
776            FancyDuration(Duration::new(324, 0)).format_compact(),
777            "5m24s"
778        );
779        assert_eq!(
780            FancyDuration(Duration::new(24 * 60 * 60 + 324, 0)).format_compact(),
781            "1d5m24s"
782        );
783        assert_eq!(
784            FancyDuration(Duration::new(27 * 24 * 60 * 60 + 324, 0)).format_compact(),
785            "3w6d5m24s"
786        );
787        assert_eq!(
788            FancyDuration(Duration::new(99 * 24 * 60 * 60 + 324, 0)).format_compact(),
789            "3m1w2d5m24s"
790        );
791    }
792
793    #[test]
794    #[cfg(feature = "time")]
795    fn test_time_duration_to_string() {
796        assert_eq!(
797            FancyDuration(time::Duration::new(0, 600)).to_string(),
798            "600ns"
799        );
800        assert_eq!(
801            FancyDuration(time::Duration::new(0, 600000)).to_string(),
802            "600us"
803        );
804        assert_eq!(
805            FancyDuration(time::Duration::new(0, 600000000)).to_string(),
806            "600ms"
807        );
808        assert_eq!(
809            FancyDuration(time::Duration::new(600, 0)).to_string(),
810            "10m"
811        );
812        assert_eq!(FancyDuration(time::Duration::new(120, 0)).to_string(), "2m");
813        assert_eq!(
814            FancyDuration(time::Duration::new(185, 0)).to_string(),
815            "3m 5s"
816        );
817        assert_eq!(
818            FancyDuration(time::Duration::new(24 * 60 * 60, 0)).to_string(),
819            "1d"
820        );
821        assert_eq!(
822            FancyDuration(time::Duration::new(324, 0)).to_string(),
823            "5m 24s"
824        );
825        assert_eq!(
826            FancyDuration(time::Duration::new(24 * 60 * 60 + 324, 0)).to_string(),
827            "1d 5m 24s"
828        );
829        assert_eq!(
830            FancyDuration(time::Duration::new(27 * 24 * 60 * 60 + 324, 0)).to_string(),
831            "3w 6d 5m 24s"
832        );
833        assert_eq!(
834            FancyDuration(time::Duration::new(99 * 24 * 60 * 60 + 324, 0)).to_string(),
835            "3m 1w 2d 5m 24s"
836        );
837
838        assert_eq!(
839            FancyDuration(time::Duration::new(
840                12 * 30 * 24 * 60 * 60 + 10 * 24 * 60 * 60,
841                0
842            ))
843            .to_string(),
844            "1y 1w 3d"
845        );
846
847        assert_eq!(
848            FancyDuration(time::Duration::new(
849                12 * 30 * 24 * 60 * 60 + 10 * 24 * 60 * 60,
850                0
851            ))
852            .format_compact(),
853            "1y1w3d"
854        );
855
856        assert_eq!(
857            FancyDuration(time::Duration::new(24 * 60 * 60 + 324, 0)).format_compact(),
858            "1d5m24s"
859        );
860        assert_eq!(
861            FancyDuration(time::Duration::new(27 * 24 * 60 * 60 + 324, 0)).format_compact(),
862            "3w6d5m24s"
863        );
864        assert_eq!(
865            FancyDuration(time::Duration::new(99 * 24 * 60 * 60 + 324, 0)).format_compact(),
866            "3m1w2d5m24s"
867        );
868    }
869
870    #[test]
871    #[cfg(feature = "chrono")]
872    fn test_chrono_duration_to_string() {
873        assert_eq!(
874            FancyDuration(chrono::Duration::nanoseconds(600)).to_string(),
875            "600ns"
876        );
877        assert_eq!(
878            FancyDuration(chrono::Duration::microseconds(600)).to_string(),
879            "600us"
880        );
881        assert_eq!(
882            FancyDuration(chrono::TimeDelta::try_milliseconds(600).unwrap_or_default()).to_string(),
883            "600ms"
884        );
885        assert_eq!(
886            FancyDuration(chrono::TimeDelta::try_seconds(600).unwrap_or_default()).to_string(),
887            "10m"
888        );
889        assert_eq!(
890            FancyDuration(chrono::TimeDelta::try_seconds(120).unwrap_or_default()).to_string(),
891            "2m"
892        );
893        assert_eq!(
894            FancyDuration(chrono::TimeDelta::try_seconds(185).unwrap_or_default()).to_string(),
895            "3m 5s"
896        );
897        assert_eq!(
898            FancyDuration(chrono::TimeDelta::try_seconds(24 * 60 * 60).unwrap_or_default())
899                .to_string(),
900            "1d"
901        );
902        assert_eq!(
903            FancyDuration(chrono::TimeDelta::try_seconds(324).unwrap_or_default()).to_string(),
904            "5m 24s"
905        );
906        assert_eq!(
907            FancyDuration(chrono::TimeDelta::try_seconds(24 * 60 * 60 + 324).unwrap_or_default())
908                .to_string(),
909            "1d 5m 24s"
910        );
911        assert_eq!(
912            FancyDuration(
913                chrono::TimeDelta::try_seconds(27 * 24 * 60 * 60 + 324).unwrap_or_default()
914            )
915            .to_string(),
916            "3w 6d 5m 24s"
917        );
918        assert_eq!(
919            FancyDuration(
920                chrono::TimeDelta::try_seconds(99 * 24 * 60 * 60 + 324).unwrap_or_default()
921            )
922            .to_string(),
923            "3m 1w 2d 5m 24s"
924        );
925
926        assert_eq!(
927            FancyDuration(
928                chrono::Duration::try_seconds(12 * 30 * 24 * 60 * 60 + 10 * 24 * 60 * 60,)
929                    .unwrap_or_default()
930            )
931            .to_string(),
932            "1y 1w 3d"
933        );
934
935        assert_eq!(
936            FancyDuration(
937                chrono::TimeDelta::try_seconds(12 * 30 * 24 * 60 * 60 + 10 * 24 * 60 * 60,)
938                    .unwrap_or_default()
939            )
940            .format_compact(),
941            "1y1w3d"
942        );
943        assert_eq!(
944            FancyDuration(chrono::TimeDelta::try_seconds(24 * 60 * 60 + 324).unwrap_or_default())
945                .format_compact(),
946            "1d5m24s"
947        );
948        assert_eq!(
949            FancyDuration(
950                chrono::TimeDelta::try_seconds(27 * 24 * 60 * 60 + 324).unwrap_or_default()
951            )
952            .format_compact(),
953            "3w6d5m24s"
954        );
955        assert_eq!(
956            FancyDuration(
957                chrono::TimeDelta::try_seconds(99 * 24 * 60 * 60 + 324).unwrap_or_default()
958            )
959            .format_compact(),
960            "3m1w2d5m24s"
961        );
962    }
963
964    #[test]
965    fn test_parse_filter() {
966        use super::DurationPart;
967        let duration_table = [
968            (
969                "1m 5s 10ms",
970                vec![DurationPart::Minutes, DurationPart::Milliseconds],
971                "1m 10ms",
972            ),
973            (
974                "1h 1m 30us",
975                vec![DurationPart::Minutes, DurationPart::Microseconds],
976                "1m 30us",
977            ),
978            ("1d 1h 30ns", vec![DurationPart::Days], "1d"),
979            (
980                "10s",
981                vec![DurationPart::Seconds, DurationPart::Minutes],
982                "10s",
983            ),
984            (
985                "3m 5s",
986                vec![
987                    DurationPart::Hours,
988                    DurationPart::Minutes,
989                    DurationPart::Seconds,
990                ],
991                "3m 5s",
992            ),
993            (
994                "3m 2w 2d 10m 10s",
995                vec![
996                    DurationPart::Months,
997                    DurationPart::Weeks,
998                    DurationPart::Days,
999                ],
1000                "3m 2w 2d",
1001            ),
1002        ];
1003
1004        for (orig_duration, filter, new_duration) in &duration_table {
1005            assert_eq!(
1006                *new_duration,
1007                FancyDuration::<Duration>::parse(orig_duration)
1008                    .unwrap()
1009                    .filter(&filter)
1010                    .to_string()
1011            )
1012        }
1013
1014        #[cfg(feature = "time")]
1015        for (orig_duration, filter, new_duration) in &duration_table {
1016            assert_eq!(
1017                *new_duration,
1018                FancyDuration::<time::Duration>::parse(orig_duration)
1019                    .unwrap()
1020                    .filter(&filter)
1021                    .to_string()
1022            )
1023        }
1024
1025        #[cfg(feature = "chrono")]
1026        for (orig_duration, filter, new_duration) in &duration_table {
1027            assert_eq!(
1028                *new_duration,
1029                FancyDuration::<chrono::Duration>::parse(orig_duration)
1030                    .unwrap()
1031                    .filter(&filter)
1032                    .to_string()
1033            )
1034        }
1035    }
1036    #[test]
1037    fn test_parse_truncate() {
1038        let duration_table = [
1039            ("1m 5s 10ms", 2, "1m 5s"),
1040            ("1h 1m 30us", 3, "1h 1m"),
1041            ("1d 1h 30ns", 1, "1d"),
1042            ("10s", 3, "10s"),
1043            ("3m 5s", 2, "3m 5s"),
1044            ("3m 2w 2d 10m 10s", 3, "3m 2w 2d"),
1045        ];
1046
1047        for (orig_duration, truncate, new_duration) in &duration_table {
1048            assert_eq!(
1049                *new_duration,
1050                FancyDuration::<Duration>::parse(orig_duration)
1051                    .unwrap()
1052                    .truncate(*truncate)
1053                    .to_string()
1054            )
1055        }
1056
1057        #[cfg(feature = "time")]
1058        for (orig_duration, truncate, new_duration) in &duration_table {
1059            assert_eq!(
1060                *new_duration,
1061                FancyDuration::<time::Duration>::parse(orig_duration)
1062                    .unwrap()
1063                    .truncate(*truncate)
1064                    .to_string()
1065            )
1066        }
1067
1068        #[cfg(feature = "chrono")]
1069        for (orig_duration, truncate, new_duration) in &duration_table {
1070            assert_eq!(
1071                *new_duration,
1072                FancyDuration::<chrono::Duration>::parse(orig_duration)
1073                    .unwrap()
1074                    .truncate(*truncate)
1075                    .to_string()
1076            )
1077        }
1078    }
1079
1080    #[test]
1081    fn test_parse_duration() {
1082        let duration_table = [
1083            ("1m 10ms", Duration::new(60, 10000000)),
1084            ("1h 30us", Duration::new(60 * 60, 30000)),
1085            ("1d 30ns", Duration::new(60 * 60 * 24, 30)),
1086            ("10s", Duration::new(10, 0)),
1087            ("3m 5s", Duration::new(185, 0)),
1088            ("3m 2w 2d 10m 10s", Duration::new(9159010, 0)),
1089        ];
1090
1091        let compact_duration_table = [
1092            ("10s30ns", Duration::new(10, 30)),
1093            ("3m5s", Duration::new(185, 0)),
1094            ("3m2w2d10m10s", Duration::new(9159010, 0)),
1095        ];
1096
1097        for item in duration_table {
1098            let fancy = FancyDuration::<Duration>::parse(item.0).unwrap();
1099            assert_eq!(fancy.duration(), item.1);
1100            assert_eq!(FancyDuration::new(item.1).to_string(), item.0);
1101        }
1102
1103        for item in compact_duration_table {
1104            let fancy = FancyDuration::<Duration>::parse(item.0).unwrap();
1105            assert_eq!(fancy.duration(), item.1);
1106            assert_eq!(FancyDuration::new(item.1).format_compact(), item.0);
1107        }
1108
1109        #[cfg(feature = "time")]
1110        {
1111            let time_table = [
1112                ("1m 10ms", time::Duration::new(60, 10000000)),
1113                ("1h 30us", time::Duration::new(60 * 60, 30000)),
1114                ("1d 30ns", time::Duration::new(60 * 60 * 24, 30)),
1115                ("10s", time::Duration::new(10, 0)),
1116                ("3m 5s", time::Duration::new(185, 0)),
1117                ("3m 2w 2d 10m 10s", time::Duration::new(9159010, 0)),
1118            ];
1119
1120            let compact_time_table = [
1121                ("3m5s", time::Duration::new(185, 0)),
1122                ("3m2w2d10m10s", time::Duration::new(9159010, 0)),
1123            ];
1124            for item in time_table {
1125                let fancy = FancyDuration::<time::Duration>::parse(item.0).unwrap();
1126                assert_eq!(fancy.duration(), item.1);
1127                assert_eq!(FancyDuration::new(item.1).to_string(), item.0);
1128            }
1129
1130            for item in compact_time_table {
1131                let fancy = FancyDuration::<time::Duration>::parse(item.0).unwrap();
1132                assert_eq!(fancy.duration(), item.1);
1133                assert_eq!(FancyDuration::new(item.1).format_compact(), item.0);
1134            }
1135        }
1136
1137        #[cfg(feature = "chrono")]
1138        {
1139            let chrono_table = [
1140                (
1141                    "1m 10ms",
1142                    chrono::TimeDelta::try_seconds(60).unwrap_or_default()
1143                        + chrono::TimeDelta::try_milliseconds(10).unwrap_or_default(),
1144                ),
1145                (
1146                    "1h 30us",
1147                    chrono::TimeDelta::try_hours(1).unwrap_or_default()
1148                        + chrono::Duration::microseconds(30),
1149                ),
1150                (
1151                    "1d 30ns",
1152                    chrono::TimeDelta::try_days(1).unwrap_or_default()
1153                        + chrono::Duration::nanoseconds(30),
1154                ),
1155                (
1156                    "10s",
1157                    chrono::TimeDelta::try_seconds(10).unwrap_or_default(),
1158                ),
1159                (
1160                    "3m 5s",
1161                    chrono::TimeDelta::try_seconds(185).unwrap_or_default(),
1162                ),
1163                (
1164                    "3m 2w 2d 10m 10s",
1165                    chrono::TimeDelta::try_seconds(9159010).unwrap_or_default(),
1166                ),
1167            ];
1168
1169            let compact_chrono_table = [
1170                (
1171                    "3m5s",
1172                    chrono::TimeDelta::try_seconds(185).unwrap_or_default(),
1173                ),
1174                (
1175                    "3m2w2d10m10s",
1176                    chrono::TimeDelta::try_seconds(9159010).unwrap_or_default(),
1177                ),
1178            ];
1179            for item in chrono_table {
1180                let fancy = FancyDuration::<chrono::Duration>::parse(item.0).unwrap();
1181                assert_eq!(fancy.duration(), item.1);
1182                assert_eq!(FancyDuration::new(item.1).to_string(), item.0);
1183            }
1184
1185            for item in compact_chrono_table {
1186                let fancy = FancyDuration::<chrono::Duration>::parse(item.0).unwrap();
1187                assert_eq!(fancy.duration(), item.1);
1188                assert_eq!(FancyDuration::new(item.1).format_compact(), item.0);
1189            }
1190        }
1191    }
1192
1193    #[cfg(feature = "serde")]
1194    #[test]
1195    fn test_serde() {
1196        use serde::{Deserialize, Serialize};
1197
1198        #[derive(Serialize, Deserialize)]
1199        struct StdDuration {
1200            duration: FancyDuration<std::time::Duration>,
1201        }
1202
1203        let duration_table = [
1204            ("{\"duration\":\"10ns\"}", Duration::new(0, 10)),
1205            ("{\"duration\":\"10s\"}", Duration::new(10, 0)),
1206            ("{\"duration\":\"3m 5s\"}", Duration::new(185, 0)),
1207            (
1208                "{\"duration\":\"1y 3m 2w 2d 10m 10s\"}",
1209                Duration::new(40263010, 0),
1210            ),
1211        ];
1212
1213        for item in duration_table {
1214            let md: StdDuration = serde_json::from_str(item.0).unwrap();
1215            assert_eq!(md.duration.duration(), item.1);
1216            assert_eq!(serde_json::to_string(&md).unwrap(), item.0);
1217        }
1218
1219        #[cfg(feature = "time")]
1220        {
1221            #[derive(Serialize, Deserialize)]
1222            struct TimeDuration {
1223                duration: FancyDuration<time::Duration>,
1224            }
1225
1226            let time_table = [
1227                ("{\"duration\":\"10ns\"}", time::Duration::new(0, 10)),
1228                ("{\"duration\":\"10s\"}", time::Duration::new(10, 0)),
1229                ("{\"duration\":\"3m 5s\"}", time::Duration::new(185, 0)),
1230                (
1231                    "{\"duration\":\"1y 3m 2w 2d 10m 10s\"}",
1232                    time::Duration::new(40263010, 0),
1233                ),
1234            ];
1235
1236            for item in time_table {
1237                let md: TimeDuration = serde_json::from_str(item.0).unwrap();
1238                assert_eq!(md.duration.duration(), item.1);
1239                assert_eq!(serde_json::to_string(&md).unwrap(), item.0);
1240            }
1241        }
1242
1243        #[cfg(feature = "chrono")]
1244        {
1245            #[derive(Serialize, Deserialize)]
1246            struct ChronoDuration {
1247                duration: FancyDuration<chrono::Duration>,
1248            }
1249
1250            let chrono_table = [
1251                ("{\"duration\":\"10ns\"}", chrono::Duration::nanoseconds(10)),
1252                (
1253                    "{\"duration\":\"10s\"}",
1254                    chrono::TimeDelta::try_seconds(10).unwrap_or_default(),
1255                ),
1256                (
1257                    "{\"duration\":\"3m 5s\"}",
1258                    chrono::TimeDelta::try_seconds(185).unwrap_or_default(),
1259                ),
1260                (
1261                    "{\"duration\":\"1y 3m 2w 2d 10m 10s\"}",
1262                    chrono::TimeDelta::try_seconds(40263010).unwrap_or_default(),
1263                ),
1264            ];
1265
1266            for item in chrono_table {
1267                let md: ChronoDuration = serde_json::from_str(item.0).unwrap();
1268                assert_eq!(md.duration.duration(), item.1);
1269                assert_eq!(serde_json::to_string(&md).unwrap(), item.0);
1270            }
1271        }
1272    }
1273}