1use core::cmp::Ordering;
43use core::ops::{Add, AddAssign, Neg, Sub, SubAssign};
44
45use qtty::time::TimeUnit;
46use qtty::unit::Second as SecondUnit;
47use qtty::{Quantity, Second};
48
49pub const NANOS_PER_SECOND: i128 = 1_000_000_000;
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum DurationError {
55 Overflow,
57 NonFinite,
59 NonCanonical,
62}
63
64impl core::fmt::Display for DurationError {
65 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
66 match self {
67 Self::Overflow => f.write_str("ExactDuration arithmetic overflowed i128 nanoseconds"),
68 Self::NonFinite => f.write_str("ExactDuration input was NaN or infinite"),
69 Self::NonCanonical => f.write_str(
70 "ExactDuration (seconds, nanos) pair is non-canonical: \
71 signs must agree (seconds > 0 ⇒ nanos ≥ 0; seconds < 0 ⇒ nanos ≤ 0)",
72 ),
73 }
74 }
75}
76
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::<SecondUnit>().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: qtty::i64::Nanosecond) -> Self {
304 Self::from_nanos(nanos.value() as i128)
305 }
306
307 #[inline]
314 pub fn from_seconds_i(seconds: 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<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(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 use qtty::unit::{Day as DayUnit, Millisecond as MsUnit};
597
598 #[test]
599 fn zero_and_constants() {
600 assert_eq!(ExactDuration::ZERO.as_nanos_i128(), 0);
601 assert_eq!(ExactDuration::NANOSECOND.as_nanos_i128(), 1);
602 assert_eq!(ExactDuration::SECOND.as_nanos_i128(), NANOS_PER_SECOND);
603 assert!(ExactDuration::ZERO.is_zero());
604 assert!(!ExactDuration::SECOND.is_negative());
605 assert!((-ExactDuration::SECOND).is_negative());
606 }
607
608 #[test]
609 fn from_seconds_and_nanos_signs() {
610 let half_neg = ExactDuration::from_seconds_and_nanos(-1, 500_000_000).unwrap();
611 assert_eq!(half_neg.as_nanos_i128(), -NANOS_PER_SECOND + 500_000_000);
612 let half_pos = ExactDuration::from_seconds_and_nanos(0, 500_000_000).unwrap();
613 assert_eq!(half_pos.as_nanos_i128(), 500_000_000);
614 }
615
616 #[test]
617 fn boundary_projection_round_trip() {
618 for nanos in [
619 0_i128,
620 1,
621 -1,
622 NANOS_PER_SECOND,
623 -NANOS_PER_SECOND,
624 1_234_567_890,
625 -9_876_543_210,
626 ] {
627 let d = ExactDuration::from_nanos(nanos);
628 let (s, n) = d.as_seconds_i64_nanos();
629 let recovered = (s as i128) * NANOS_PER_SECOND + n as i128;
630 assert_eq!(recovered, nanos, "round trip failed for {nanos}");
631 }
632 }
633
634 #[test]
635 fn neg_round_trip_and_min_overflow() {
636 let d = ExactDuration::from_nanos(1_500_000_000);
637 assert_eq!((-(-d)), d);
638 assert!(matches!(
639 ExactDuration::MIN.checked_neg(),
640 Err(DurationError::Overflow)
641 ));
642 }
643
644 #[test]
645 fn ordering_matches_i128() {
646 let a = ExactDuration::from_nanos(-5);
647 let b = ExactDuration::from_nanos(0);
648 let c = ExactDuration::from_nanos(5);
649 assert!(a < b && b < c);
650 assert_eq!(a.cmp(&a), Ordering::Equal);
651 }
652
653 #[test]
654 fn checked_add_sub_overflow() {
655 assert_eq!(
656 ExactDuration::MAX.checked_add(ExactDuration::NANOSECOND),
657 Err(DurationError::Overflow)
658 );
659 assert_eq!(
660 ExactDuration::MIN.checked_sub(ExactDuration::NANOSECOND),
661 Err(DurationError::Overflow)
662 );
663 assert_eq!(
664 ExactDuration::ZERO
665 .checked_add(ExactDuration::SECOND)
666 .unwrap(),
667 ExactDuration::SECOND
668 );
669 }
670
671 #[test]
672 fn saturating_add_sub() {
673 assert_eq!(
674 ExactDuration::MAX.saturating_add(ExactDuration::SECOND),
675 ExactDuration::MAX
676 );
677 assert_eq!(
678 ExactDuration::MIN.saturating_sub(ExactDuration::SECOND),
679 ExactDuration::MIN
680 );
681 }
682
683 #[test]
684 fn quantity_round_trip_within_mantissa() {
685 let q = Second::new(123.456_789_012_345);
686 let d = ExactDuration::try_from_quantity(q).unwrap();
687 let back = d.as_quantity::<SecondUnit>();
688 assert!((back.value() - q.value()).abs() < 1e-9);
689 }
690
691 #[test]
692 fn quantity_non_finite_errors() {
693 assert_eq!(
694 ExactDuration::try_from_quantity(Second::new(f64::NAN)),
695 Err(DurationError::NonFinite)
696 );
697 assert_eq!(
698 ExactDuration::try_from_quantity(Second::new(f64::INFINITY)),
699 Err(DurationError::NonFinite)
700 );
701 }
702
703 #[test]
704 fn quantity_overflow_errors() {
705 let q = Second::new(1.0e30);
708 assert_eq!(
709 ExactDuration::try_from_quantity(q),
710 Err(DurationError::Overflow)
711 );
712 }
713
714 #[test]
715 fn quantity_unit_conversion() {
716 let ms = Quantity::<MsUnit>::new(1500.0);
717 let d = ExactDuration::try_from_quantity(ms).unwrap();
718 assert_eq!(d.as_nanos_i128(), 1_500_000_000);
719
720 let day = Quantity::<DayUnit>::new(1.0);
721 let d2 = ExactDuration::try_from_quantity(day).unwrap();
722 assert_eq!(d2.as_nanos_i128(), 86_400 * NANOS_PER_SECOND);
723 }
724
725 #[test]
726 fn from_seconds_f64_lossy_handles_edges() {
727 assert!(ExactDuration::from_seconds_f64_lossy(f64::NAN).is_none());
728 assert!(ExactDuration::from_seconds_f64_lossy(f64::INFINITY).is_none());
729 assert_eq!(
730 ExactDuration::from_seconds_f64_lossy(1.5)
731 .unwrap()
732 .as_nanos_i128(),
733 1_500_000_000
734 );
735 }
736
737 #[test]
738 fn display_basic() {
739 assert_eq!(ExactDuration::SECOND.to_string(), "1 s");
740 assert_eq!(ExactDuration::from_nanos(0).to_string(), "0 s");
741 assert_eq!(
742 ExactDuration::from_seconds_and_nanos(3, 250_000_000)
743 .unwrap()
744 .to_string(),
745 "3.250000000 s"
746 );
747 }
748
749 #[test]
750 fn add_sub_neg_operators() {
751 let a = ExactDuration::SECOND;
752 let b = ExactDuration::NANOSECOND;
753 assert_eq!((a + b).as_nanos_i128(), 1_000_000_001);
754 assert_eq!((a - b).as_nanos_i128(), 999_999_999);
755 assert_eq!((-a).as_nanos_i128(), -1_000_000_000);
756
757 let mut c = a;
758 c += b;
759 assert_eq!(c.as_nanos_i128(), 1_000_000_001);
760 c -= b;
761 assert_eq!(c.as_nanos_i128(), 1_000_000_000);
762 }
763
764 #[test]
765 #[should_panic(expected = "overflowed")]
766 fn add_panics_on_overflow() {
767 let _ = ExactDuration::MAX + ExactDuration::NANOSECOND;
768 }
769
770 #[test]
771 fn checked_abs_works() {
772 assert_eq!(
773 ExactDuration::from_nanos(-5)
774 .checked_abs()
775 .unwrap()
776 .as_nanos_i128(),
777 5
778 );
779 assert!(matches!(
780 ExactDuration::MIN.checked_abs(),
781 Err(DurationError::Overflow)
782 ));
783 }
784
785 #[cfg(feature = "serde")]
786 #[test]
787 fn serde_round_trip() {
788 let cases = [0_i128, 1, -1, 1_500_000_000, -2_345_678_901];
789 for n in cases {
790 let d = ExactDuration::from_nanos(n);
791 let s = serde_json::to_string(&d).unwrap();
792 let back: ExactDuration = serde_json::from_str(&s).unwrap();
793 assert_eq!(back, d, "serde round-trip {n}");
794 }
795 }
796
797 #[test]
798 fn floor_ceil_round_basic() {
799 let q = ExactDuration::from_nanos(1_000_000_000); assert_eq!(
801 ExactDuration::from_nanos(1_500_000_000)
802 .floor_to(q)
803 .as_nanos_i128(),
804 1_000_000_000
805 );
806 assert_eq!(
807 ExactDuration::from_nanos(1_500_000_000)
808 .ceil_to(q)
809 .as_nanos_i128(),
810 2_000_000_000
811 );
812 assert_eq!(
814 ExactDuration::from_nanos(1_500_000_000)
815 .round_to(q)
816 .as_nanos_i128(),
817 2_000_000_000
818 );
819 assert_eq!(
820 ExactDuration::from_nanos(2_500_000_000)
821 .round_to(q)
822 .as_nanos_i128(),
823 2_000_000_000
824 );
825 assert_eq!(
826 ExactDuration::from_nanos(500_000_000)
827 .round_to(q)
828 .as_nanos_i128(),
829 0
830 );
831 }
832
833 #[test]
834 fn floor_ceil_round_negative() {
835 let q = ExactDuration::from_nanos(1_000_000_000);
836 let n = ExactDuration::from_nanos(-1_500_000_000);
838 assert_eq!(n.floor_to(q).as_nanos_i128(), -2_000_000_000);
839 assert_eq!(n.ceil_to(q).as_nanos_i128(), -1_000_000_000);
840 assert_eq!(n.round_to(q).as_nanos_i128(), -2_000_000_000);
842 }
843
844 #[test]
845 fn round_with_non_positive_quantum_is_identity() {
846 let n = ExactDuration::from_nanos(123);
847 assert_eq!(n.round_to(ExactDuration::ZERO), n);
848 assert_eq!(n.floor_to(ExactDuration::from_nanos(-1)), n);
849 assert_eq!(n.ceil_to(ExactDuration::ZERO), n);
850 }
851
852 #[test]
853 fn round_floor_ceil_saturate_at_extremes() {
854 let q = ExactDuration::SECOND;
855 let near_max = ExactDuration::MAX;
857 let _ = near_max.round_to(q);
858 let _ = near_max.floor_to(q);
859 let _ = near_max.ceil_to(q);
860 let near_min = ExactDuration::MIN;
861 let _ = near_min.round_to(q);
862 let _ = near_min.floor_to(q);
863 let _ = near_min.ceil_to(q);
864 }
865
866 #[test]
867 #[should_panic(expected = "ExactDuration::from_quantity")]
868 fn from_quantity_panics_on_nan() {
869 let _ = ExactDuration::from_quantity(Second::new(f64::NAN));
870 }
871
872 #[test]
873 #[should_panic(expected = "ExactDuration::from_quantity")]
874 fn from_quantity_panics_on_overflow() {
875 let _ = ExactDuration::from_quantity(Second::new(1.0e40));
876 }
877
878 #[cfg(feature = "serde")]
879 #[test]
880 fn serde_serialize_fails_on_out_of_range() {
881 let huge = ExactDuration::MAX;
883 let result = serde_json::to_string(&huge);
884 assert!(
885 result.is_err(),
886 "expected serde error for out-of-range duration"
887 );
888 }
889
890 #[test]
891 fn checked_projection_overflow_on_max() {
892 assert_eq!(
893 ExactDuration::MAX.as_seconds_i64_nanos_checked(),
894 Err(DurationError::Overflow)
895 );
896 assert_eq!(
897 ExactDuration::MIN.as_seconds_i64_nanos_checked(),
898 Err(DurationError::Overflow)
899 );
900 }
901
902 #[test]
903 fn checked_projection_small_round_trips() {
904 for nanos in [0_i128, 1, -1, 999_999_999, -999_999_999, 1_500_000_000] {
905 let d = ExactDuration::from_nanos(nanos);
906 let (s, n) = d.as_seconds_i64_nanos_checked().unwrap();
907 let recovered = (s as i128) * NANOS_PER_SECOND + n as i128;
908 assert_eq!(recovered, nanos, "checked round-trip failed for {nanos}");
909 }
910 }
911
912 #[test]
913 fn saturating_projection_extremes() {
914 let (s_max, _) = ExactDuration::MAX.as_seconds_i64_nanos_saturating();
915 assert_eq!(s_max, i64::MAX);
916 let (s_min, _) = ExactDuration::MIN.as_seconds_i64_nanos_saturating();
917 assert_eq!(s_min, i64::MIN);
918 }
919
920 #[test]
921 #[should_panic(expected = "as_seconds_i64_nanos: seconds out of i64 range")]
922 fn panicking_projection_panics_on_max() {
923 let _ = ExactDuration::MAX.as_seconds_i64_nanos();
924 }
925
926 #[test]
927 fn canonical_constructor_validates_nanos() {
928 assert!(ExactDuration::from_canonical_seconds_nanos(5, 0).is_ok());
930 assert!(ExactDuration::from_canonical_seconds_nanos(0, 999_999_999).is_ok());
931 assert!(ExactDuration::from_canonical_seconds_nanos(-1, -999_999_999).is_ok());
932 assert!(ExactDuration::from_canonical_seconds_nanos(0, 0).is_ok());
933 assert!(ExactDuration::from_canonical_seconds_nanos(0, -999_999_999).is_ok());
934 assert_eq!(
936 ExactDuration::from_canonical_seconds_nanos(0, 1_000_000_000),
937 Err(DurationError::Overflow)
938 );
939 assert_eq!(
940 ExactDuration::from_canonical_seconds_nanos(0, -1_000_000_000),
941 Err(DurationError::Overflow)
942 );
943 assert_eq!(
945 ExactDuration::from_canonical_seconds_nanos(1, -1),
946 Err(DurationError::NonCanonical)
947 );
948 assert_eq!(
949 ExactDuration::from_canonical_seconds_nanos(-1, 1),
950 Err(DurationError::NonCanonical)
951 );
952 assert_eq!(
953 ExactDuration::from_canonical_seconds_nanos(100, -500_000_000),
954 Err(DurationError::NonCanonical)
955 );
956 assert_eq!(
957 ExactDuration::from_canonical_seconds_nanos(-100, 500_000_000),
958 Err(DurationError::NonCanonical)
959 );
960 }
961
962 #[test]
963 fn display_extreme_falls_back_to_raw_nanos() {
964 let s = ExactDuration::MAX.to_string();
966 assert!(s.contains("ns"), "expected raw-ns fallback, got: {s}");
967 }
968
969 #[cfg(feature = "serde")]
970 #[test]
971 fn serde_rejects_non_canonical_pairs() {
972 let r: Result<ExactDuration, _> = serde_json::from_str(r#"{"sec":0,"ns":1000000000}"#);
974 assert!(r.is_err(), "expected Err for ns=1e9, got {:?}", r);
975
976 let r: Result<ExactDuration, _> = serde_json::from_str(r#"{"sec":0,"ns":-1000000000}"#);
978 assert!(r.is_err(), "expected Err for ns=-1e9, got {:?}", r);
979
980 let r: Result<ExactDuration, _> = serde_json::from_str(r#"{"sec":1,"ns":-1}"#);
982 assert!(r.is_err(), "expected Err for sec=1,ns=-1, got {:?}", r);
983
984 let r: Result<ExactDuration, _> = serde_json::from_str(r#"{"sec":-1,"ns":1}"#);
986 assert!(r.is_err(), "expected Err for sec=-1,ns=1, got {:?}", r);
987 }
988
989 #[test]
990 fn qtty_integer_nanosecond_round_trip() {
991 let d = ExactDuration::from_nanos(123_456_789);
993 let q = d.as_nanoseconds_i().unwrap();
994 assert_eq!(q.value(), 123_456_789_i64);
995 let back = ExactDuration::from_nanoseconds_i(q);
996 assert_eq!(back, d);
997
998 let d2 = ExactDuration::from_nanos(-999_000_000);
1000 let q2 = d2.as_nanoseconds_i().unwrap();
1001 assert_eq!(q2.value(), -999_000_000_i64);
1002 assert_eq!(ExactDuration::from_nanoseconds_i(q2), d2);
1003
1004 let q0 = ExactDuration::ZERO.as_nanoseconds_i().unwrap();
1006 assert_eq!(q0.value(), 0_i64);
1007 }
1008
1009 #[test]
1010 fn qtty_integer_nanosecond_overflow() {
1011 assert_eq!(
1013 ExactDuration::MAX.as_nanoseconds_i(),
1014 Err(DurationError::Overflow)
1015 );
1016 assert_eq!(
1017 ExactDuration::MIN.as_nanoseconds_i(),
1018 Err(DurationError::Overflow)
1019 );
1020 }
1021}