1use core::cmp::Ordering;
43use core::ops::{Add, AddAssign, Neg, Sub, SubAssign};
44
45use crate::qtty::time::TimeUnit;
46use crate::qtty::{self, Quantity, Second};
47
48pub const NANOS_PER_SECOND: i128 = 1_000_000_000;
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum DurationError {
54 Overflow,
56 NonFinite,
58 NonCanonical,
61}
62
63impl core::fmt::Display for DurationError {
64 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
65 match self {
66 Self::Overflow => f.write_str("ExactDuration arithmetic overflowed i128 nanoseconds"),
67 Self::NonFinite => f.write_str("ExactDuration input was NaN or infinite"),
68 Self::NonCanonical => f.write_str(
69 "ExactDuration (seconds, nanos) pair is non-canonical: \
70 signs must agree (seconds > 0 ⇒ nanos ≥ 0; seconds < 0 ⇒ nanos ≤ 0)",
71 ),
72 }
73 }
74}
75
76#[cfg(feature = "std")]
77impl std::error::Error for DurationError {}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
102pub struct ExactDuration {
103 nanos: i128,
104}
105
106impl ExactDuration {
107 pub const ZERO: Self = Self { nanos: 0 };
109
110 pub const NANOSECOND: Self = Self { nanos: 1 };
112
113 pub const SECOND: Self = Self {
115 nanos: NANOS_PER_SECOND,
116 };
117
118 pub const MAX: Self = Self { nanos: i128::MAX };
120
121 pub const MIN: Self = Self { nanos: i128::MIN };
123
124 #[inline]
126 pub const fn from_nanos(nanos: i128) -> Self {
127 Self { nanos }
128 }
129
130 #[inline]
139 pub const fn from_seconds_and_nanos(seconds: i64, nanos: i32) -> Result<Self, DurationError> {
140 let secs_nanos = (seconds as i128).wrapping_mul(NANOS_PER_SECOND);
145 match secs_nanos.checked_add(nanos as i128) {
146 Some(n) => Ok(Self { nanos: n }),
147 None => Err(DurationError::Overflow),
148 }
149 }
150
151 #[inline]
164 pub const fn from_canonical_seconds_nanos(
165 seconds: i64,
166 nanos: i32,
167 ) -> Result<Self, DurationError> {
168 if nanos <= -(NANOS_PER_SECOND as i32) || nanos >= NANOS_PER_SECOND as i32 {
169 return Err(DurationError::Overflow);
170 }
171 if (seconds > 0 && nanos < 0) || (seconds < 0 && nanos > 0) {
173 return Err(DurationError::NonCanonical);
174 }
175 let secs_nanos = (seconds as i128).wrapping_mul(NANOS_PER_SECOND);
176 match secs_nanos.checked_add(nanos as i128) {
177 Some(n) => Ok(Self { nanos: n }),
178 None => Err(DurationError::Overflow),
179 }
180 }
181
182 #[inline]
186 pub fn try_from_quantity<U: TimeUnit>(q: Quantity<U>) -> Result<Self, DurationError> {
187 let secs = q.to::<qtty::unit::Second>().value();
188 if !secs.is_finite() {
189 return Err(DurationError::NonFinite);
190 }
191 let nanos_f = secs * (NANOS_PER_SECOND as f64);
196 if nanos_f >= (i128::MAX as f64) || nanos_f <= (i128::MIN as f64) {
197 return Err(DurationError::Overflow);
198 }
199 Ok(Self {
200 nanos: nanos_f as i128,
201 })
202 }
203
204 #[inline]
208 pub fn from_quantity<U: TimeUnit>(q: Quantity<U>) -> Self {
209 Self::try_from_quantity(q).unwrap_or_else(|e| panic!("ExactDuration::from_quantity: {e}"))
210 }
211
212 #[inline]
216 pub fn from_seconds_f64_lossy(seconds: f64) -> Option<Self> {
217 if !seconds.is_finite() {
218 return None;
219 }
220 let nanos_f = seconds * (NANOS_PER_SECOND as f64);
221 if nanos_f >= (i128::MAX as f64) || nanos_f <= (i128::MIN as f64) {
222 return None;
223 }
224 Some(Self {
225 nanos: nanos_f as i128,
226 })
227 }
228
229 #[inline]
231 pub const fn as_nanos_i128(self) -> i128 {
232 self.nanos
233 }
234
235 #[inline]
246 pub const fn as_seconds_i64_nanos_checked(self) -> Result<(i64, i32), DurationError> {
247 let secs = self.nanos / NANOS_PER_SECOND;
248 let rem = (self.nanos - secs * NANOS_PER_SECOND) as i32;
249 if secs > i64::MAX as i128 || secs < i64::MIN as i128 {
250 Err(DurationError::Overflow)
251 } else {
252 Ok((secs as i64, rem))
253 }
254 }
255
256 #[inline]
265 pub const fn as_seconds_i64_nanos_saturating(self) -> (i64, i32) {
266 let secs = self.nanos / NANOS_PER_SECOND;
267 let rem = (self.nanos - secs * NANOS_PER_SECOND) as i32;
268 let secs_i64 = if secs > i64::MAX as i128 {
269 i64::MAX
270 } else if secs < i64::MIN as i128 {
271 i64::MIN
272 } else {
273 secs as i64
274 };
275 (secs_i64, rem)
276 }
277
278 #[inline]
285 pub const fn as_seconds_i64_nanos(self) -> (i64, i32) {
286 match self.as_seconds_i64_nanos_checked() {
287 Ok(pair) => pair,
288 Err(_) => panic!("ExactDuration::as_seconds_i64_nanos: seconds out of i64 range"),
289 }
290 }
291
292 #[inline]
294 pub fn as_seconds_f64(self) -> f64 {
295 (self.nanos as f64) / (NANOS_PER_SECOND as f64)
296 }
297
298 #[inline]
303 pub fn from_nanoseconds_i(nanos: crate::qtty::i64::Nanosecond) -> Self {
304 Self::from_nanos(nanos.value() as i128)
305 }
306
307 #[inline]
314 pub fn from_seconds_i(seconds: crate::qtty::i64::Second) -> Self {
315 Self::from_nanos(seconds.value() as i128 * NANOS_PER_SECOND)
316 }
317
318 #[inline]
323 pub fn as_nanoseconds_i(self) -> Result<crate::qtty::i64::Nanosecond, DurationError> {
324 if self.nanos > i64::MAX as i128 || self.nanos < i64::MIN as i128 {
325 Err(DurationError::Overflow)
326 } else {
327 Ok(crate::qtty::i64::Nanosecond::new(self.nanos as i64))
328 }
329 }
330
331 #[inline]
333 pub fn as_quantity<U: TimeUnit>(self) -> Quantity<U> {
334 Second::new(self.as_seconds_f64()).to::<U>()
335 }
336
337 #[inline]
339 pub const fn is_zero(self) -> bool {
340 self.nanos == 0
341 }
342
343 #[inline]
345 pub const fn is_negative(self) -> bool {
346 self.nanos < 0
347 }
348
349 #[inline]
352 pub const fn checked_abs(self) -> Result<Self, DurationError> {
353 match self.nanos.checked_abs() {
354 Some(n) => Ok(Self { nanos: n }),
355 None => Err(DurationError::Overflow),
356 }
357 }
358
359 #[inline]
361 pub const fn checked_add(self, rhs: Self) -> Result<Self, DurationError> {
362 match self.nanos.checked_add(rhs.nanos) {
363 Some(n) => Ok(Self { nanos: n }),
364 None => Err(DurationError::Overflow),
365 }
366 }
367
368 #[inline]
370 pub const fn checked_sub(self, rhs: Self) -> Result<Self, DurationError> {
371 match self.nanos.checked_sub(rhs.nanos) {
372 Some(n) => Ok(Self { nanos: n }),
373 None => Err(DurationError::Overflow),
374 }
375 }
376
377 #[inline]
379 pub const fn checked_neg(self) -> Result<Self, DurationError> {
380 match self.nanos.checked_neg() {
381 Some(n) => Ok(Self { nanos: n }),
382 None => Err(DurationError::Overflow),
383 }
384 }
385
386 #[inline]
388 pub const fn saturating_add(self, rhs: Self) -> Self {
389 Self {
390 nanos: self.nanos.saturating_add(rhs.nanos),
391 }
392 }
393
394 #[inline]
396 pub const fn saturating_sub(self, rhs: Self) -> Self {
397 Self {
398 nanos: self.nanos.saturating_sub(rhs.nanos),
399 }
400 }
401
402 #[inline]
407 pub const fn round_to(self, quantum: ExactDuration) -> Self {
408 let q = quantum.nanos;
409 if q <= 0 {
410 return self;
411 }
412 let n = self.nanos;
413 let div = n / q;
415 let rem = n - div * q;
416 let abs_rem = if rem < 0 { -rem } else { rem };
417 let half = q / 2;
418 let result = if abs_rem.saturating_mul(2) < q {
419 div
420 } else if abs_rem.saturating_mul(2) > q {
421 if n >= 0 {
422 div.saturating_add(1)
423 } else {
424 div.saturating_sub(1)
425 }
426 } else {
427 let _ = half;
429 if div % 2 == 0 {
430 div
431 } else if n >= 0 {
432 div.saturating_add(1)
433 } else {
434 div.saturating_sub(1)
435 }
436 };
437 Self {
438 nanos: result.saturating_mul(q),
439 }
440 }
441
442 #[inline]
444 pub const fn floor_to(self, quantum: ExactDuration) -> Self {
445 let q = quantum.nanos;
446 if q <= 0 {
447 return self;
448 }
449 let n = self.nanos;
450 let div = n / q;
451 let rem = n - div * q;
452 let floor_div = if rem < 0 { div.saturating_sub(1) } else { div };
453 Self {
454 nanos: floor_div.saturating_mul(q),
455 }
456 }
457
458 #[inline]
460 pub const fn ceil_to(self, quantum: ExactDuration) -> Self {
461 let q = quantum.nanos;
462 if q <= 0 {
463 return self;
464 }
465 let n = self.nanos;
466 let div = n / q;
467 let rem = n - div * q;
468 let ceil_div = if rem > 0 { div.saturating_add(1) } else { div };
469 Self {
470 nanos: ceil_div.saturating_mul(q),
471 }
472 }
473}
474
475impl Default for ExactDuration {
476 #[inline]
477 fn default() -> Self {
478 Self::ZERO
479 }
480}
481
482impl PartialOrd for ExactDuration {
483 #[inline]
484 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
485 Some(self.cmp(other))
486 }
487}
488
489impl Ord for ExactDuration {
490 #[inline]
491 fn cmp(&self, other: &Self) -> Ordering {
492 self.nanos.cmp(&other.nanos)
493 }
494}
495
496impl core::fmt::Display for ExactDuration {
497 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
498 match self.as_seconds_i64_nanos_checked() {
499 Ok((s, n)) => {
500 if n == 0 {
501 write!(f, "{s} s")
502 } else if s == 0 {
503 if n < 0 {
504 write!(f, "-0.{:09} s", -n)
505 } else {
506 write!(f, "0.{:09} s", n)
507 }
508 } else {
509 write!(f, "{s}.{:09} s", n.abs())
510 }
511 }
512 Err(_) => {
513 write!(f, "{} ns", self.nanos)
515 }
516 }
517 }
518}
519
520impl Add for ExactDuration {
524 type Output = Self;
525 #[inline]
526 fn add(self, rhs: Self) -> Self {
527 self.checked_add(rhs)
528 .expect("ExactDuration::add overflowed i128 ns")
529 }
530}
531
532impl Sub for ExactDuration {
533 type Output = Self;
534 #[inline]
535 fn sub(self, rhs: Self) -> Self {
536 self.checked_sub(rhs)
537 .expect("ExactDuration::sub overflowed i128 ns")
538 }
539}
540
541impl Neg for ExactDuration {
542 type Output = Self;
543 #[inline]
544 fn neg(self) -> Self {
545 self.checked_neg()
546 .expect("ExactDuration::neg overflowed i128 ns")
547 }
548}
549
550impl AddAssign for ExactDuration {
551 #[inline]
552 fn add_assign(&mut self, rhs: Self) {
553 *self = *self + rhs;
554 }
555}
556
557impl SubAssign for ExactDuration {
558 #[inline]
559 fn sub_assign(&mut self, rhs: Self) {
560 *self = *self - rhs;
561 }
562}
563
564#[cfg(feature = "serde")]
565mod serde_impl {
566 use super::ExactDuration;
567 use serde::{Deserialize, Deserializer, Serialize, Serializer};
568
569 #[derive(Serialize, Deserialize)]
570 struct Boundary {
571 sec: i64,
572 ns: i32,
573 }
574
575 impl Serialize for ExactDuration {
576 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
577 let (sec, ns) = self
578 .as_seconds_i64_nanos_checked()
579 .map_err(|e| serde::ser::Error::custom(e.to_string()))?;
580 Boundary { sec, ns }.serialize(serializer)
581 }
582 }
583
584 impl<'de> Deserialize<'de> for ExactDuration {
585 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
586 let b = Boundary::deserialize(deserializer)?;
587 ExactDuration::from_canonical_seconds_nanos(b.sec, b.ns)
588 .map_err(|e| serde::de::Error::custom(e.to_string()))
589 }
590 }
591}
592
593#[cfg(test)]
594mod tests {
595 use super::*;
596
597 #[test]
598 fn zero_and_constants() {
599 assert_eq!(ExactDuration::ZERO.as_nanos_i128(), 0);
600 assert_eq!(ExactDuration::NANOSECOND.as_nanos_i128(), 1);
601 assert_eq!(ExactDuration::SECOND.as_nanos_i128(), NANOS_PER_SECOND);
602 assert!(ExactDuration::ZERO.is_zero());
603 assert!(!ExactDuration::SECOND.is_negative());
604 assert!((-ExactDuration::SECOND).is_negative());
605 }
606
607 #[test]
608 fn from_seconds_and_nanos_signs() {
609 let half_neg = ExactDuration::from_seconds_and_nanos(-1, 500_000_000).unwrap();
610 assert_eq!(half_neg.as_nanos_i128(), -NANOS_PER_SECOND + 500_000_000);
611 let half_pos = ExactDuration::from_seconds_and_nanos(0, 500_000_000).unwrap();
612 assert_eq!(half_pos.as_nanos_i128(), 500_000_000);
613 }
614
615 #[test]
616 fn boundary_projection_round_trip() {
617 for nanos in [
618 0_i128,
619 1,
620 -1,
621 NANOS_PER_SECOND,
622 -NANOS_PER_SECOND,
623 1_234_567_890,
624 -9_876_543_210,
625 ] {
626 let d = ExactDuration::from_nanos(nanos);
627 let (s, n) = d.as_seconds_i64_nanos();
628 let recovered = (s as i128) * NANOS_PER_SECOND + n as i128;
629 assert_eq!(recovered, nanos, "round trip failed for {nanos}");
630 }
631 }
632
633 #[test]
634 fn neg_round_trip_and_min_overflow() {
635 let d = ExactDuration::from_nanos(1_500_000_000);
636 assert_eq!((-(-d)), d);
637 assert!(matches!(
638 ExactDuration::MIN.checked_neg(),
639 Err(DurationError::Overflow)
640 ));
641 }
642
643 #[test]
644 fn ordering_matches_i128() {
645 let a = ExactDuration::from_nanos(-5);
646 let b = ExactDuration::from_nanos(0);
647 let c = ExactDuration::from_nanos(5);
648 assert!(a < b && b < c);
649 assert_eq!(a.cmp(&a), Ordering::Equal);
650 }
651
652 #[test]
653 fn checked_add_sub_overflow() {
654 assert_eq!(
655 ExactDuration::MAX.checked_add(ExactDuration::NANOSECOND),
656 Err(DurationError::Overflow)
657 );
658 assert_eq!(
659 ExactDuration::MIN.checked_sub(ExactDuration::NANOSECOND),
660 Err(DurationError::Overflow)
661 );
662 assert_eq!(
663 ExactDuration::ZERO
664 .checked_add(ExactDuration::SECOND)
665 .unwrap(),
666 ExactDuration::SECOND
667 );
668 }
669
670 #[test]
671 fn saturating_add_sub() {
672 assert_eq!(
673 ExactDuration::MAX.saturating_add(ExactDuration::SECOND),
674 ExactDuration::MAX
675 );
676 assert_eq!(
677 ExactDuration::MIN.saturating_sub(ExactDuration::SECOND),
678 ExactDuration::MIN
679 );
680 }
681
682 #[test]
683 fn quantity_round_trip_within_mantissa() {
684 let q = Second::new(123.456_789_012_345);
685 let d = ExactDuration::try_from_quantity(q).unwrap();
686 let back = d.as_quantity::<qtty::unit::Second>();
687 assert!((back.value() - q.value()).abs() < 1e-9);
688 }
689
690 #[test]
691 fn quantity_non_finite_errors() {
692 assert_eq!(
693 ExactDuration::try_from_quantity(Second::new(f64::NAN)),
694 Err(DurationError::NonFinite)
695 );
696 assert_eq!(
697 ExactDuration::try_from_quantity(Second::new(f64::INFINITY)),
698 Err(DurationError::NonFinite)
699 );
700 }
701
702 #[test]
703 fn quantity_overflow_errors() {
704 let q = Second::new(1.0e30);
707 assert_eq!(
708 ExactDuration::try_from_quantity(q),
709 Err(DurationError::Overflow)
710 );
711 }
712
713 #[test]
714 fn quantity_unit_conversion() {
715 let ms = Quantity::<qtty::unit::Millisecond>::new(1500.0);
716 let d = ExactDuration::try_from_quantity(ms).unwrap();
717 assert_eq!(d.as_nanos_i128(), 1_500_000_000);
718
719 let day = Quantity::<qtty::unit::Day>::new(1.0);
720 let d2 = ExactDuration::try_from_quantity(day).unwrap();
721 assert_eq!(d2.as_nanos_i128(), 86_400 * NANOS_PER_SECOND);
722 }
723
724 #[test]
725 fn from_seconds_f64_lossy_handles_edges() {
726 assert!(ExactDuration::from_seconds_f64_lossy(f64::NAN).is_none());
727 assert!(ExactDuration::from_seconds_f64_lossy(f64::INFINITY).is_none());
728 assert_eq!(
729 ExactDuration::from_seconds_f64_lossy(1.5)
730 .unwrap()
731 .as_nanos_i128(),
732 1_500_000_000
733 );
734 }
735
736 #[test]
737 fn display_basic() {
738 assert_eq!(ExactDuration::SECOND.to_string(), "1 s");
739 assert_eq!(ExactDuration::from_nanos(0).to_string(), "0 s");
740 assert_eq!(
741 ExactDuration::from_seconds_and_nanos(3, 250_000_000)
742 .unwrap()
743 .to_string(),
744 "3.250000000 s"
745 );
746 }
747
748 #[test]
749 fn add_sub_neg_operators() {
750 let a = ExactDuration::SECOND;
751 let b = ExactDuration::NANOSECOND;
752 assert_eq!((a + b).as_nanos_i128(), 1_000_000_001);
753 assert_eq!((a - b).as_nanos_i128(), 999_999_999);
754 assert_eq!((-a).as_nanos_i128(), -1_000_000_000);
755
756 let mut c = a;
757 c += b;
758 assert_eq!(c.as_nanos_i128(), 1_000_000_001);
759 c -= b;
760 assert_eq!(c.as_nanos_i128(), 1_000_000_000);
761 }
762
763 #[test]
764 #[should_panic(expected = "overflowed")]
765 fn add_panics_on_overflow() {
766 let _ = ExactDuration::MAX + ExactDuration::NANOSECOND;
767 }
768
769 #[test]
770 fn checked_abs_works() {
771 assert_eq!(
772 ExactDuration::from_nanos(-5)
773 .checked_abs()
774 .unwrap()
775 .as_nanos_i128(),
776 5
777 );
778 assert!(matches!(
779 ExactDuration::MIN.checked_abs(),
780 Err(DurationError::Overflow)
781 ));
782 }
783
784 #[cfg(feature = "serde")]
785 #[test]
786 fn serde_round_trip() {
787 let cases = [0_i128, 1, -1, 1_500_000_000, -2_345_678_901];
788 for n in cases {
789 let d = ExactDuration::from_nanos(n);
790 let s = serde_json::to_string(&d).unwrap();
791 let back: ExactDuration = serde_json::from_str(&s).unwrap();
792 assert_eq!(back, d, "serde round-trip {n}");
793 }
794 }
795
796 #[test]
797 fn floor_ceil_round_basic() {
798 let q = ExactDuration::from_nanos(1_000_000_000); assert_eq!(
800 ExactDuration::from_nanos(1_500_000_000)
801 .floor_to(q)
802 .as_nanos_i128(),
803 1_000_000_000
804 );
805 assert_eq!(
806 ExactDuration::from_nanos(1_500_000_000)
807 .ceil_to(q)
808 .as_nanos_i128(),
809 2_000_000_000
810 );
811 assert_eq!(
813 ExactDuration::from_nanos(1_500_000_000)
814 .round_to(q)
815 .as_nanos_i128(),
816 2_000_000_000
817 );
818 assert_eq!(
819 ExactDuration::from_nanos(2_500_000_000)
820 .round_to(q)
821 .as_nanos_i128(),
822 2_000_000_000
823 );
824 assert_eq!(
825 ExactDuration::from_nanos(500_000_000)
826 .round_to(q)
827 .as_nanos_i128(),
828 0
829 );
830 }
831
832 #[test]
833 fn floor_ceil_round_negative() {
834 let q = ExactDuration::from_nanos(1_000_000_000);
835 let n = ExactDuration::from_nanos(-1_500_000_000);
837 assert_eq!(n.floor_to(q).as_nanos_i128(), -2_000_000_000);
838 assert_eq!(n.ceil_to(q).as_nanos_i128(), -1_000_000_000);
839 assert_eq!(n.round_to(q).as_nanos_i128(), -2_000_000_000);
841 }
842
843 #[test]
844 fn round_with_non_positive_quantum_is_identity() {
845 let n = ExactDuration::from_nanos(123);
846 assert_eq!(n.round_to(ExactDuration::ZERO), n);
847 assert_eq!(n.floor_to(ExactDuration::from_nanos(-1)), n);
848 assert_eq!(n.ceil_to(ExactDuration::ZERO), n);
849 }
850
851 #[test]
852 fn round_floor_ceil_saturate_at_extremes() {
853 let q = ExactDuration::SECOND;
854 let near_max = ExactDuration::MAX;
856 let _ = near_max.round_to(q);
857 let _ = near_max.floor_to(q);
858 let _ = near_max.ceil_to(q);
859 let near_min = ExactDuration::MIN;
860 let _ = near_min.round_to(q);
861 let _ = near_min.floor_to(q);
862 let _ = near_min.ceil_to(q);
863 }
864
865 #[test]
866 #[should_panic(expected = "ExactDuration::from_quantity")]
867 fn from_quantity_panics_on_nan() {
868 let _ = ExactDuration::from_quantity(Second::new(f64::NAN));
869 }
870
871 #[test]
872 #[should_panic(expected = "ExactDuration::from_quantity")]
873 fn from_quantity_panics_on_overflow() {
874 let _ = ExactDuration::from_quantity(Second::new(1.0e40));
875 }
876
877 #[cfg(feature = "serde")]
878 #[test]
879 fn serde_serialize_fails_on_out_of_range() {
880 let huge = ExactDuration::MAX;
882 let result = serde_json::to_string(&huge);
883 assert!(
884 result.is_err(),
885 "expected serde error for out-of-range duration"
886 );
887 }
888
889 #[test]
890 fn checked_projection_overflow_on_max() {
891 assert_eq!(
892 ExactDuration::MAX.as_seconds_i64_nanos_checked(),
893 Err(DurationError::Overflow)
894 );
895 assert_eq!(
896 ExactDuration::MIN.as_seconds_i64_nanos_checked(),
897 Err(DurationError::Overflow)
898 );
899 }
900
901 #[test]
902 fn checked_projection_small_round_trips() {
903 for nanos in [0_i128, 1, -1, 999_999_999, -999_999_999, 1_500_000_000] {
904 let d = ExactDuration::from_nanos(nanos);
905 let (s, n) = d.as_seconds_i64_nanos_checked().unwrap();
906 let recovered = (s as i128) * NANOS_PER_SECOND + n as i128;
907 assert_eq!(recovered, nanos, "checked round-trip failed for {nanos}");
908 }
909 }
910
911 #[test]
912 fn saturating_projection_extremes() {
913 let (s_max, _) = ExactDuration::MAX.as_seconds_i64_nanos_saturating();
914 assert_eq!(s_max, i64::MAX);
915 let (s_min, _) = ExactDuration::MIN.as_seconds_i64_nanos_saturating();
916 assert_eq!(s_min, i64::MIN);
917 }
918
919 #[test]
920 #[should_panic(expected = "as_seconds_i64_nanos: seconds out of i64 range")]
921 fn panicking_projection_panics_on_max() {
922 let _ = ExactDuration::MAX.as_seconds_i64_nanos();
923 }
924
925 #[test]
926 fn canonical_constructor_validates_nanos() {
927 assert!(ExactDuration::from_canonical_seconds_nanos(5, 0).is_ok());
929 assert!(ExactDuration::from_canonical_seconds_nanos(0, 999_999_999).is_ok());
930 assert!(ExactDuration::from_canonical_seconds_nanos(-1, -999_999_999).is_ok());
931 assert!(ExactDuration::from_canonical_seconds_nanos(0, 0).is_ok());
932 assert!(ExactDuration::from_canonical_seconds_nanos(0, -999_999_999).is_ok());
933 assert_eq!(
935 ExactDuration::from_canonical_seconds_nanos(0, 1_000_000_000),
936 Err(DurationError::Overflow)
937 );
938 assert_eq!(
939 ExactDuration::from_canonical_seconds_nanos(0, -1_000_000_000),
940 Err(DurationError::Overflow)
941 );
942 assert_eq!(
944 ExactDuration::from_canonical_seconds_nanos(1, -1),
945 Err(DurationError::NonCanonical)
946 );
947 assert_eq!(
948 ExactDuration::from_canonical_seconds_nanos(-1, 1),
949 Err(DurationError::NonCanonical)
950 );
951 assert_eq!(
952 ExactDuration::from_canonical_seconds_nanos(100, -500_000_000),
953 Err(DurationError::NonCanonical)
954 );
955 assert_eq!(
956 ExactDuration::from_canonical_seconds_nanos(-100, 500_000_000),
957 Err(DurationError::NonCanonical)
958 );
959 }
960
961 #[test]
962 fn display_extreme_falls_back_to_raw_nanos() {
963 let s = ExactDuration::MAX.to_string();
965 assert!(s.contains("ns"), "expected raw-ns fallback, got: {s}");
966 }
967
968 #[cfg(feature = "serde")]
969 #[test]
970 fn serde_rejects_non_canonical_pairs() {
971 let r: Result<ExactDuration, _> = serde_json::from_str(r#"{"sec":0,"ns":1000000000}"#);
973 assert!(r.is_err(), "expected Err for ns=1e9, got {:?}", r);
974
975 let r: Result<ExactDuration, _> = serde_json::from_str(r#"{"sec":0,"ns":-1000000000}"#);
977 assert!(r.is_err(), "expected Err for ns=-1e9, got {:?}", r);
978
979 let r: Result<ExactDuration, _> = serde_json::from_str(r#"{"sec":1,"ns":-1}"#);
981 assert!(r.is_err(), "expected Err for sec=1,ns=-1, got {:?}", r);
982
983 let r: Result<ExactDuration, _> = serde_json::from_str(r#"{"sec":-1,"ns":1}"#);
985 assert!(r.is_err(), "expected Err for sec=-1,ns=1, got {:?}", r);
986 }
987
988 #[test]
989 fn qtty_integer_nanosecond_round_trip() {
990 let d = ExactDuration::from_nanos(123_456_789);
992 let q = d.as_nanoseconds_i().unwrap();
993 assert_eq!(q.value(), 123_456_789_i64);
994 let back = ExactDuration::from_nanoseconds_i(q);
995 assert_eq!(back, d);
996
997 let d2 = ExactDuration::from_nanos(-999_000_000);
999 let q2 = d2.as_nanoseconds_i().unwrap();
1000 assert_eq!(q2.value(), -999_000_000_i64);
1001 assert_eq!(ExactDuration::from_nanoseconds_i(q2), d2);
1002
1003 let q0 = ExactDuration::ZERO.as_nanoseconds_i().unwrap();
1005 assert_eq!(q0.value(), 0_i64);
1006 }
1007
1008 #[test]
1009 fn qtty_integer_nanosecond_overflow() {
1010 assert_eq!(
1012 ExactDuration::MAX.as_nanoseconds_i(),
1013 Err(DurationError::Overflow)
1014 );
1015 assert_eq!(
1016 ExactDuration::MIN.as_nanoseconds_i(),
1017 Err(DurationError::Overflow)
1018 );
1019 }
1020}