Skip to main content

deep_time/dt/
mod.rs

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