Skip to main content

deep_time/dt/
mod.rs

1mod arithmetic;
2mod constructors;
3mod conversions;
4mod decimal_year;
5mod from_ccsds;
6mod from_gps;
7mod from_str;
8mod gregorian;
9mod julian_date;
10mod ops;
11mod tdb;
12mod to_ccsds_bin;
13mod to_gps;
14mod to_str;
15
16pub mod lunar;
17pub mod numbers_traits;
18pub mod trajectory;
19
20#[cfg(feature = "alloc")]
21mod formatting;
22#[cfg(feature = "alloc")]
23mod to_ccsds_str;
24
25#[cfg(feature = "mars")]
26pub mod mars;
27
28#[cfg(feature = "hifitime")]
29mod hifitime;
30
31#[cfg(feature = "chrono")]
32mod chrono;
33
34#[cfg(feature = "jiff")]
35mod jiff;
36
37use crate::ATTOS_PER_SEC;
38use core::fmt;
39
40/// Dt, and the library, is in the process of being switched from the sec
41/// and subsec fields being related to the scale, TO the sec and subsec fields
42/// always being TAI Epoch 2000-01-01 noon.
43/// Much of the documentation is outdated and should be ignored.
44#[derive(Clone, Copy)]
45#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
46#[cfg_attr(feature = "js", derive(tsify::Tsify))]
47pub struct Dt {
48    pub(crate) sec: i64,
49    pub(crate) attos: u64,
50}
51
52impl Dt {
53    /// Seconds field getter.
54    #[inline]
55    pub const fn sec(&self) -> i64 {
56        self.sec
57    }
58
59    /// Subseconds field getter (attoseconds).
60    #[inline]
61    pub const fn attos(&self) -> u64 {
62        self.attos
63    }
64
65    /// Normalizes the representation so that the attosecond part lies in the range `[0, ATTOS_PER_SEC)`.
66    #[inline]
67    pub const fn carry_over_mut(&mut self) -> &mut Self {
68        if self.attos >= ATTOS_PER_SEC {
69            self.sec = self.sec.saturating_add((self.attos / ATTOS_PER_SEC) as i64);
70            self.attos %= ATTOS_PER_SEC;
71        }
72        self
73    }
74
75    /// Normalizes the representation so that the attosecond part lies in the range `[0, ATTOS_PER_SEC)`.
76    #[inline]
77    pub const fn carry_over(&self) -> Self {
78        if self.attos < ATTOS_PER_SEC {
79            return *self;
80        }
81        Self {
82            sec: self.sec.saturating_add((self.attos / ATTOS_PER_SEC) as i64),
83            attos: self.attos % ATTOS_PER_SEC,
84        }
85    }
86}
87
88impl Default for Dt {
89    fn default() -> Self {
90        Self::ZERO
91    }
92}
93
94impl fmt::Display for Dt {
95    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96        let sec = self.sec();
97        let attos = self.attos();
98
99        // Default to nanosecond precision (9 digits) — most useful for everyday use
100        let precision = f.precision().unwrap_or(9);
101
102        // Respect the `+` sign when the user writes {:+}
103        if f.sign_plus() && sec >= 0 {
104            write!(f, "+")?;
105        }
106
107        write!(f, "{}", sec)?;
108
109        if precision > 0 {
110            let prec = precision.min(18);
111            let scale = 10u64.pow(18 - prec as u32);
112            let value = attos / scale;
113            write!(f, ".{:0>width$}", value, width = prec)?;
114        }
115
116        Ok(())
117    }
118}
119
120impl fmt::Debug for Dt {
121    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122        f.debug_struct("Dt")
123            .field("sec", &self.sec())
124            .field("attos", &self.attos())
125            .finish()
126    }
127}
128
129#[cfg(feature = "wire")]
130impl Dt {
131    /// Current wire format version.
132    pub const WIRE_VERSION: u8 = 1;
133
134    /// Size of the canonical wire representation in bytes (17 bytes).
135    pub const WIRE_SIZE: usize = 17;
136
137    /// Serializes this `Dt` into a fixed 17-byte little-endian buffer.
138    ///
139    /// # Wire Format
140    ///
141    /// - Byte `0`: Version (`WIRE_VERSION`)
142    /// - Bytes `[1..9]`: `sec` as little-endian `i64`
143    /// - Bytes `[9..17]`: `subsec` as little-endian `u64`
144    ///
145    /// This format is stable, portable, and suitable for network transmission,
146    /// file storage, or FFI. The internal representation is always TAI.
147    pub fn to_wire_bytes(&self) -> [u8; Self::WIRE_SIZE] {
148        let mut buf = [0u8; Self::WIRE_SIZE];
149        buf[0] = Self::WIRE_VERSION;
150        buf[1..9].copy_from_slice(&self.sec.to_le_bytes());
151        buf[9..17].copy_from_slice(&self.attos.to_le_bytes());
152        buf
153    }
154
155    /// Deserializes a `Dt` from exactly 17 bytes of wire data.
156    ///
157    /// Returns `None` if the version byte is unknown.
158    /// Any `subsec` value ≥ 10¹⁸ is automatically normalized using
159    /// [`carry_over`](Self::carry_over) so the resulting `Dt`
160    /// is always in canonical form.
161    ///
162    /// ## Security
163    ///
164    /// Safe to call with completely untrusted input. Fixed-size format,
165    /// no allocation, no `unsafe`, and no possibility of code execution.
166    /// Malicious data simply produces a normalized (but still valid) `Dt`.
167    pub fn from_wire_bytes(bytes: &[u8]) -> Option<Self> {
168        if bytes.len() != Self::WIRE_SIZE {
169            return None;
170        }
171
172        if bytes[0] != Self::WIRE_VERSION {
173            return None;
174        }
175
176        let sec = i64::from_le_bytes([
177            bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], bytes[8],
178        ]);
179        let subsec = u64::from_le_bytes([
180            bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15], bytes[16],
181        ]);
182
183        Some(Self::new(sec, subsec))
184    }
185}