1use std::fmt::{self, Display, Formatter};
2use std::ops::{Add, Div, Mul, Neg, Sub};
3
4use chrono::{DateTime, Duration as ChronoDuration, Utc};
5
6const SECONDS_PER_MINUTE: i64 = 60;
7const SECONDS_PER_HOUR: i64 = 60 * SECONDS_PER_MINUTE;
8const SECONDS_PER_DAY: i64 = 24 * SECONDS_PER_HOUR;
9const SECONDS_PER_WEEK: i64 = 7 * SECONDS_PER_DAY;
10const SECONDS_PER_MONTH: i64 = 2_629_746;
11const SECONDS_PER_YEAR: i64 = 31_556_952;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub enum DurationPart {
16 Seconds,
18 Minutes,
20 Hours,
22 Days,
24 Weeks,
26 Months,
28 Years,
30}
31
32#[derive(Debug, Clone, PartialEq)]
34pub struct Duration {
35 parts: Vec<(DurationPart, i64)>,
36 value: i64,
37}
38
39impl Duration {
40 #[must_use]
42 pub fn new(parts: Vec<(DurationPart, i64)>) -> Self {
43 let parts = normalize_parts(parts);
44 let value = parts
45 .iter()
46 .map(|(part, amount)| part_seconds(*part).saturating_mul(*amount))
47 .sum();
48
49 Self { parts, value }
50 }
51
52 #[must_use]
54 pub fn seconds(n: i64) -> Self {
55 Self::new(vec![(DurationPart::Seconds, n)])
56 }
57
58 #[must_use]
60 pub fn minutes(n: i64) -> Self {
61 Self::new(vec![(DurationPart::Minutes, n)])
62 }
63
64 #[must_use]
66 pub fn hours(n: i64) -> Self {
67 Self::new(vec![(DurationPart::Hours, n)])
68 }
69
70 #[must_use]
72 pub fn days(n: i64) -> Self {
73 Self::new(vec![(DurationPart::Days, n)])
74 }
75
76 #[must_use]
78 pub fn weeks(n: i64) -> Self {
79 Self::new(vec![(DurationPart::Weeks, n)])
80 }
81
82 #[must_use]
84 pub fn months(n: i64) -> Self {
85 Self::new(vec![(DurationPart::Months, n)])
86 }
87
88 #[must_use]
90 pub fn years(n: i64) -> Self {
91 Self::new(vec![(DurationPart::Years, n)])
92 }
93
94 #[must_use]
96 pub fn value(&self) -> i64 {
97 self.value
98 }
99
100 #[must_use]
102 pub fn parts(&self) -> &[(DurationPart, i64)] {
103 &self.parts
104 }
105
106 #[must_use]
108 pub fn in_seconds(&self) -> i64 {
109 self.value
110 }
111
112 #[must_use]
114 pub fn in_minutes(&self) -> f64 {
115 self.value as f64 / SECONDS_PER_MINUTE as f64
116 }
117
118 #[must_use]
120 pub fn in_hours(&self) -> f64 {
121 self.value as f64 / SECONDS_PER_HOUR as f64
122 }
123
124 #[must_use]
126 pub fn in_days(&self) -> f64 {
127 self.value as f64 / SECONDS_PER_DAY as f64
128 }
129
130 #[must_use]
132 pub fn ago(&self) -> DateTime<Utc> {
133 self.until(Utc::now())
134 }
135
136 #[must_use]
138 pub fn from_now(&self) -> DateTime<Utc> {
139 self.since(Utc::now())
140 }
141
142 #[must_use]
144 pub fn since(&self, time: DateTime<Utc>) -> DateTime<Utc> {
145 match time.checked_add_signed(ChronoDuration::seconds(self.value)) {
146 Some(result) => result,
147 None => time,
148 }
149 }
150
151 #[must_use]
153 pub fn until(&self, time: DateTime<Utc>) -> DateTime<Utc> {
154 match time.checked_sub_signed(ChronoDuration::seconds(self.value)) {
155 Some(result) => result,
156 None => time,
157 }
158 }
159
160 #[must_use]
162 pub fn iso8601(&self) -> String {
163 if self.parts.is_empty() {
164 return String::from("PT0S");
165 }
166
167 let mut years = 0;
168 let mut months = 0;
169 let mut weeks = 0;
170 let mut days = 0;
171 let mut hours = 0;
172 let mut minutes = 0;
173 let mut seconds = 0;
174
175 for (part, amount) in &self.parts {
176 match part {
177 DurationPart::Years => years += amount,
178 DurationPart::Months => months += amount,
179 DurationPart::Weeks => weeks += amount,
180 DurationPart::Days => days += amount,
181 DurationPart::Hours => hours += amount,
182 DurationPart::Minutes => minutes += amount,
183 DurationPart::Seconds => seconds += amount,
184 }
185 }
186
187 let mut result = String::from("P");
188 let has_other_date_parts = years != 0 || months != 0 || days != 0;
189
190 if weeks != 0 && !has_other_date_parts && hours == 0 && minutes == 0 && seconds == 0 {
191 result.push_str(&format_component(weeks, 'W'));
192 return result;
193 }
194
195 if weeks != 0 {
196 days += weeks * 7;
197 }
198
199 result.push_str(&format_component(years, 'Y'));
200 result.push_str(&format_component(months, 'M'));
201 result.push_str(&format_component(days, 'D'));
202
203 if hours != 0 || minutes != 0 || seconds != 0 {
204 result.push('T');
205 result.push_str(&format_component(hours, 'H'));
206 result.push_str(&format_component(minutes, 'M'));
207 result.push_str(&format_component(seconds, 'S'));
208 }
209
210 if result == "P" {
211 String::from("PT0S")
212 } else if result.ends_with('T') {
213 format!("{result}0S")
214 } else {
215 result
216 }
217 }
218
219 #[must_use]
223 pub fn to_std(&self) -> std::time::Duration {
224 if self.value <= 0 {
225 return std::time::Duration::ZERO;
226 }
227
228 std::time::Duration::from_secs(self.value as u64)
229 }
230}
231
232impl Add for Duration {
233 type Output = Duration;
234
235 fn add(self, rhs: Self) -> Self::Output {
236 let mut parts = self.parts;
237 parts.extend(rhs.parts);
238 Duration::new(parts)
239 }
240}
241
242impl Sub for Duration {
243 type Output = Duration;
244
245 fn sub(self, rhs: Self) -> Self::Output {
246 let mut parts = self.parts;
247 parts.extend(rhs.parts.into_iter().map(|(part, amount)| (part, -amount)));
248 Duration::new(parts)
249 }
250}
251
252impl Mul<i64> for Duration {
253 type Output = Duration;
254
255 fn mul(self, rhs: i64) -> Self::Output {
256 Duration::new(
257 self.parts
258 .into_iter()
259 .map(|(part, amount)| (part, amount.saturating_mul(rhs)))
260 .collect(),
261 )
262 }
263}
264
265impl Div<i64> for Duration {
266 type Output = Duration;
267
268 fn div(self, rhs: i64) -> Self::Output {
269 if rhs == 0 {
270 return Duration::seconds(0);
271 }
272
273 decompose_seconds(self.value / rhs)
274 }
275}
276
277impl Neg for Duration {
278 type Output = Duration;
279
280 fn neg(self) -> Self::Output {
281 Duration::new(
282 self.parts
283 .into_iter()
284 .map(|(part, amount)| (part, -amount))
285 .collect(),
286 )
287 }
288}
289
290impl Display for Duration {
291 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
292 if self.parts.is_empty() {
293 return write!(f, "0 seconds");
294 }
295
296 let segments: Vec<String> = self
297 .parts
298 .iter()
299 .map(|(part, amount)| {
300 let name = part_name(*part, amount.unsigned_abs() == 1);
301 format!("{amount} {name}")
302 })
303 .collect();
304
305 match segments.len() {
306 0 => write!(f, "0 seconds"),
307 1 => write!(f, "{}", segments[0]),
308 2 => write!(f, "{} and {}", segments[0], segments[1]),
309 _ => {
310 for segment in &segments[..segments.len() - 1] {
311 write!(f, "{segment}, ")?;
312 }
313 write!(f, "and {}", segments[segments.len() - 1])
314 }
315 }
316 }
317}
318
319fn normalize_parts(parts: Vec<(DurationPart, i64)>) -> Vec<(DurationPart, i64)> {
320 let mut normalized = Vec::new();
321
322 for part in ordered_parts() {
323 let total: i64 = parts
324 .iter()
325 .filter(|(candidate, _)| candidate == &part)
326 .map(|(_, amount)| *amount)
327 .sum();
328 if total != 0 {
329 normalized.push((part, total));
330 }
331 }
332
333 normalized
334}
335
336fn ordered_parts() -> [DurationPart; 7] {
337 [
338 DurationPart::Years,
339 DurationPart::Months,
340 DurationPart::Weeks,
341 DurationPart::Days,
342 DurationPart::Hours,
343 DurationPart::Minutes,
344 DurationPart::Seconds,
345 ]
346}
347
348fn part_seconds(part: DurationPart) -> i64 {
349 match part {
350 DurationPart::Seconds => 1,
351 DurationPart::Minutes => SECONDS_PER_MINUTE,
352 DurationPart::Hours => SECONDS_PER_HOUR,
353 DurationPart::Days => SECONDS_PER_DAY,
354 DurationPart::Weeks => SECONDS_PER_WEEK,
355 DurationPart::Months => SECONDS_PER_MONTH,
356 DurationPart::Years => SECONDS_PER_YEAR,
357 }
358}
359
360fn part_name(part: DurationPart, singular: bool) -> &'static str {
361 match (part, singular) {
362 (DurationPart::Seconds, true) => "second",
363 (DurationPart::Seconds, false) => "seconds",
364 (DurationPart::Minutes, true) => "minute",
365 (DurationPart::Minutes, false) => "minutes",
366 (DurationPart::Hours, true) => "hour",
367 (DurationPart::Hours, false) => "hours",
368 (DurationPart::Days, true) => "day",
369 (DurationPart::Days, false) => "days",
370 (DurationPart::Weeks, true) => "week",
371 (DurationPart::Weeks, false) => "weeks",
372 (DurationPart::Months, true) => "month",
373 (DurationPart::Months, false) => "months",
374 (DurationPart::Years, true) => "year",
375 (DurationPart::Years, false) => "years",
376 }
377}
378
379fn format_component(amount: i64, suffix: char) -> String {
380 if amount == 0 {
381 String::new()
382 } else {
383 format!("{amount}{suffix}")
384 }
385}
386
387fn decompose_seconds(total_seconds: i64) -> Duration {
388 if total_seconds == 0 {
389 return Duration::seconds(0) - Duration::seconds(0);
390 }
391
392 let sign = if total_seconds < 0 { -1 } else { 1 };
393 let mut remaining = total_seconds.unsigned_abs() as i64;
394 let mut parts = Vec::new();
395
396 for part in ordered_parts() {
397 let unit = part_seconds(part);
398 if remaining >= unit {
399 let amount = remaining / unit;
400 remaining %= unit;
401 parts.push((part, amount * sign));
402 }
403 }
404
405 Duration::new(parts)
406}
407
408#[cfg(test)]
409mod tests {
410 use super::*;
411
412 #[test]
413 fn constructs_seconds() {
414 let duration = Duration::seconds(5);
415 assert_eq!(duration.value(), 5);
416 assert_eq!(duration.parts(), &[(DurationPart::Seconds, 5)]);
417 }
418
419 #[test]
420 fn constructs_minutes() {
421 let duration = Duration::minutes(2);
422 assert_eq!(duration.value(), 120);
423 assert_eq!(duration.parts(), &[(DurationPart::Minutes, 2)]);
424 }
425
426 #[test]
427 fn constructs_hours() {
428 let duration = Duration::hours(3);
429 assert_eq!(duration.value(), 10_800);
430 assert_eq!(duration.parts(), &[(DurationPart::Hours, 3)]);
431 }
432
433 #[test]
434 fn constructs_days() {
435 let duration = Duration::days(2);
436 assert_eq!(duration.value(), 172_800);
437 assert_eq!(duration.parts(), &[(DurationPart::Days, 2)]);
438 }
439
440 #[test]
441 fn constructs_weeks() {
442 let duration = Duration::weeks(2);
443 assert_eq!(duration.value(), 1_209_600);
444 assert_eq!(duration.parts(), &[(DurationPart::Weeks, 2)]);
445 }
446
447 #[test]
448 fn constructs_months() {
449 let duration = Duration::months(1);
450 assert_eq!(duration.value(), SECONDS_PER_MONTH);
451 assert_eq!(duration.parts(), &[(DurationPart::Months, 1)]);
452 }
453
454 #[test]
455 fn constructs_years() {
456 let duration = Duration::years(1);
457 assert_eq!(duration.value(), SECONDS_PER_YEAR);
458 assert_eq!(duration.parts(), &[(DurationPart::Years, 1)]);
459 }
460
461 #[test]
462 fn addition_combines_parts() {
463 let duration = Duration::hours(1) + Duration::minutes(30);
464 assert_eq!(duration.value(), 5_400);
465 assert_eq!(
466 duration.parts(),
467 &[(DurationPart::Hours, 1), (DurationPart::Minutes, 30)]
468 );
469 }
470
471 #[test]
472 fn subtraction_negates_rhs_parts() {
473 let duration = Duration::days(2) - Duration::hours(12);
474 assert_eq!(duration.value(), 129_600);
475 assert_eq!(
476 duration.parts(),
477 &[(DurationPart::Days, 2), (DurationPart::Hours, -12)]
478 );
479 }
480
481 #[test]
482 fn multiplication_scales_parts() {
483 let duration = Duration::hours(2) * 3;
484 assert_eq!(duration, Duration::hours(6));
485 }
486
487 #[test]
488 fn division_rebuilds_from_seconds() {
489 let duration = Duration::days(1) / 24;
490 assert_eq!(duration, Duration::hours(1));
491 }
492
493 #[test]
494 fn division_by_zero_returns_zero_seconds() {
495 let duration = Duration::hours(1) / 0;
496 assert_eq!(duration.value(), 0);
497 assert!(duration.parts().is_empty());
498 }
499
500 #[test]
501 fn negation_negates_parts() {
502 let duration = -Duration::minutes(5);
503 assert_eq!(duration.value(), -300);
504 assert_eq!(duration.parts(), &[(DurationPart::Minutes, -5)]);
505 }
506
507 #[test]
508 fn conversions_return_expected_values() {
509 let duration = Duration::days(1);
510 assert_eq!(duration.in_seconds(), 86_400);
511 assert_eq!(duration.in_minutes(), 1_440.0);
512 assert_eq!(duration.in_hours(), 24.0);
513 assert_eq!(duration.in_days(), 1.0);
514 }
515
516 #[test]
517 fn composite_duration_retains_ordered_parts() {
518 let duration = Duration::minutes(30) + Duration::hours(1) + Duration::seconds(15);
519 assert_eq!(
520 duration.parts(),
521 &[
522 (DurationPart::Hours, 1),
523 (DurationPart::Minutes, 30),
524 (DurationPart::Seconds, 15),
525 ]
526 );
527 }
528
529 #[test]
530 fn zero_duration_formats_as_zero_seconds() {
531 let duration = Duration::new(Vec::new());
532 assert_eq!(duration.value(), 0);
533 assert_eq!(duration.to_string(), "0 seconds");
534 assert_eq!(duration.iso8601(), "PT0S");
535 }
536
537 #[test]
538 fn ago_is_approximately_in_the_past() {
539 let now = Utc::now();
540 let ago = Duration::seconds(1).ago();
541 let delta = now.signed_duration_since(ago).num_seconds();
542 assert!((0..=2).contains(&delta));
543 }
544
545 #[test]
546 fn from_now_is_approximately_in_the_future() {
547 let now = Utc::now();
548 let future = Duration::seconds(1).from_now();
549 let delta = future.signed_duration_since(now).num_seconds();
550 assert!((0..=2).contains(&delta));
551 }
552
553 #[test]
554 fn since_adds_seconds_to_time() {
555 let time = DateTime::from_timestamp(1_700_000_000, 0).expect("valid timestamp");
556 let result = Duration::minutes(2).since(time);
557 assert_eq!(result, time + ChronoDuration::minutes(2));
558 }
559
560 #[test]
561 fn until_subtracts_seconds_from_time() {
562 let time = DateTime::from_timestamp(1_700_000_000, 0).expect("valid timestamp");
563 let result = Duration::minutes(2).until(time);
564 assert_eq!(result, time - ChronoDuration::minutes(2));
565 }
566
567 #[test]
568 fn iso8601_formats_date_and_time_parts() {
569 let duration =
570 Duration::days(1) + Duration::hours(2) + Duration::minutes(3) + Duration::seconds(4);
571 assert_eq!(duration.iso8601(), "P1DT2H3M4S");
572 }
573
574 #[test]
575 fn iso8601_uses_weeks_when_alone() {
576 assert_eq!(Duration::weeks(1).iso8601(), "P1W");
577 }
578
579 #[test]
580 fn iso8601_converts_weeks_when_mixed() {
581 let duration = Duration::years(1) + Duration::weeks(1);
582 assert_eq!(duration.iso8601(), "P1Y7D");
583 }
584
585 #[test]
586 fn iso8601_preserves_negative_parts() {
587 let duration = Duration::months(6) - Duration::days(2);
588 assert_eq!(duration.iso8601(), "P6M-2D");
589 }
590
591 #[test]
592 fn to_std_converts_positive_values() {
593 assert_eq!(
594 Duration::seconds(5).to_std(),
595 std::time::Duration::from_secs(5)
596 );
597 }
598
599 #[test]
600 fn to_std_clamps_negative_values_to_zero() {
601 assert_eq!(Duration::seconds(-5).to_std(), std::time::Duration::ZERO);
602 }
603
604 #[test]
605 fn display_is_human_readable() {
606 let duration = Duration::years(1) + Duration::months(2) + Duration::days(1);
607 assert_eq!(duration.to_string(), "1 year, 2 months, and 1 day");
608 }
609
610 #[test]
611 fn display_handles_negative_parts() {
612 let duration = Duration::months(6) - Duration::days(2);
613 assert_eq!(duration.to_string(), "6 months and -2 days");
614 }
615
616 #[test]
617 fn new_normalizes_and_orders_parts() {
618 let duration = Duration::new(vec![
619 (DurationPart::Minutes, 2),
620 (DurationPart::Hours, 1),
621 (DurationPart::Minutes, -1),
622 (DurationPart::Seconds, 0),
623 ]);
624
625 assert_eq!(duration.value(), 3_660);
626 assert_eq!(
627 duration.parts(),
628 &[(DurationPart::Hours, 1), (DurationPart::Minutes, 1)]
629 );
630 assert_eq!(duration, Duration::hours(1) + Duration::minutes(1));
631 }
632
633 #[test]
634 fn zero_amount_parts_normalize_to_zero_duration() {
635 let duration = Duration::days(0);
636
637 assert_eq!(duration.value(), 0);
638 assert!(duration.parts().is_empty());
639 assert_eq!(duration.to_string(), "0 seconds");
640 assert_eq!(duration.iso8601(), "PT0S");
641 }
642
643 #[test]
644 fn multiplication_by_negative_scalar_negates_parts() {
645 let duration = Duration::days(2) * -3;
646
647 assert_eq!(duration.value(), -518_400);
648 assert_eq!(duration.parts(), &[(DurationPart::Days, -6)]);
649 }
650
651 #[test]
652 fn division_of_negative_duration_rebuilds_negative_parts() {
653 let duration = Duration::hours(-1) / 2;
654
655 assert_eq!(duration.value(), -1_800);
656 assert_eq!(duration.parts(), &[(DurationPart::Minutes, -30)]);
657 }
658
659 #[test]
660 fn since_and_until_handle_zero_and_negative_durations() {
661 let time = DateTime::from_timestamp(1_700_000_000, 0).expect("valid timestamp");
662
663 assert_eq!(Duration::seconds(0).since(time), time);
664 assert_eq!(Duration::seconds(0).until(time), time);
665
666 let duration = Duration::minutes(-2);
667 assert_eq!(duration.since(time), time - ChronoDuration::minutes(2));
668 assert_eq!(duration.until(time), time + ChronoDuration::minutes(2));
669 }
670
671 #[test]
672 fn iso8601_formats_year_month_and_negative_time_parts() {
673 let duration =
674 Duration::years(1) + Duration::months(1) + Duration::days(1) + Duration::hours(1);
675 assert_eq!(duration.iso8601(), "P1Y1M1DT1H");
676
677 let negative = Duration::years(1) - Duration::days(1) - Duration::seconds(1);
678 assert_eq!(negative.iso8601(), "P1Y-1DT-1S");
679 }
680
681 #[test]
682 fn display_formats_single_and_two_part_durations() {
683 assert_eq!(Duration::weeks(1).to_string(), "1 week");
684 assert_eq!(
685 (Duration::months(1) + Duration::days(1)).to_string(),
686 "1 month and 1 day"
687 );
688 }
689
690 #[test]
691 fn new_orders_parts_from_largest_to_smallest_units() {
692 let duration = Duration::new(vec![
693 (DurationPart::Seconds, 5),
694 (DurationPart::Years, 1),
695 (DurationPart::Days, 2),
696 ]);
697
698 assert_eq!(
699 duration.parts(),
700 &[
701 (DurationPart::Years, 1),
702 (DurationPart::Days, 2),
703 (DurationPart::Seconds, 5),
704 ]
705 );
706 }
707
708 #[test]
709 fn new_cancels_matching_parts_that_sum_to_zero() {
710 let duration = Duration::new(vec![
711 (DurationPart::Minutes, 5),
712 (DurationPart::Minutes, -5),
713 ]);
714
715 assert_eq!(duration.value(), 0);
716 assert!(duration.parts().is_empty());
717 }
718
719 #[test]
720 fn addition_normalizes_matching_parts() {
721 let duration = Duration::minutes(15) + Duration::minutes(45);
722
723 assert_eq!(duration.value(), 3_600);
724 assert_eq!(duration.parts(), &[(DurationPart::Minutes, 60)]);
725 }
726
727 #[test]
728 fn subtraction_normalizes_matching_parts() {
729 let duration = Duration::days(2) - Duration::days(1);
730
731 assert_eq!(duration.value(), 86_400);
732 assert_eq!(duration.parts(), &[(DurationPart::Days, 1)]);
733 }
734
735 #[test]
736 fn subtraction_of_identical_parts_produces_zero_duration() {
737 let duration = Duration::hours(1) - Duration::hours(1);
738
739 assert_eq!(duration.value(), 0);
740 assert!(duration.parts().is_empty());
741 }
742
743 #[test]
744 fn negation_flips_each_part_in_composite_duration() {
745 let duration = -(Duration::days(1) + Duration::hours(2));
746
747 assert_eq!(
748 duration.parts(),
749 &[(DurationPart::Days, -1), (DurationPart::Hours, -2)]
750 );
751 assert_eq!(duration.value(), -93_600);
752 }
753
754 #[test]
755 #[allow(clippy::erasing_op)]
756 fn multiplication_by_zero_returns_zero_duration() {
757 let duration = (Duration::days(1) + Duration::hours(2)) * 0;
758
759 assert_eq!(duration.value(), 0);
760 assert!(duration.parts().is_empty());
761 }
762
763 #[test]
764 fn multiplication_scales_composite_parts() {
765 let duration = (Duration::days(1) + Duration::hours(2)) * 2;
766
767 assert_eq!(
768 duration.parts(),
769 &[(DurationPart::Days, 2), (DurationPart::Hours, 4)]
770 );
771 assert_eq!(duration.value(), 187_200);
772 }
773
774 #[test]
775 fn division_truncates_fractional_seconds() {
776 let duration = Duration::seconds(5) / 2;
777
778 assert_eq!(duration.value(), 2);
779 assert_eq!(duration.parts(), &[(DurationPart::Seconds, 2)]);
780 }
781
782 #[test]
783 fn division_of_weeks_decomposes_into_days_and_hours() {
784 let duration = Duration::weeks(1) / 2;
785
786 assert_eq!(duration.value(), 302_400);
787 assert_eq!(
788 duration.parts(),
789 &[(DurationPart::Days, 3), (DurationPart::Hours, 12)]
790 );
791 }
792
793 #[test]
794 fn in_minutes_handles_negative_durations() {
795 assert_eq!(Duration::seconds(-90).in_minutes(), -1.5);
796 }
797
798 #[test]
799 fn in_hours_handles_partial_days() {
800 assert_eq!(Duration::minutes(90).in_hours(), 1.5);
801 }
802
803 #[test]
804 fn in_days_handles_negative_hours() {
805 assert_eq!(Duration::hours(-12).in_days(), -0.5);
806 }
807
808 #[test]
809 fn since_adds_composite_duration_value_to_time() {
810 let time = DateTime::from_timestamp(1_700_000_000, 0).expect("valid timestamp");
811 let duration = Duration::days(1) + Duration::minutes(90);
812
813 assert_eq!(duration.since(time), time + ChronoDuration::seconds(91_800));
814 }
815
816 #[test]
817 fn until_subtracts_composite_duration_value_from_time() {
818 let time = DateTime::from_timestamp(1_700_000_000, 0).expect("valid timestamp");
819 let duration = Duration::days(1) + Duration::minutes(90);
820
821 assert_eq!(duration.until(time), time - ChronoDuration::seconds(91_800));
822 }
823
824 #[test]
825 fn iso8601_formats_minutes_only() {
826 assert_eq!(Duration::minutes(5).iso8601(), "PT5M");
827 }
828
829 #[test]
830 fn iso8601_formats_negative_seconds_only() {
831 assert_eq!(Duration::seconds(-5).iso8601(), "PT-5S");
832 }
833
834 #[test]
835 fn iso8601_formats_negative_weeks_only() {
836 assert_eq!(Duration::weeks(-2).iso8601(), "P-2W");
837 }
838
839 #[test]
840 fn iso8601_converts_mixed_weeks_to_days() {
841 let duration = Duration::weeks(1) + Duration::days(2) + Duration::hours(3);
842
843 assert_eq!(duration.iso8601(), "P9DT3H");
844 }
845
846 #[test]
847 fn to_std_preserves_composite_positive_values() {
848 assert_eq!(
849 (Duration::days(1) + Duration::seconds(2)).to_std(),
850 std::time::Duration::from_secs(86_402)
851 );
852 }
853
854 #[test]
855 fn display_handles_negative_single_part_singular() {
856 assert_eq!(Duration::seconds(-1).to_string(), "-1 second");
857 }
858
859 #[test]
860 fn display_formats_three_part_durations_with_commas() {
861 assert_eq!(
862 (Duration::weeks(1) + Duration::days(2) + Duration::hours(3)).to_string(),
863 "1 week, 2 days, and 3 hours"
864 );
865 }
866
867 #[test]
868 fn equality_is_sensitive_to_representation_even_when_values_match() {
869 assert_eq!(Duration::minutes(60).value(), Duration::hours(1).value());
870 assert_ne!(Duration::minutes(60), Duration::hours(1));
871 }
872
873 #[test]
874 fn zero_duration_ago_and_from_now_stay_close_to_now() {
875 let now = Utc::now();
876 let past = Duration::seconds(0).ago();
877 let future = Duration::seconds(0).from_now();
878
879 assert!(now.signed_duration_since(past).num_seconds().abs() <= 1);
880 assert!(future.signed_duration_since(now).num_seconds().abs() <= 1);
881 }
882}