Skip to main content

deep_time/dt/
mod.rs

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