duration_breakdown/
lib.rs

1//! This crate breaks down durations of time into their constituent parts of
2//! various units (weeks, days, hours, minutes, seconds, and nanoseconds).
3//!
4//! This can be used to convert a duration such as 10,000 seconds into the
5//! following form: 0 weeks, 0 days, 2 hours, 46 minutes, 40 seconds, and 0 nanoseconds.
6//!
7//! # Examples
8//! ```
9//! use duration_breakdown::DurationBreakdown;
10//! use std::time::Duration;
11//! let breakdown = DurationBreakdown::from(Duration::new(12_345_678, 1234));
12//! assert_eq!(
13//!     breakdown.to_string(),
14//!     "20 weeks, 2 days, 21 hours, 21 minutes, 18 seconds, and 1234 nanoseconds");
15//! ```
16
17use std::{
18    convert::{From, TryFrom},
19    fmt::{self, Display},
20    time::Duration,
21};
22
23// Constants for converting between units of time.
24const NANOSECONDS_PER_SECOND: u64 = 1_000_000_000;
25const SECONDS_PER_MINUTE: u64 = 60;
26const MINUTES_PER_HOUR: u64 = 60;
27const HOURS_PER_DAY: u64 = 24;
28const DAYS_PER_WEEK: u64 = 7;
29
30// We access a `std::time::Duration`'s total duration in seconds,
31// so these facilitate conversion into a breakdown.
32const SECONDS_PER_HOUR: u64 = SECONDS_PER_MINUTE * 60;
33const SECONDS_PER_DAY: u64 = SECONDS_PER_HOUR * 24;
34const SECONDS_PER_WEEK: u64 = SECONDS_PER_DAY * 7;
35
36/// A `DurationBreakdown` represents a duration of time that has been
37/// broken up into several units (i.e. weeks, days, etc) in such a way
38/// that the sum of each unit comprises the whole duration of time.
39#[derive(Eq, PartialEq, Debug, Clone, Copy)]
40pub struct DurationBreakdown {
41    weeks: u64,
42    days: u64,
43    hours: u64,
44    minutes: u64,
45    seconds: u64,
46    nanoseconds: u64,
47}
48
49/// The granularity of a breakdown. A `DurationBreakdown` with a `Minutes` precision
50/// would have possibly non-zero values for its weeks, days, hours, and minutes,
51/// but 0 for its seconds and nanoseconds.
52///
53/// See the `with_precision` method on [`DurationBreakdown`] for more on how
54/// `Precision` is used.
55#[derive(Debug, Eq, PartialEq, PartialOrd, Clone, Copy)]
56pub enum Precision {
57    Weeks = 0,
58    Days = 1,
59    Hours = 2,
60    Minutes = 3,
61    Seconds = 4,
62    Nanoseconds = 5,
63}
64
65impl DurationBreakdown {
66    /// Constructs a `DurationBreakdown` directly from the given component parts.
67    ///
68    /// # Examples
69    /// ```
70    /// # use duration_breakdown::DurationBreakdown;
71    /// let breakdown = DurationBreakdown::from_parts(
72    ///     4,   // weeks
73    ///     2,   // days
74    ///     17,  // hours
75    ///     41,  // minutes
76    ///     18,  // seconds
77    ///     100, // nanoseconds
78    /// );
79    /// assert_eq!(breakdown.weeks(), 4);
80    /// assert_eq!(breakdown.days(), 2);
81    /// assert_eq!(breakdown.hours(), 17);
82    /// assert_eq!(breakdown.minutes(), 41);
83    /// assert_eq!(breakdown.seconds(), 18);
84    /// assert_eq!(breakdown.nanoseconds(), 100);
85    /// ```
86    pub fn from_parts(
87        weeks: u64,
88        days: u64,
89        hours: u64,
90        minutes: u64,
91        seconds: u64,
92        nanoseconds: u64,
93    ) -> Self {
94        DurationBreakdown {
95            weeks,
96            days,
97            hours,
98            minutes,
99            seconds,
100            nanoseconds,
101        }
102    }
103
104    /// Creates a copy of the given `DurationBreakdown` with a given precision.
105    /// Specifying a precision allows you to discard the pieces of the breakdown
106    /// which are below a certain granularity.
107    ///
108    /// All units below the given precision are set to 0 in the breakdown (not
109    /// rounded).
110    ///
111    /// # Examples
112    /// ```
113    /// # use duration_breakdown::DurationBreakdown;
114    /// # use duration_breakdown::Precision;
115    /// let breakdown = DurationBreakdown::from_parts(14, 2, 16, 25, 55, 400);
116    /// assert_eq!(
117    ///     breakdown.with_precision(Precision::Hours),
118    ///     DurationBreakdown::from_parts(14, 2, 16, 0, 0, 0)
119    /// );
120    /// ```
121    pub fn with_precision(&self, precision: Precision) -> Self {
122        // Make a copy of self
123        let mut breakdown = *self;
124
125        macro_rules! zero_if_under_threshold {
126            ($field:ident, $precision:expr) => {
127                // If the precision falls below the given level, zero
128                // the corresponding part of the breakdown.
129                if precision < $precision {
130                    breakdown.$field = 0;
131                }
132            };
133        }
134
135        zero_if_under_threshold!(nanoseconds, Precision::Nanoseconds);
136        zero_if_under_threshold!(seconds, Precision::Seconds);
137        zero_if_under_threshold!(minutes, Precision::Minutes);
138        zero_if_under_threshold!(hours, Precision::Hours);
139        zero_if_under_threshold!(days, Precision::Days);
140        zero_if_under_threshold!(weeks, Precision::Weeks);
141
142        breakdown
143    }
144
145    /// Converts a `DurationBreakdown` into a standard form in which the value
146    /// of a given time component (week, day, etc) is no greater than the value
147    /// of a single unit of the time component one level up. For instance,
148    /// a `DurationBreakdown` with 68 as its minutes value and 3 as its
149    /// hours value would be normalized to 8 minutes and 4 hours.
150    ///
151    /// # Examples
152    /// ```
153    /// # use duration_breakdown::DurationBreakdown;
154    /// // 9 days, 1 hour, 50 minutes, 70 seconds (not normalized)
155    /// let mut breakdown = DurationBreakdown::from_parts(0, 9, 1, 50, 70, 0);
156    /// breakdown.normalize();
157    /// assert_eq!(
158    ///     breakdown.as_string(),
159    ///     "1 week, 2 days, 1 hour, 51 minutes, 10 seconds, and 0 nanoseconds");
160    /// ```
161    pub fn normalize(&mut self) {
162        // Propagates overflow from one unit (sub_unit) into the next (super_unit)
163        macro_rules! propagate_overflow {
164            ($sub_unit:ident, $super_unit:ident, $sub_per_super:ident) => {
165                // If the sub-unit exceeds the number of sub-units per super, spill
166                // the sub-unit into the super and decrease the sub-unit accordingly
167                if self.$sub_unit >= $sub_per_super {
168                    self.$super_unit += self.$sub_unit / $sub_per_super;
169                    self.$sub_unit %= $sub_per_super;
170                }
171            };
172        }
173
174        propagate_overflow!(nanoseconds, seconds, NANOSECONDS_PER_SECOND);
175        propagate_overflow!(seconds, minutes, SECONDS_PER_MINUTE);
176        propagate_overflow!(minutes, hours, MINUTES_PER_HOUR);
177        propagate_overflow!(hours, days, HOURS_PER_DAY);
178        propagate_overflow!(days, weeks, DAYS_PER_WEEK);
179    }
180
181    /// Gets the number of weeks in the breakdown.
182    pub fn weeks(&self) -> u64 {
183        self.weeks
184    }
185
186    /// Gets the number of days in the breakdown.
187    pub fn days(&self) -> u64 {
188        self.days
189    }
190
191    /// Gets the number of hours in the breakdown.
192    pub fn hours(&self) -> u64 {
193        self.hours
194    }
195
196    /// Gets the number of minutes in the breakdown.
197    pub fn minutes(&self) -> u64 {
198        self.minutes
199    }
200
201    /// Gets the number of seconds in the breakdown.
202    pub fn seconds(&self) -> u64 {
203        self.seconds
204    }
205
206    /// Gets the number of nanoseconds in the breakdown.
207    pub fn nanoseconds(&self) -> u64 {
208        self.nanoseconds
209    }
210
211    // Determines whether or not to attach a plural suffix.
212    fn plural(quantity: u64) -> String {
213        (if quantity == 1 { "" } else { "s" }).to_string()
214    }
215
216    /// A string describing the number of weeks in the breakdown. E.g. `"14 weeks"`.
217    pub fn weeks_as_string(&self) -> String {
218        format!(
219            "{} week{}",
220            self.weeks,
221            DurationBreakdown::plural(self.weeks)
222        )
223    }
224
225    /// A string describing the number of days in the breakdown. E.g. `"6 days"`.
226    pub fn days_as_string(&self) -> String {
227        format!("{} day{}", self.days, DurationBreakdown::plural(self.days))
228    }
229
230    /// A string describing the number of hours in the breakdown. E.g. `"1 hour"`.
231    pub fn hours_as_string(&self) -> String {
232        format!(
233            "{} hour{}",
234            self.hours,
235            DurationBreakdown::plural(self.hours)
236        )
237    }
238
239    /// A string describing the number of minutes in the breakdown. E.g. `"53 minutes"`.
240    pub fn minutes_as_string(&self) -> String {
241        format!(
242            "{} minute{}",
243            self.minutes,
244            DurationBreakdown::plural(self.minutes)
245        )
246    }
247
248    /// A string describing the number of seconds in the breakdown. E.g. `"40 seconds"`.
249    pub fn seconds_as_string(&self) -> String {
250        format!(
251            "{} second{}",
252            self.seconds,
253            DurationBreakdown::plural(self.seconds)
254        )
255    }
256
257    /// A string describing the number of nanoseconds in the breakdown. E.g. `"1700 nanoseconds"`.
258    pub fn nanoseconds_as_string(&self) -> String {
259        format!(
260            "{} nanosecond{}",
261            self.nanoseconds,
262            DurationBreakdown::plural(self.nanoseconds)
263        )
264    }
265
266    /// A string describing the entire `DurationBreakdown`. All components
267    /// are included, even if their value is 0. See `as_string_hide_zeros`
268    /// for an alternate display of the breakdown.
269    ///
270    /// Note that this function is used by the implementation of `Display` for
271    /// `DurationBreakdown`.
272    ///
273    /// # Examples
274    /// ```
275    /// # use duration_breakdown::DurationBreakdown;
276    /// let breakdown = DurationBreakdown::from_parts(0, 4, 0, 10, 48, 200);
277    /// assert_eq!(
278    ///     breakdown.as_string(),
279    ///     "0 weeks, 4 days, 0 hours, 10 minutes, 48 seconds, and 200 nanoseconds");
280    /// ```
281    pub fn as_string(&self) -> String {
282        format!(
283            "{}, {}, {}, {}, {}, and {}",
284            self.weeks_as_string(),
285            self.days_as_string(),
286            self.hours_as_string(),
287            self.minutes_as_string(),
288            self.seconds_as_string(),
289            self.nanoseconds_as_string(),
290        )
291    }
292
293    /// A string describing the entire `DurationBreakdown`, but any components
294    /// that have a value of 0 are omitted from the description. See
295    /// `as_string` for a version of this function that includes 0-valued
296    /// components.
297    ///
298    /// # Examples
299    /// ```
300    /// # use duration_breakdown::DurationBreakdown;
301    /// let breakdown = DurationBreakdown::from_parts(0, 4, 0, 10, 48, 200);
302    /// assert_eq!(
303    ///     breakdown.as_string_hide_zeros(),
304    ///     "4 days, 10 minutes, 48 seconds, and 200 nanoseconds");
305    /// ```
306    pub fn as_string_hide_zeros(&self) -> String {
307        let mut components: Vec<String> = vec![
308            (self.weeks, self.weeks_as_string()),
309            (self.days, self.days_as_string()),
310            (self.hours, self.hours_as_string()),
311            (self.minutes, self.minutes_as_string()),
312            (self.seconds, self.seconds_as_string()),
313            (self.nanoseconds, self.nanoseconds_as_string()),
314        ]
315        .into_iter()
316        .filter_map(|(v, s)| if v != 0 { Some(s) } else { None })
317        .collect();
318
319        if let Some(last) = components.last_mut() {
320            *last = format!("and {}", last);
321        }
322
323        components.join(", ")
324    }
325}
326
327impl Display for DurationBreakdown {
328    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
329        write!(f, "{}", self.as_string())
330    }
331}
332
333impl From<Duration> for DurationBreakdown {
334    /// Constructs a new duration breakdown, given an instance of `std::time::Duration`.
335    fn from(duration: Duration) -> Self {
336        let mut seconds_left = duration.as_secs();
337
338        let weeks = seconds_left / SECONDS_PER_WEEK;
339        seconds_left %= SECONDS_PER_WEEK;
340
341        let days = seconds_left / SECONDS_PER_DAY;
342        seconds_left %= SECONDS_PER_DAY;
343
344        let hours = seconds_left / SECONDS_PER_HOUR;
345        seconds_left %= SECONDS_PER_HOUR;
346
347        let minutes = seconds_left / SECONDS_PER_MINUTE;
348        seconds_left %= SECONDS_PER_MINUTE;
349
350        let seconds = seconds_left;
351        let nanoseconds = u64::from(duration.subsec_nanos());
352
353        DurationBreakdown {
354            weeks,
355            days,
356            hours,
357            minutes,
358            seconds,
359            nanoseconds,
360        }
361    }
362}
363
364impl From<DurationBreakdown> for Duration {
365    /// Constructs a new `std::time::Duration`, given a `DurationBreakdown`.
366    ///
367    /// # Panics
368    /// This will panic if the `DurationBreakdown`'s nanoseconds value is
369    /// greater than `u32::MAX`.
370    fn from(db: DurationBreakdown) -> Self {
371        Duration::new(
372            (db.weeks * SECONDS_PER_WEEK)
373                + (db.days * SECONDS_PER_DAY)
374                + (db.hours * SECONDS_PER_HOUR)
375                + (db.minutes * SECONDS_PER_MINUTE)
376                + (db.seconds),
377            u32::try_from(db.nanoseconds)
378                .expect("DurationBreakdown's nanoseconds value greater than max u32"),
379        )
380    }
381}
382
383#[cfg(test)]
384mod test {
385    use super::*;
386    use quickcheck::quickcheck;
387    use std::time::Duration;
388
389    #[test]
390    fn zero_duration_is_all_zeros() {
391        assert_eq!(
392            DurationBreakdown::from(Duration::new(0, 0)),
393            DurationBreakdown {
394                weeks: 0,
395                days: 0,
396                hours: 0,
397                minutes: 0,
398                seconds: 0,
399                nanoseconds: 0,
400            }
401        );
402    }
403
404    #[test]
405    fn two_hours() {
406        assert_eq!(
407            DurationBreakdown::from(Duration::from_secs(60 * 60 * 2)),
408            DurationBreakdown {
409                weeks: 0,
410                days: 0,
411                hours: 2,
412                minutes: 0,
413                seconds: 0,
414                nanoseconds: 0,
415            }
416        )
417    }
418
419    #[test]
420    fn more_complicated() {
421        assert_eq!(
422            DurationBreakdown::from(Duration::from_secs(15403)),
423            DurationBreakdown {
424                weeks: 0,
425                days: 0,
426                hours: 4,
427                minutes: 16,
428                seconds: 43,
429                nanoseconds: 0,
430            }
431        )
432    }
433
434    #[test]
435    fn with_nanoseconds() {
436        assert_eq!(
437            DurationBreakdown::from(Duration::from_nanos(4150)),
438            DurationBreakdown {
439                weeks: 0,
440                days: 0,
441                hours: 0,
442                minutes: 0,
443                seconds: 0,
444                nanoseconds: 4150,
445            }
446        );
447    }
448
449    #[test]
450    fn extracting_components() {
451        let d = DurationBreakdown {
452            weeks: 14,
453            days: 5,
454            hours: 20,
455            minutes: 13,
456            seconds: 48,
457            nanoseconds: 1600,
458        };
459
460        assert_eq!(d.weeks(), 14);
461        assert_eq!(d.days(), 5);
462        assert_eq!(d.hours(), 20);
463        assert_eq!(d.minutes(), 13);
464        assert_eq!(d.seconds(), 48);
465        assert_eq!(d.nanoseconds(), 1600);
466    }
467
468    #[test]
469    fn from_parts() {
470        let d = DurationBreakdown::from_parts(45, 10, 16, 0, 17, 450);
471        assert_eq!(d.weeks(), 45);
472        assert_eq!(d.days(), 10);
473        assert_eq!(d.hours(), 16);
474        assert_eq!(d.minutes(), 0);
475        assert_eq!(d.seconds(), 17);
476        assert_eq!(d.nanoseconds(), 450);
477    }
478
479    #[test]
480    fn hide_zeros() {
481        let d = DurationBreakdown::from_parts(40, 0, 0, 16, 1, 0);
482        assert_eq!(
483            d.as_string_hide_zeros(),
484            "40 weeks, 16 minutes, and 1 second"
485        );
486        let d = DurationBreakdown::from(Duration::new(0, 0));
487        assert_eq!(d.as_string_hide_zeros(), "");
488    }
489
490    #[test]
491    fn duration_from_breakdown() {
492        let db = DurationBreakdown::from_parts(0, 0, 2, 13, 48, 700);
493        assert_eq!(Duration::from(db), Duration::new(8028, 700));
494    }
495
496    #[test]
497    fn normalize() {
498        let mut breakdown = DurationBreakdown::from_parts(0, 9, 1, 50, 70, 0);
499        breakdown.normalize();
500        assert_eq!(breakdown.weeks(), 1);
501        assert_eq!(breakdown.days(), 2);
502        assert_eq!(breakdown.hours(), 1);
503        assert_eq!(breakdown.minutes(), 51);
504        assert_eq!(breakdown.seconds(), 10);
505        assert_eq!(breakdown.nanoseconds(), 0);
506    }
507
508    #[test]
509    fn max_breakdown() {
510        // Duration::MAX is platform dependent, so this test
511        // just makes sure that creating a breakdown doesn't panic
512        DurationBreakdown::from(Duration::MAX);
513    }
514
515    #[test]
516    fn precision() {
517        let breakdown = DurationBreakdown::from_parts(40, 2, 18, 12, 22, 7200);
518        assert_eq!(
519            breakdown.with_precision(Precision::Weeks),
520            DurationBreakdown::from_parts(40, 0, 0, 0, 0, 0)
521        );
522        assert_eq!(
523            breakdown.with_precision(Precision::Minutes),
524            DurationBreakdown::from_parts(40, 2, 18, 12, 0, 0)
525        );
526        // nanosecond precision just copies the original
527        assert_eq!(breakdown.with_precision(Precision::Nanoseconds), breakdown);
528    }
529
530    fn breakdown_from_secs(secs: u64) -> DurationBreakdown {
531        DurationBreakdown::from(Duration::from_secs(secs))
532    }
533
534    quickcheck! {
535        // Weeks is total seconds divided by how many seconds
536        // are in a week
537        fn weeks_is_sec_over_sec_per_week(secs: u64) -> bool {
538            let b = breakdown_from_secs(secs);
539            b.weeks() == secs / SECONDS_PER_WEEK
540        }
541
542        // Days is whatever is left over after taking out weeks,
543        // divided by number of seconds in a day
544        fn days_is_leftover_sec_per_day(secs: u64) -> bool {
545            let b = breakdown_from_secs(secs);
546            b.days() == (secs % SECONDS_PER_WEEK) / SECONDS_PER_DAY
547        }
548
549        // Hours is whatever is left over after taking out days,
550        // divided by number of seconds in an hour
551        fn hours_is_leftover_sec_per_hour(secs: u64) -> bool {
552            let b = breakdown_from_secs(secs);
553            b.hours() == (secs % SECONDS_PER_DAY) / SECONDS_PER_HOUR
554        }
555
556        // Minutes is whatever is left over after taking out hours,
557        // divided by number of seconds in a minute
558        fn minutes_is_leftover_seconds_per_minute(secs: u64) -> bool {
559            let b = breakdown_from_secs(secs);
560            b.minutes() == (secs % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE
561        }
562
563        // Seconds is whatever is left over after taking out minutes.
564        fn seconds_is_leftover_sec(secs: u64) -> bool {
565            let b = breakdown_from_secs(secs);
566            b.seconds() == (secs % SECONDS_PER_MINUTE)
567        }
568
569        // Converting from a duration to a breakdown and back should
570        // yield the same duration.
571        fn conversions_work(secs: u64) -> bool {
572            let d = Duration::from_secs(secs);
573            Duration::from(DurationBreakdown::from(d)) == d
574        }
575    }
576}