Skip to main content

deep_time/dt/
mod.rs

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