Skip to main content

gnss_time/
duration.rs

1//! # Duration
2//!
3//! A signed, fixed-precision time interval with nanosecond resolution.
4//!
5//! `Duration` represents the difference between two instants and is
6//! independent of any time scale, epoch, or calendar system.
7//!
8//! This type is a thin wrapper around a signed 64-bit integer storing a
9//! count of nanoseconds.
10//!
11//! ## Representation
12//!
13//! Internally represented as an `i64` number of nanoseconds.
14//!
15//! ## Range
16//!
17//! Approximately ±292 years (`i64::MIN..=i64::MAX` nanoseconds).
18//!
19//! ## Semantics
20//!
21//! - Linear, uniform time (no leap seconds, no calendar irregularities)
22//! - Arithmetic is performed in integer nanoseconds
23//! - Negative durations are fully supported
24//!
25//! ## Guarantees
26//!
27//! - `#[repr(transparent)]` over `i64`
28//! - `Copy`, `Clone`, `Eq`, `Ord`, `Hash`
29//! - `no_std` compatible
30//! - No hidden allocations
31//!
32//! ## Arithmetic
33//!
34//! - Operator-based arithmetic (`Add`, `Sub`, etc.) does **not** check for
35//!   overflow
36//! - Checked variants (`checked_*`) return `None` on overflow
37//! - Saturating variants clamp to [`Duration::MIN`] / [`Duration::MAX`]
38//! - Fallible variants return [`GnssTimeError`]
39//!
40//! ## Notes
41//!
42//! This type intentionally does **not** model:
43//!
44//! - Calendar units (months, years)
45//! - Non-uniform days (leap seconds, DST)
46//! - Any time scale (TAI, UTC, GPS, etc.)
47//!
48//! For such concepts, use higher-level types.
49
50use core::{
51    fmt,
52    ops::{Add, AddAssign, Neg, Sub, SubAssign},
53};
54
55use crate::GnssTimeError;
56
57/// The number of nanoseconds in one second.
58const NANOS_PER_SECOND: i64 = 1_000_000_000;
59
60/// The number of nanoseconds in one millisecond.
61const NANOS_PER_MILLI: i64 = 1_000_000;
62
63/// The number of nanoseconds in one microsecond.
64const NANOS_PER_MICRO: i64 = 1_000;
65
66/// A signed time interval with nanosecond precision.
67///
68/// `Duration` is a value type representing a span of time, stored as a
69/// signed 64-bit count of nanoseconds.
70///
71/// ## Precision
72///
73/// 1 nanosecond.
74///
75/// ## Range
76///
77/// Approximately ±292 years.
78///
79/// ## Examples
80///
81/// ```rust
82/// use gnss_time::Duration;
83///
84/// let a = Duration::from_seconds(1);
85/// let b = Duration::from_millis(500);
86///
87/// assert_eq!(a - b, b);
88///
89/// let neg = -a;
90/// assert_eq!(neg.as_nanos(), -1_000_000_000);
91/// ```
92#[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); // nanoseconds
96
97impl Duration {
98    /// Zero duration.
99    pub const ZERO: Duration = Duration(0);
100
101    /// Maximum representable duration.
102    pub const MAX: Duration = Duration(i64::MAX);
103
104    /// Minimum representable duration.
105    pub const MIN: Duration = Duration(i64::MIN);
106
107    /// One nanosecond.
108    pub const ONE_NANOSECOND: Duration = Duration(1);
109
110    /// One second.
111    pub const ONE_SECOND: Duration = Duration(NANOS_PER_SECOND);
112
113    /// Creates a `Duration` from nanoseconds.
114    #[inline]
115    pub const fn from_nanos(nanos: i64) -> Self {
116        Duration(nanos)
117    }
118
119    /// Creates a `Duration` from microseconds.
120    #[inline]
121    pub const fn from_micros(micros: i64) -> Self {
122        Duration(micros * NANOS_PER_MICRO)
123    }
124
125    /// Creates a `Duration` from milliseconds.
126    #[inline]
127    pub const fn from_millis(millis: i64) -> Self {
128        Duration(millis * NANOS_PER_MILLI)
129    }
130
131    /// Creates a `Duration` from seconds.
132    #[inline]
133    pub const fn from_seconds(secs: i64) -> Self {
134        Duration(secs * NANOS_PER_SECOND)
135    }
136
137    /// Creates a `Duration` from minutes.
138    #[inline]
139    pub const fn from_minutes(mins: i64) -> Self {
140        Duration(mins * 60 * NANOS_PER_SECOND)
141    }
142
143    /// Creates a `Duration` from hours.
144    #[inline]
145    pub const fn from_hours(hours: i64) -> Self {
146        Duration(hours * 3_600 * NANOS_PER_SECOND)
147    }
148
149    /// Creates a `Duration` from days.
150    #[inline]
151    pub const fn from_days(days: i64) -> Self {
152        Duration(days * 86_400 * NANOS_PER_SECOND)
153    }
154
155    /// Creates a `Duration` from microseconds, returning `None` on overflow.
156    #[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    /// Creates a `Duration` from milliseconds, returning `None` on overflow.
166    #[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    /// Creates a `Duration` from seconds, returning `None` on overflow.
176    #[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    /// Returns the raw nanosecond value.
186    #[inline]
187    #[must_use]
188    pub const fn as_nanos(self) -> i64 {
189        self.0
190    }
191
192    /// Returns whole microseconds (truncated toward zero).
193    #[inline]
194    #[must_use]
195    pub const fn as_micros(self) -> i64 {
196        self.0 / NANOS_PER_MICRO
197    }
198
199    /// Returns whole milliseconds (truncated toward zero).
200    #[inline]
201    #[must_use]
202    pub const fn as_millis(self) -> i64 {
203        self.0 / NANOS_PER_MILLI
204    }
205
206    /// Returns whole seconds (truncated toward zero).
207    #[inline]
208    #[must_use]
209    pub const fn as_seconds(self) -> i64 {
210        self.0 / NANOS_PER_SECOND
211    }
212
213    /// Returns seconds as `f64`.
214    ///
215    /// # Precision
216    ///
217    /// May lose precision for large durations (> ~2^53 nanoseconds ≈ 104 days).
218    #[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    /// Returns `true` if positive.
226    #[inline]
227    #[must_use]
228    pub const fn is_positive(self) -> bool {
229        self.0 > 0
230    }
231
232    /// Returns `true` if negative.
233    #[inline]
234    #[must_use]
235    pub const fn is_negative(self) -> bool {
236        self.0 < 0
237    }
238
239    /// Returns `true` if zero.
240    #[inline]
241    #[must_use]
242    pub const fn is_zero(self) -> bool {
243        self.0 == 0
244    }
245
246    /// Returns absolute value, or `None` on overflow.
247    #[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    /// Checked addition.
257    #[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    /// Checked subtraction.
270    #[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    /// Saturating addition.
283    #[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    /// Saturating subtraction.
293    #[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    /// Fallible addition.
303    ///
304    /// # Errors
305    ///
306    /// Returns [`GnssTimeError::Overflow`] if the result cannot be represented.
307    #[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    /// Fallible subtraction.
316    ///
317    /// # Errors
318    ///
319    /// Returns [`GnssTimeError::Overflow`] if the result cannot be represented.
320    #[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////////////////////////////////////////////////////////////////////////////////
398// Tests
399////////////////////////////////////////////////////////////////////////////////
400
401#[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); // 1.500000001 s
505        let f = d.as_seconds_f64();
506
507        // f64 has ~15 significant digits; 1.500000001 requires 10 → represented exactly
508        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        // важно: trunc toward zero
652        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}