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