Skip to main content

gnss_time/
duration.rs

1//! # Duration
2//!
3//! A signed, nanosecond-resolution time interval with no attachment to any
4//! particular [`TimeScale`](crate::scale::TimeScale).
5//!
6//! ## Design decisions
7//!
8//! - **`i64` nanoseconds** — covers ±292 years, more than enough for any
9//!   realistic GNSS differential measurement.
10//! - **No scale phantom** — a `Duration` is just an interval; it means the same
11//!   thing regardless of which constellation produced it.
12//! - **No allocations** — `Copy` type, works in `#[no_std]` environments.
13//! - **Checked and saturating arithmetic** — you choose the overflow policy
14
15use core::{
16    fmt,
17    ops::{Add, AddAssign, Neg, Sub, SubAssign},
18};
19
20use crate::GnssTimeError;
21
22const NANOS_PER_SECOND: i64 = 1_000_000_000;
23const NANOS_PER_MILLI: i64 = 1_000_000;
24const NANOS_PER_MICRO: i64 = 1_000;
25
26/// A signed time interval measured in nanoseconds.
27///
28/// `Duration` is domain-agnostic: it represents the *difference* between two
29/// instants and can be added to or subtracted from any [`crate::Time<S>`].
30///
31/// # Range
32///
33/// `i64` nanoseconds ==> approximately ±292 years.
34///
35/// # Examples
36///
37/// ```rust
38/// use gnss_time::Duration;
39///
40/// let one_sec = Duration::from_seconds(1);
41/// let half_sec = Duration::from_millis(500);
42///
43/// assert_eq!(one_sec - half_sec, half_sec);
44///
45/// let neg = -one_sec;
46///
47/// assert_eq!(neg.as_nanos(), -1_000_000_000);
48/// ```
49#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
50#[must_use = "Duration is a value type; ignoring it has no effect"]
51#[repr(transparent)]
52pub struct Duration(i64); // nanoseconds
53
54impl Duration {
55    /// Zero duration.
56    pub const ZERO: Duration = Duration(0);
57
58    /// Maximum representable duration (~292 years).
59    pub const MAX: Duration = Duration(i64::MAX);
60
61    /// Minimum representable duration (~ -292 years).
62    pub const MIN: Duration = Duration(i64::MIN);
63
64    /// One nanosecond.
65    pub const ONE_NANOSECOND: Duration = Duration(1);
66
67    /// One second expressed as a `Duration`.
68    pub const ONE_SECOND: Duration = Duration(NANOS_PER_SECOND);
69
70    /// Create from a raw nanosecond count. Prefer the types constructors below.
71    #[inline(always)]
72    pub const fn from_nanos(nanos: i64) -> Self {
73        Duration(nanos)
74    }
75
76    /// Create from whole microseconds.
77    #[inline]
78    pub const fn from_micros(micros: i64) -> Self {
79        Duration(micros * NANOS_PER_MICRO)
80    }
81
82    /// Create from whole milliseconds.
83    #[inline]
84    pub const fn from_millis(millis: i64) -> Self {
85        Duration(millis * NANOS_PER_MILLI)
86    }
87
88    /// Create from whole seconds.
89    #[inline]
90    pub const fn from_seconds(secs: i64) -> Self {
91        Duration(secs * NANOS_PER_SECOND)
92    }
93
94    /// Create from whole minutes.
95    #[inline]
96    pub const fn from_minutes(mins: i64) -> Self {
97        Duration(mins * 60 * NANOS_PER_SECOND)
98    }
99
100    /// Create from whole hours.
101    #[inline]
102    pub const fn from_hours(hours: i64) -> Self {
103        Duration(hours * 3_600 * NANOS_PER_SECOND)
104    }
105
106    /// Create from whole days.
107    #[inline]
108    pub const fn from_days(days: i64) -> Self {
109        Duration(days * 86_400 * NANOS_PER_SECOND)
110    }
111
112    /// Create from microseconds, returning `None` on overflow.
113    #[inline]
114    #[must_use = "returns None on overflow; check the result"]
115    pub const fn checked_from_micros(micros: i64) -> Option<Self> {
116        match micros.checked_mul(NANOS_PER_MICRO) {
117            Some(n) => Some(Duration(n)),
118            None => None,
119        }
120    }
121
122    /// Create from milliseconds, returning `None` on overflow.
123    #[inline]
124    #[must_use = "returns None on overflow; check the result"]
125    pub const fn checked_from_millis(millis: i64) -> Option<Self> {
126        match millis.checked_mul(NANOS_PER_MILLI) {
127            Some(n) => Some(Duration(n)),
128            None => None,
129        }
130    }
131
132    /// Create from whole seconds, returning `None` on overflow.
133    #[inline]
134    #[must_use = "returns None on overflow; check the result"]
135    pub const fn checked_from_seconds(secs: i64) -> Option<Self> {
136        match secs.checked_mul(NANOS_PER_SECOND) {
137            Some(n) => Some(Duration(n)),
138            None => None,
139        }
140    }
141}
142
143impl Duration {
144    /// Raw nanosecond count (may be negative).
145    #[inline(always)]
146    #[must_use]
147    pub const fn as_nanos(self) -> i64 {
148        self.0
149    }
150
151    /// Whole microseconds (truncated toward zero).
152    #[inline]
153    #[must_use]
154    pub const fn as_micros(self) -> i64 {
155        self.0 / NANOS_PER_MICRO
156    }
157
158    /// Whole millisecond (truncated toward zero).
159    #[inline]
160    #[must_use]
161    pub const fn as_millis(self) -> i64 {
162        self.0 / NANOS_PER_MILLI
163    }
164
165    /// Whole seconds (truncated toward zero).
166    #[inline]
167    #[must_use]
168    pub const fn as_seconds(self) -> i64 {
169        self.0 / NANOS_PER_SECOND
170    }
171
172    /// Floating-point seconds. Precision is limited by `f64` mantissa (~15
173    /// significant decimal digits), sufficient for sub-nanosecond accuracy up
174    /// to ~10 000 seconds.
175    #[inline]
176    #[must_use]
177    pub fn as_seconds_f64(self) -> f64 {
178        self.0 as f64 / NANOS_PER_SECOND as f64
179    }
180
181    /// Returns `true` if the duration is positive (> 0).
182    #[inline]
183    #[must_use]
184    pub const fn is_positive(self) -> bool {
185        self.0 > 0
186    }
187
188    /// Returns `true` if the duration is negative (< 0).
189    #[inline]
190    #[must_use]
191    pub const fn is_negative(self) -> bool {
192        self.0 < 0
193    }
194
195    /// Returns `true` if the duration is zero.
196    #[inline]
197    #[must_use]
198    pub const fn is_zero(self) -> bool {
199        self.0 == 0
200    }
201
202    /// Absolute value. Returns `None` for `Duration::MIN` (no positive
203    /// counterpart in `i64`).
204    #[inline]
205    #[must_use = "returns None for Duration::MIN; check the result"]
206    pub const fn abs(self) -> Option<Self> {
207        match self.0.checked_abs() {
208            Some(n) => Some(Duration(n)),
209            None => None,
210        }
211    }
212}
213
214impl Duration {
215    /// Add, returning `None` on overflow.
216    #[inline]
217    #[must_use = "returns None on overflow; check the result"]
218    pub const fn checked_add(
219        self,
220        rhs: Duration,
221    ) -> Option<Duration> {
222        match self.0.checked_add(rhs.0) {
223            Some(n) => Some(Duration(n)),
224            None => None,
225        }
226    }
227
228    /// Subtract, returning `None` on overflow.
229    #[inline]
230    #[must_use = "returns None on overflow; check the result"]
231    pub const fn checked_sub(
232        self,
233        rhs: Duration,
234    ) -> Option<Duration> {
235        match self.0.checked_sub(rhs.0) {
236            Some(n) => Some(Duration(n)),
237            None => None,
238        }
239    }
240
241    /// Add, saturating at `i64::MAX` / `i64::MIN`.
242    #[inline]
243    #[must_use = "saturating_add returns a new Duration; the original is unchanged"]
244    pub const fn saturating_add(
245        self,
246        rhs: Duration,
247    ) -> Duration {
248        Duration(self.0.saturating_add(rhs.0))
249    }
250
251    /// Subtract, saturating at `i64::MAX` / `i64::MIN`.
252    #[inline]
253    #[must_use = "saturating_sub returns a new Duration; the original is unchanged"]
254    pub const fn saturating_sub(
255        self,
256        rhs: Duration,
257    ) -> Duration {
258        Duration(self.0.saturating_sub(rhs.0))
259    }
260
261    /// Fallible addition - returns [`GnssTimeError::Overflow`] on overflow.
262    #[inline]
263    pub fn try_add(
264        self,
265        rhs: Duration,
266    ) -> Result<Duration, GnssTimeError> {
267        self.checked_add(rhs).ok_or(GnssTimeError::Overflow)
268    }
269
270    /// Fallible subtraction — returns [`GnssTimeError::Overflow`] on overflow.
271    #[inline]
272    pub fn try_sub(
273        self,
274        rhs: Duration,
275    ) -> Result<Duration, GnssTimeError> {
276        self.checked_sub(rhs).ok_or(GnssTimeError::Overflow)
277    }
278}
279
280impl Add for Duration {
281    type Output = Duration;
282
283    /// # Panics / Overflow
284    ///
285    /// In debug builds, panics on overflow.
286    /// In release builds, wraps around (same semantics as `i64`).
287    #[inline]
288    fn add(
289        self,
290        rhs: Self,
291    ) -> Self::Output {
292        Duration(self.0 + rhs.0)
293    }
294}
295
296impl AddAssign for Duration {
297    #[inline]
298    fn add_assign(
299        &mut self,
300        rhs: Self,
301    ) {
302        self.0 += rhs.0
303    }
304}
305
306impl Sub for Duration {
307    type Output = Duration;
308
309    #[inline]
310    fn sub(
311        self,
312        rhs: Self,
313    ) -> Self::Output {
314        Duration(self.0 - rhs.0)
315    }
316}
317
318impl SubAssign for Duration {
319    #[inline]
320    fn sub_assign(
321        &mut self,
322        rhs: Self,
323    ) {
324        self.0 -= rhs.0
325    }
326}
327
328impl Neg for Duration {
329    type Output = Duration;
330
331    #[inline]
332    fn neg(self) -> Self::Output {
333        Duration(-self.0)
334    }
335}
336
337impl fmt::Display for Duration {
338    /// Formats as `[−]Xs Ynano_s` preserving full precision.
339    ///
340    /// Examples: `"1s 0ns"`, `"-3s 141592654ns"`, `"0s 500000000ns"`.
341    fn fmt(
342        &self,
343        f: &mut fmt::Formatter<'_>,
344    ) -> fmt::Result {
345        let total = self.0;
346        let sign = if total < 0 { "-" } else { "" };
347        let abs = total.unsigned_abs(); // u64
348        let secs = abs / 1_000_000_000u64;
349        let nanos = abs % 1_000_000_000u64;
350
351        write!(f, "{sign}{secs}s {nanos}ns")
352    }
353}
354
355////////////////////////////////////////////////////////////////////////////////
356// Tests
357////////////////////////////////////////////////////////////////////////////////
358
359#[cfg(test)]
360mod tests {
361    #[allow(unused_imports)]
362    use std::string::ToString;
363
364    use super::*;
365
366    #[test]
367    fn test_from_seconds_roundtrip() {
368        let d = Duration::from_seconds(42);
369
370        assert_eq!(d.as_seconds(), 42);
371        assert_eq!(d.as_nanos(), 42_000_000_000);
372    }
373
374    #[test]
375    fn test_from_millis_roundtrip() {
376        let d = Duration::from_millis(1500);
377
378        assert_eq!(d.as_millis(), 1500);
379        assert_eq!(d.as_seconds(), 1);
380    }
381
382    #[test]
383    fn test_from_micros_roundtrip() {
384        let d = Duration::from_micros(1_000_000);
385
386        assert_eq!(d.as_micros(), 1_000_000);
387        assert_eq!(d.as_millis(), 1_000);
388    }
389
390    #[test]
391    fn test_zero_constants() {
392        assert!(Duration::ZERO.is_zero());
393        assert_eq!(Duration::ZERO.as_nanos(), 0);
394    }
395
396    #[test]
397    fn test_sign_helpers() {
398        assert!(Duration::from_seconds(1).is_positive());
399        assert!(Duration::from_seconds(-1).is_negative());
400        assert!(!Duration::ZERO.is_positive());
401        assert!(!Duration::ZERO.is_negative());
402    }
403
404    #[test]
405    fn test_add_sub_identify() {
406        let a = Duration::from_seconds(10);
407        let b = Duration::from_seconds(3);
408
409        assert_eq!(a - b + b, a);
410    }
411
412    #[test]
413    fn test_negative() {
414        let d = Duration::from_seconds(5);
415
416        assert_eq!((-d).as_nanos(), -5_000_000_000);
417        assert_eq!(-(-d), d);
418    }
419
420    #[test]
421    fn test_checked_add_overflow() {
422        assert!(Duration::MAX
423            .checked_add(Duration::ONE_NANOSECOND)
424            .is_none());
425    }
426
427    #[test]
428    fn test_checked_add_underflow() {
429        assert!(Duration::MIN
430            .checked_sub(Duration::ONE_NANOSECOND)
431            .is_none());
432    }
433
434    #[test]
435    fn test_saturating_add_clamps() {
436        let result = Duration::MAX.saturating_add(Duration::ONE_NANOSECOND);
437
438        assert_eq!(result, Duration::MAX);
439    }
440
441    #[test]
442    fn test_saturating_sub_clamps() {
443        let result = Duration::MIN.saturating_sub(Duration::ONE_NANOSECOND);
444
445        assert_eq!(result, Duration::MIN);
446    }
447
448    #[test]
449    fn test_abs_positive() {
450        let d = Duration::from_seconds(-7);
451
452        assert_eq!(d.abs().unwrap().as_seconds(), 7);
453    }
454
455    #[test]
456    fn test_abs_min_is_none() {
457        assert!(Duration::MIN.abs().is_none());
458    }
459
460    #[test]
461    fn test_as_seconds_f64_precision() {
462        let d = Duration::from_nanos(1_500_000_001); // 1.500000001 s
463        let f = d.as_seconds_f64();
464
465        // f64 has ~15 significant digits; 1.500000001 requires 10 → represented exactly
466        assert!((f - 1.500_000_001_f64).abs() < 1e-9);
467    }
468
469    #[test]
470    fn test_display_positive() {
471        assert_eq!(Duration::from_seconds(1).to_string(), "1s 0ns");
472    }
473
474    #[test]
475    fn test_display_negative() {
476        let d = Duration::from_nanos(-3_141_592_654);
477        assert_eq!(d.to_string(), "-3s 141592654ns");
478    }
479
480    #[test]
481    fn test_display_zero() {
482        assert_eq!(Duration::ZERO.to_string(), "0s 0ns");
483    }
484
485    #[test]
486    fn test_size_of_duration_is_8_bytes() {
487        assert_eq!(core::mem::size_of::<Duration>(), 8);
488    }
489
490    #[test]
491    fn test_identity_zero_addition() {
492        let d = Duration::from_seconds(123);
493
494        assert_eq!(d + Duration::ZERO, d);
495        assert_eq!(Duration::ZERO + d, d);
496    }
497
498    #[test]
499    fn test_identity_zero_subtraction() {
500        let d = Duration::from_seconds(123);
501
502        assert_eq!(d - Duration::ZERO, d);
503    }
504
505    #[test]
506    fn test_double_negation() {
507        let d = Duration::from_seconds(999);
508
509        assert_eq!(-(-d), d);
510    }
511
512    #[test]
513    fn test_add_sub_inverse() {
514        let a = Duration::from_seconds(1000);
515        let b = Duration::from_seconds(250);
516
517        assert_eq!((a + b) - b, a);
518    }
519
520    #[test]
521    fn test_sub_add_inverse() {
522        let a = Duration::from_seconds(1000);
523        let b = Duration::from_seconds(250);
524
525        assert_eq!((a - b) + b, a);
526    }
527
528    #[test]
529    fn test_add_commutativity() {
530        let a = Duration::from_seconds(10);
531        let b = Duration::from_seconds(3);
532
533        assert_eq!(a + b, b + a);
534    }
535
536    #[test]
537    fn test_add_associativity() {
538        let a = Duration::from_seconds(1);
539        let b = Duration::from_seconds(2);
540        let c = Duration::from_seconds(3);
541
542        assert_eq!((a + b) + c, a + (b + c));
543    }
544
545    #[test]
546    fn test_checked_add_matches_operator_when_safe() {
547        let a = Duration::from_seconds(10);
548        let b = Duration::from_seconds(5);
549
550        assert_eq!(a.checked_add(b), Some(a + b));
551    }
552
553    #[test]
554    fn test_checked_sub_matches_operator_when_safe() {
555        let a = Duration::from_seconds(10);
556        let b = Duration::from_seconds(5);
557
558        assert_eq!(a.checked_sub(b), Some(a - b));
559    }
560
561    #[test]
562    fn test_sign_symmetry() {
563        let d = Duration::from_seconds(42);
564
565        assert_eq!(d.is_positive(), (-d).is_negative());
566        assert_eq!(d.is_negative(), (-d).is_positive());
567    }
568
569    #[test]
570    fn test_conversion_consistency() {
571        let d = Duration::from_seconds(1);
572
573        assert_eq!(Duration::from_millis(1000), d);
574        assert_eq!(Duration::from_micros(1_000_000), d);
575    }
576
577    #[test]
578    fn test_nanos_identity() {
579        let d = Duration::from_nanos(123456789);
580
581        assert_eq!(d.as_nanos(), 123456789);
582    }
583
584    #[test]
585    fn test_checked_from_seconds_overflow() {
586        assert!(Duration::checked_from_seconds(i64::MAX / NANOS_PER_SECOND + 1).is_none());
587    }
588
589    #[test]
590    fn test_checked_from_millis_overflow() {
591        assert!(Duration::checked_from_millis(i64::MAX / NANOS_PER_MILLI + 1).is_none());
592    }
593
594    #[test]
595    fn test_checked_from_micros_overflow() {
596        assert!(Duration::checked_from_micros(i64::MAX / NANOS_PER_MICRO + 1).is_none());
597    }
598
599    #[test]
600    fn test_as_seconds_truncation_positive() {
601        let d = Duration::from_nanos(1_500_000_000);
602        assert_eq!(d.as_seconds(), 1);
603    }
604
605    #[test]
606    fn test_as_seconds_truncation_negative() {
607        let d = Duration::from_nanos(-1_500_000_000);
608
609        // важно: trunc toward zero
610        assert_eq!(d.as_seconds(), -1);
611    }
612
613    #[test]
614    fn test_as_millis_truncation_negative() {
615        let d = Duration::from_nanos(-1_500_000);
616        assert_eq!(d.as_millis(), -1);
617    }
618
619    #[test]
620    fn test_add_assign() {
621        let mut d = Duration::from_seconds(10);
622        d += Duration::from_seconds(5);
623
624        assert_eq!(d, Duration::from_seconds(15));
625    }
626
627    #[test]
628    fn test_sub_assign() {
629        let mut d = Duration::from_seconds(10);
630        d -= Duration::from_seconds(5);
631
632        assert_eq!(d, Duration::from_seconds(5));
633    }
634
635    #[test]
636    fn test_add_assign_zero_identity() {
637        let mut d = Duration::from_seconds(42);
638        d += Duration::ZERO;
639
640        assert_eq!(d, Duration::from_seconds(42));
641    }
642
643    #[test]
644    fn test_sub_assign_zero_identity() {
645        let mut d = Duration::from_seconds(42);
646        d -= Duration::ZERO;
647
648        assert_eq!(d, Duration::from_seconds(42));
649    }
650
651    #[test]
652    fn test_min_plus_zero() {
653        assert_eq!(Duration::MIN + Duration::ZERO, Duration::MIN);
654    }
655
656    #[test]
657    fn test_max_plus_zero() {
658        assert_eq!(Duration::MAX + Duration::ZERO, Duration::MAX);
659    }
660
661    #[test]
662    fn test_min_minus_zero() {
663        assert_eq!(Duration::MIN - Duration::ZERO, Duration::MIN);
664    }
665
666    #[test]
667    fn test_max_minus_zero() {
668        assert_eq!(Duration::MAX - Duration::ZERO, Duration::MAX);
669    }
670
671    #[test]
672    fn test_abs_positive_identity() {
673        let d = Duration::from_seconds(10);
674        assert_eq!(d.abs().unwrap(), d);
675    }
676
677    #[test]
678    fn test_abs_zero() {
679        assert_eq!(Duration::ZERO.abs().unwrap(), Duration::ZERO);
680    }
681
682    #[test]
683    fn test_seconds_millis_consistency() {
684        assert_eq!(Duration::from_seconds(1), Duration::from_millis(1000));
685    }
686
687    #[test]
688    fn test_seconds_micros_consistency() {
689        assert_eq!(Duration::from_seconds(1), Duration::from_micros(1_000_000));
690    }
691
692    #[test]
693    fn test_seconds_nanos_consistency() {
694        assert_eq!(
695            Duration::from_seconds(1),
696            Duration::from_nanos(1_000_000_000)
697        );
698    }
699
700    #[test]
701    fn test_checked_add_matches_manual() {
702        let a = Duration::from_seconds(123);
703        let b = Duration::from_seconds(456);
704
705        assert_eq!(a.checked_add(b), Some(Duration::from_seconds(579)));
706    }
707
708    #[test]
709    fn test_checked_sub_matches_manual() {
710        let a = Duration::from_seconds(500);
711        let b = Duration::from_seconds(200);
712
713        assert_eq!(a.checked_sub(b), Some(Duration::from_seconds(300)));
714    }
715
716    #[test]
717    fn test_ordering_basic() {
718        let a = Duration::from_seconds(1);
719        let b = Duration::from_seconds(2);
720
721        assert!(a < b);
722        assert!(b > a);
723    }
724
725    #[test]
726    fn test_ordering_zero() {
727        let a = Duration::ZERO;
728        let b = Duration::from_seconds(1);
729
730        assert!(a < b);
731    }
732
733    #[test]
734    fn test_neg_zero() {
735        assert_eq!(-Duration::ZERO, Duration::ZERO);
736    }
737
738    #[test]
739    fn test_neg_sign_flip() {
740        let d = Duration::from_seconds(100);
741
742        assert_eq!(-d, Duration::from_seconds(-100));
743    }
744
745    #[test]
746    fn test_checked_add_overflow_returns_none() {
747        assert_eq!(Duration::MAX.checked_add(Duration::ONE_NANOSECOND), None);
748    }
749
750    #[test]
751    fn test_checked_sub_underflow_returns_none() {
752        assert_eq!(Duration::MIN.checked_sub(Duration::ONE_NANOSECOND), None);
753    }
754
755    #[test]
756    fn test_try_add_overflow_returns_err() {
757        assert_eq!(
758            Duration::MAX.try_add(Duration::ONE_NANOSECOND),
759            Err(GnssTimeError::Overflow)
760        );
761    }
762
763    #[test]
764    fn test_try_sub_underflow_returns_err() {
765        assert_eq!(
766            Duration::MIN.try_sub(Duration::ONE_NANOSECOND),
767            Err(GnssTimeError::Overflow)
768        );
769    }
770}