1use std::{
18 convert::{From, TryFrom},
19 fmt::{self, Display},
20 time::Duration,
21};
22
23const 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
30const 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#[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#[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 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 pub fn with_precision(&self, precision: Precision) -> Self {
122 let mut breakdown = *self;
124
125 macro_rules! zero_if_under_threshold {
126 ($field:ident, $precision:expr) => {
127 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 pub fn normalize(&mut self) {
162 macro_rules! propagate_overflow {
164 ($sub_unit:ident, $super_unit:ident, $sub_per_super:ident) => {
165 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 pub fn weeks(&self) -> u64 {
183 self.weeks
184 }
185
186 pub fn days(&self) -> u64 {
188 self.days
189 }
190
191 pub fn hours(&self) -> u64 {
193 self.hours
194 }
195
196 pub fn minutes(&self) -> u64 {
198 self.minutes
199 }
200
201 pub fn seconds(&self) -> u64 {
203 self.seconds
204 }
205
206 pub fn nanoseconds(&self) -> u64 {
208 self.nanoseconds
209 }
210
211 fn plural(quantity: u64) -> String {
213 (if quantity == 1 { "" } else { "s" }).to_string()
214 }
215
216 pub fn weeks_as_string(&self) -> String {
218 format!(
219 "{} week{}",
220 self.weeks,
221 DurationBreakdown::plural(self.weeks)
222 )
223 }
224
225 pub fn days_as_string(&self) -> String {
227 format!("{} day{}", self.days, DurationBreakdown::plural(self.days))
228 }
229
230 pub fn hours_as_string(&self) -> String {
232 format!(
233 "{} hour{}",
234 self.hours,
235 DurationBreakdown::plural(self.hours)
236 )
237 }
238
239 pub fn minutes_as_string(&self) -> String {
241 format!(
242 "{} minute{}",
243 self.minutes,
244 DurationBreakdown::plural(self.minutes)
245 )
246 }
247
248 pub fn seconds_as_string(&self) -> String {
250 format!(
251 "{} second{}",
252 self.seconds,
253 DurationBreakdown::plural(self.seconds)
254 )
255 }
256
257 pub fn nanoseconds_as_string(&self) -> String {
259 format!(
260 "{} nanosecond{}",
261 self.nanoseconds,
262 DurationBreakdown::plural(self.nanoseconds)
263 )
264 }
265
266 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 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 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 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 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 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 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 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 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 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 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 fn conversions_work(secs: u64) -> bool {
572 let d = Duration::from_secs(secs);
573 Duration::from(DurationBreakdown::from(d)) == d
574 }
575 }
576}