1use core::{
51 fmt,
52 ops::{Add, AddAssign, Neg, Sub, SubAssign},
53};
54
55use crate::GnssTimeError;
56
57const NANOS_PER_SECOND: i64 = 1_000_000_000;
59
60const NANOS_PER_MILLI: i64 = 1_000_000;
62
63const NANOS_PER_MICRO: i64 = 1_000;
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
93#[must_use = "Duration is a value type; ignoring it has no effect"]
94#[repr(transparent)]
95pub struct Duration(i64); impl Duration {
98 pub const ZERO: Duration = Duration(0);
100
101 pub const MAX: Duration = Duration(i64::MAX);
103
104 pub const MIN: Duration = Duration(i64::MIN);
106
107 pub const ONE_NANOSECOND: Duration = Duration(1);
109
110 pub const ONE_SECOND: Duration = Duration(NANOS_PER_SECOND);
112
113 #[inline]
115 pub const fn from_nanos(nanos: i64) -> Self {
116 Duration(nanos)
117 }
118
119 #[inline]
121 pub const fn from_micros(micros: i64) -> Self {
122 Duration(micros * NANOS_PER_MICRO)
123 }
124
125 #[inline]
127 pub const fn from_millis(millis: i64) -> Self {
128 Duration(millis * NANOS_PER_MILLI)
129 }
130
131 #[inline]
133 pub const fn from_seconds(secs: i64) -> Self {
134 Duration(secs * NANOS_PER_SECOND)
135 }
136
137 #[inline]
139 pub const fn from_minutes(mins: i64) -> Self {
140 Duration(mins * 60 * NANOS_PER_SECOND)
141 }
142
143 #[inline]
145 pub const fn from_hours(hours: i64) -> Self {
146 Duration(hours * 3_600 * NANOS_PER_SECOND)
147 }
148
149 #[inline]
151 pub const fn from_days(days: i64) -> Self {
152 Duration(days * 86_400 * NANOS_PER_SECOND)
153 }
154
155 #[inline]
157 #[must_use = "returns None on overflow; check the result"]
158 pub const fn checked_from_micros(micros: i64) -> Option<Self> {
159 match micros.checked_mul(NANOS_PER_MICRO) {
160 Some(n) => Some(Duration(n)),
161 None => None,
162 }
163 }
164
165 #[inline]
167 #[must_use = "returns None on overflow; check the result"]
168 pub const fn checked_from_millis(millis: i64) -> Option<Self> {
169 match millis.checked_mul(NANOS_PER_MILLI) {
170 Some(n) => Some(Duration(n)),
171 None => None,
172 }
173 }
174
175 #[inline]
177 #[must_use = "returns None on overflow; check the result"]
178 pub const fn checked_from_seconds(secs: i64) -> Option<Self> {
179 match secs.checked_mul(NANOS_PER_SECOND) {
180 Some(n) => Some(Duration(n)),
181 None => None,
182 }
183 }
184
185 #[inline]
187 #[must_use]
188 pub const fn as_nanos(self) -> i64 {
189 self.0
190 }
191
192 #[inline]
194 #[must_use]
195 pub const fn as_micros(self) -> i64 {
196 self.0 / NANOS_PER_MICRO
197 }
198
199 #[inline]
201 #[must_use]
202 pub const fn as_millis(self) -> i64 {
203 self.0 / NANOS_PER_MILLI
204 }
205
206 #[inline]
208 #[must_use]
209 pub const fn as_seconds(self) -> i64 {
210 self.0 / NANOS_PER_SECOND
211 }
212
213 #[inline]
219 #[must_use]
220 #[allow(clippy::cast_precision_loss)]
221 pub fn as_seconds_f64(self) -> f64 {
222 self.0 as f64 / NANOS_PER_SECOND as f64
223 }
224
225 #[inline]
227 #[must_use]
228 pub const fn is_positive(self) -> bool {
229 self.0 > 0
230 }
231
232 #[inline]
234 #[must_use]
235 pub const fn is_negative(self) -> bool {
236 self.0 < 0
237 }
238
239 #[inline]
241 #[must_use]
242 pub const fn is_zero(self) -> bool {
243 self.0 == 0
244 }
245
246 #[inline]
248 #[must_use = "returns None for Duration::MIN; check the result"]
249 pub const fn abs(self) -> Option<Self> {
250 match self.0.checked_abs() {
251 Some(n) => Some(Duration(n)),
252 None => None,
253 }
254 }
255
256 #[inline]
258 #[must_use = "returns None on overflow; check the result"]
259 pub const fn checked_add(
260 self,
261 rhs: Duration,
262 ) -> Option<Duration> {
263 match self.0.checked_add(rhs.0) {
264 Some(n) => Some(Duration(n)),
265 None => None,
266 }
267 }
268
269 #[inline]
271 #[must_use = "returns None on overflow; check the result"]
272 pub const fn checked_sub(
273 self,
274 rhs: Duration,
275 ) -> Option<Duration> {
276 match self.0.checked_sub(rhs.0) {
277 Some(n) => Some(Duration(n)),
278 None => None,
279 }
280 }
281
282 #[inline]
284 #[must_use = "saturating_add returns a new Duration; the original is unchanged"]
285 pub const fn saturating_add(
286 self,
287 rhs: Duration,
288 ) -> Duration {
289 Duration(self.0.saturating_add(rhs.0))
290 }
291
292 #[inline]
294 #[must_use = "saturating_sub returns a new Duration; the original is unchanged"]
295 pub const fn saturating_sub(
296 self,
297 rhs: Duration,
298 ) -> Duration {
299 Duration(self.0.saturating_sub(rhs.0))
300 }
301
302 #[inline]
308 pub fn try_add(
309 self,
310 rhs: Duration,
311 ) -> Result<Duration, GnssTimeError> {
312 self.checked_add(rhs).ok_or(GnssTimeError::Overflow)
313 }
314
315 #[inline]
321 pub fn try_sub(
322 self,
323 rhs: Duration,
324 ) -> Result<Duration, GnssTimeError> {
325 self.checked_sub(rhs).ok_or(GnssTimeError::Overflow)
326 }
327}
328
329impl Add for Duration {
330 type Output = Duration;
331
332 #[inline]
333 fn add(
334 self,
335 rhs: Self,
336 ) -> Self::Output {
337 Duration(self.0 + rhs.0)
338 }
339}
340
341impl AddAssign for Duration {
342 #[inline]
343 fn add_assign(
344 &mut self,
345 rhs: Self,
346 ) {
347 self.0 += rhs.0;
348 }
349}
350
351impl Sub for Duration {
352 type Output = Duration;
353
354 #[inline]
355 fn sub(
356 self,
357 rhs: Self,
358 ) -> Self::Output {
359 Duration(self.0 - rhs.0)
360 }
361}
362
363impl SubAssign for Duration {
364 #[inline]
365 fn sub_assign(
366 &mut self,
367 rhs: Self,
368 ) {
369 self.0 -= rhs.0;
370 }
371}
372
373impl Neg for Duration {
374 type Output = Duration;
375
376 #[inline]
377 fn neg(self) -> Self::Output {
378 Duration(-self.0)
379 }
380}
381
382impl fmt::Display for Duration {
383 fn fmt(
384 &self,
385 f: &mut fmt::Formatter<'_>,
386 ) -> fmt::Result {
387 let abs = self.0.unsigned_abs();
388 let sign = if self.0 < 0 { "-" } else { "" };
389
390 let secs = abs / 1_000_000_000;
391 let nanos = abs % 1_000_000_000;
392
393 write!(f, "{sign}{secs}s {nanos}ns")
394 }
395}
396
397#[cfg(test)]
402mod tests {
403 #[allow(unused_imports)]
404 use std::string::ToString;
405
406 use super::*;
407
408 #[test]
409 fn test_from_seconds_roundtrip() {
410 let d = Duration::from_seconds(42);
411
412 assert_eq!(d.as_seconds(), 42);
413 assert_eq!(d.as_nanos(), 42_000_000_000);
414 }
415
416 #[test]
417 fn test_from_millis_roundtrip() {
418 let d = Duration::from_millis(1500);
419
420 assert_eq!(d.as_millis(), 1500);
421 assert_eq!(d.as_seconds(), 1);
422 }
423
424 #[test]
425 fn test_from_micros_roundtrip() {
426 let d = Duration::from_micros(1_000_000);
427
428 assert_eq!(d.as_micros(), 1_000_000);
429 assert_eq!(d.as_millis(), 1_000);
430 }
431
432 #[test]
433 fn test_zero_constants() {
434 assert!(Duration::ZERO.is_zero());
435 assert_eq!(Duration::ZERO.as_nanos(), 0);
436 }
437
438 #[test]
439 fn test_sign_helpers() {
440 assert!(Duration::from_seconds(1).is_positive());
441 assert!(Duration::from_seconds(-1).is_negative());
442 assert!(!Duration::ZERO.is_positive());
443 assert!(!Duration::ZERO.is_negative());
444 }
445
446 #[test]
447 fn test_add_sub_identify() {
448 let a = Duration::from_seconds(10);
449 let b = Duration::from_seconds(3);
450
451 assert_eq!(a - b + b, a);
452 }
453
454 #[test]
455 fn test_negative() {
456 let d = Duration::from_seconds(5);
457
458 assert_eq!((-d).as_nanos(), -5_000_000_000);
459 assert_eq!(-(-d), d);
460 }
461
462 #[test]
463 fn test_checked_add_overflow() {
464 assert!(Duration::MAX
465 .checked_add(Duration::ONE_NANOSECOND)
466 .is_none());
467 }
468
469 #[test]
470 fn test_checked_add_underflow() {
471 assert!(Duration::MIN
472 .checked_sub(Duration::ONE_NANOSECOND)
473 .is_none());
474 }
475
476 #[test]
477 fn test_saturating_add_clamps() {
478 let result = Duration::MAX.saturating_add(Duration::ONE_NANOSECOND);
479
480 assert_eq!(result, Duration::MAX);
481 }
482
483 #[test]
484 fn test_saturating_sub_clamps() {
485 let result = Duration::MIN.saturating_sub(Duration::ONE_NANOSECOND);
486
487 assert_eq!(result, Duration::MIN);
488 }
489
490 #[test]
491 fn test_abs_positive() {
492 let d = Duration::from_seconds(-7);
493
494 assert_eq!(d.abs().unwrap().as_seconds(), 7);
495 }
496
497 #[test]
498 fn test_abs_min_is_none() {
499 assert!(Duration::MIN.abs().is_none());
500 }
501
502 #[test]
503 fn test_as_seconds_f64_precision() {
504 let d = Duration::from_nanos(1_500_000_001); let f = d.as_seconds_f64();
506
507 assert!((f - 1.500_000_001_f64).abs() < 1e-9);
509 }
510
511 #[test]
512 fn test_display_positive() {
513 assert_eq!(Duration::from_seconds(1).to_string(), "1s 0ns");
514 }
515
516 #[test]
517 fn test_display_negative() {
518 let d = Duration::from_nanos(-3_141_592_654);
519 assert_eq!(d.to_string(), "-3s 141592654ns");
520 }
521
522 #[test]
523 fn test_display_zero() {
524 assert_eq!(Duration::ZERO.to_string(), "0s 0ns");
525 }
526
527 #[test]
528 fn test_size_of_duration_is_8_bytes() {
529 assert_eq!(core::mem::size_of::<Duration>(), 8);
530 }
531
532 #[test]
533 fn test_identity_zero_addition() {
534 let d = Duration::from_seconds(123);
535
536 assert_eq!(d + Duration::ZERO, d);
537 assert_eq!(Duration::ZERO + d, d);
538 }
539
540 #[test]
541 fn test_identity_zero_subtraction() {
542 let d = Duration::from_seconds(123);
543
544 assert_eq!(d - Duration::ZERO, d);
545 }
546
547 #[test]
548 fn test_double_negation() {
549 let d = Duration::from_seconds(999);
550
551 assert_eq!(-(-d), d);
552 }
553
554 #[test]
555 fn test_add_sub_inverse() {
556 let a = Duration::from_seconds(1000);
557 let b = Duration::from_seconds(250);
558
559 assert_eq!((a + b) - b, a);
560 }
561
562 #[test]
563 fn test_sub_add_inverse() {
564 let a = Duration::from_seconds(1000);
565 let b = Duration::from_seconds(250);
566
567 assert_eq!((a - b) + b, a);
568 }
569
570 #[test]
571 fn test_add_commutativity() {
572 let a = Duration::from_seconds(10);
573 let b = Duration::from_seconds(3);
574
575 assert_eq!(a + b, b + a);
576 }
577
578 #[test]
579 fn test_add_associativity() {
580 let a = Duration::from_seconds(1);
581 let b = Duration::from_seconds(2);
582 let c = Duration::from_seconds(3);
583
584 assert_eq!((a + b) + c, a + (b + c));
585 }
586
587 #[test]
588 fn test_checked_add_matches_operator_when_safe() {
589 let a = Duration::from_seconds(10);
590 let b = Duration::from_seconds(5);
591
592 assert_eq!(a.checked_add(b), Some(a + b));
593 }
594
595 #[test]
596 fn test_checked_sub_matches_operator_when_safe() {
597 let a = Duration::from_seconds(10);
598 let b = Duration::from_seconds(5);
599
600 assert_eq!(a.checked_sub(b), Some(a - b));
601 }
602
603 #[test]
604 fn test_sign_symmetry() {
605 let d = Duration::from_seconds(42);
606
607 assert_eq!(d.is_positive(), (-d).is_negative());
608 assert_eq!(d.is_negative(), (-d).is_positive());
609 }
610
611 #[test]
612 fn test_conversion_consistency() {
613 let d = Duration::from_seconds(1);
614
615 assert_eq!(Duration::from_millis(1000), d);
616 assert_eq!(Duration::from_micros(1_000_000), d);
617 }
618
619 #[test]
620 fn test_nanos_identity() {
621 let d = Duration::from_nanos(123_456_789);
622
623 assert_eq!(d.as_nanos(), 123_456_789);
624 }
625
626 #[test]
627 fn test_checked_from_seconds_overflow() {
628 assert!(Duration::checked_from_seconds(i64::MAX / NANOS_PER_SECOND + 1).is_none());
629 }
630
631 #[test]
632 fn test_checked_from_millis_overflow() {
633 assert!(Duration::checked_from_millis(i64::MAX / NANOS_PER_MILLI + 1).is_none());
634 }
635
636 #[test]
637 fn test_checked_from_micros_overflow() {
638 assert!(Duration::checked_from_micros(i64::MAX / NANOS_PER_MICRO + 1).is_none());
639 }
640
641 #[test]
642 fn test_as_seconds_truncation_positive() {
643 let d = Duration::from_nanos(1_500_000_000);
644 assert_eq!(d.as_seconds(), 1);
645 }
646
647 #[test]
648 fn test_as_seconds_truncation_negative() {
649 let d = Duration::from_nanos(-1_500_000_000);
650
651 assert_eq!(d.as_seconds(), -1);
653 }
654
655 #[test]
656 fn test_as_millis_truncation_negative() {
657 let d = Duration::from_nanos(-1_500_000);
658 assert_eq!(d.as_millis(), -1);
659 }
660
661 #[test]
662 fn test_add_assign() {
663 let mut d = Duration::from_seconds(10);
664 d += Duration::from_seconds(5);
665
666 assert_eq!(d, Duration::from_seconds(15));
667 }
668
669 #[test]
670 fn test_sub_assign() {
671 let mut d = Duration::from_seconds(10);
672 d -= Duration::from_seconds(5);
673
674 assert_eq!(d, Duration::from_seconds(5));
675 }
676
677 #[test]
678 fn test_add_assign_zero_identity() {
679 let mut d = Duration::from_seconds(42);
680 d += Duration::ZERO;
681
682 assert_eq!(d, Duration::from_seconds(42));
683 }
684
685 #[test]
686 fn test_sub_assign_zero_identity() {
687 let mut d = Duration::from_seconds(42);
688 d -= Duration::ZERO;
689
690 assert_eq!(d, Duration::from_seconds(42));
691 }
692
693 #[test]
694 fn test_min_plus_zero() {
695 assert_eq!(Duration::MIN + Duration::ZERO, Duration::MIN);
696 }
697
698 #[test]
699 fn test_max_plus_zero() {
700 assert_eq!(Duration::MAX + Duration::ZERO, Duration::MAX);
701 }
702
703 #[test]
704 fn test_min_minus_zero() {
705 assert_eq!(Duration::MIN - Duration::ZERO, Duration::MIN);
706 }
707
708 #[test]
709 fn test_max_minus_zero() {
710 assert_eq!(Duration::MAX - Duration::ZERO, Duration::MAX);
711 }
712
713 #[test]
714 fn test_abs_positive_identity() {
715 let d = Duration::from_seconds(10);
716 assert_eq!(d.abs().unwrap(), d);
717 }
718
719 #[test]
720 fn test_abs_zero() {
721 assert_eq!(Duration::ZERO.abs().unwrap(), Duration::ZERO);
722 }
723
724 #[test]
725 fn test_seconds_millis_consistency() {
726 assert_eq!(Duration::from_seconds(1), Duration::from_millis(1000));
727 }
728
729 #[test]
730 fn test_seconds_micros_consistency() {
731 assert_eq!(Duration::from_seconds(1), Duration::from_micros(1_000_000));
732 }
733
734 #[test]
735 fn test_seconds_nanos_consistency() {
736 assert_eq!(
737 Duration::from_seconds(1),
738 Duration::from_nanos(1_000_000_000)
739 );
740 }
741
742 #[test]
743 fn test_checked_add_matches_manual() {
744 let a = Duration::from_seconds(123);
745 let b = Duration::from_seconds(456);
746
747 assert_eq!(a.checked_add(b), Some(Duration::from_seconds(579)));
748 }
749
750 #[test]
751 fn test_checked_sub_matches_manual() {
752 let a = Duration::from_seconds(500);
753 let b = Duration::from_seconds(200);
754
755 assert_eq!(a.checked_sub(b), Some(Duration::from_seconds(300)));
756 }
757
758 #[test]
759 fn test_ordering_basic() {
760 let a = Duration::from_seconds(1);
761 let b = Duration::from_seconds(2);
762
763 assert!(a < b);
764 assert!(b > a);
765 }
766
767 #[test]
768 fn test_ordering_zero() {
769 let a = Duration::ZERO;
770 let b = Duration::from_seconds(1);
771
772 assert!(a < b);
773 }
774
775 #[test]
776 fn test_neg_zero() {
777 assert_eq!(-Duration::ZERO, Duration::ZERO);
778 }
779
780 #[test]
781 fn test_neg_sign_flip() {
782 let d = Duration::from_seconds(100);
783
784 assert_eq!(-d, Duration::from_seconds(-100));
785 }
786
787 #[test]
788 fn test_checked_add_overflow_returns_none() {
789 assert_eq!(Duration::MAX.checked_add(Duration::ONE_NANOSECOND), None);
790 }
791
792 #[test]
793 fn test_checked_sub_underflow_returns_none() {
794 assert_eq!(Duration::MIN.checked_sub(Duration::ONE_NANOSECOND), None);
795 }
796
797 #[test]
798 fn test_try_add_overflow_returns_err() {
799 assert_eq!(
800 Duration::MAX.try_add(Duration::ONE_NANOSECOND),
801 Err(GnssTimeError::Overflow)
802 );
803 }
804
805 #[test]
806 fn test_try_sub_underflow_returns_err() {
807 assert_eq!(
808 Duration::MIN.try_sub(Duration::ONE_NANOSECOND),
809 Err(GnssTimeError::Overflow)
810 );
811 }
812}