Skip to main content

gnss_time/
duration.rs

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