deep_time/dt/mod.rs
1mod arithmetic;
2mod arithmetic_calendar;
3mod constructors;
4mod conveniences;
5mod conversions;
6mod et;
7mod from_ccsds;
8mod from_str;
9mod gregorian;
10mod julian_date;
11
12mod ops;
13mod to_bin_ccsds;
14mod to_str;
15
16pub mod lunar;
17pub mod numbers_traits;
18
19#[cfg(feature = "alloc")]
20mod to_str_ccsds;
21
22#[cfg(feature = "hifitime")]
23mod hifitime;
24
25#[cfg(feature = "chrono")]
26mod chrono;
27
28#[cfg(feature = "jiff")]
29mod jiff;
30
31#[cfg(feature = "mars")]
32pub mod mars;
33
34#[cfg(feature = "tdb_fairhead1990")]
35pub mod tdb_fairhead1990;
36
37#[cfg(not(feature = "tdb_fairhead1990"))]
38mod tdb;
39
40use crate::{ATTOS_PER_SEC, Scale};
41use core::fmt;
42
43/// **The library's central time type.** A high-precision instant/duration with attosecond
44/// resolution.
45///
46/// **Fields:**
47///
48/// - pub attos: [`i128`] - total time in attoseconds since the reference epoch
49/// (2000-01-01 noon), as a signed integer. Negative values represent times
50/// before the epoch.
51/// - pub scale: [`Scale`] - the current time scale of the object.
52/// - pub target: [`Scale`] - a target time scale used by many output functions such as
53/// [`Dt::to_ymd`](../struct.Dt.html#method.to_ymd) and
54/// [`Dt::to_unix`](../struct.Dt.html#method.to_unix). The functions convert to the
55/// `target` time scale before producing an output.
56///
57/// **Notes:**
58///
59/// - In theory it supports a range of roughly ±5.39 trillion years but many of the to and
60/// from functions cap at i64 seconds, which can mean a range of ±292 billion years in practice.
61/// Additionally, when parsing dates with a timezone the Rust library `jiff` is used which has
62/// a limit of `-9999 - 9999` years.
63/// - Implements `Copy` and `Clone`. Optional derives for `serde` and `tsify` are available
64/// behind the corresponding features.
65/// - A wide range of math is available for this type, including basic calendar aware math and,
66/// with the `jiff-tz` feature enabled, timezone and DST aware math. **Behavior greatly
67/// differs between functions.**
68/// - **Comparison** (`==`, `Ord`, and [`Dt::cmp`](../struct.Dt.html#method.cmp)) uses only the
69/// `attos` field. `scale` and `target` are not consulted and no time-scale conversion is
70/// performed. To test whether two values denote the same physical instant, convert both to a
71/// common scale (e.g. with [`Dt::to`](../struct.Dt.html#method.to)) before comparing.
72///
73/// ```rust
74/// use deep_time::{Dt, Scale};
75///
76/// let tai = Dt::ZERO;
77/// let relabeled = tai.with(Scale::TT); // relabels scale only — attos unchanged
78///
79/// assert_eq!(tai, relabeled);
80/// assert_ne!(tai, tai.to(Scale::TT)); // .to() converts attos — no longer equal
81/// ```
82///
83/// ## Reference epoch and scales
84///
85/// - The librarys epoch for nearly all functionality such as the conversion functions is
86/// **2000-01-01 noon**. See also: [`Scale`](../enum.Scale.html).
87/// - Leap-second handling follows the chosen `Scale` (UTC, UtcSpice, UtcHist).
88///
89/// ## See also (non-exhaustive list)
90///
91/// ### From and to calendar dates
92///
93/// - [`Dt::from_ymd`](../struct.Dt.html#method.from_ymd)
94/// - [`Dt::to_ymd`](../struct.Dt.html#method.to_ymd)
95///
96/// ### From and to str and bytes
97///
98/// Some of these require the alloc feature, they're marked with *
99///
100/// - [`Dt::from_str_parse`](../struct.Dt.html#method.from_str_parse)*
101/// - [`Dt::from_str_iso`](../struct.Dt.html#method.from_str_iso)
102/// - [`Dt::parse`](../struct.Dt.html#method.parse)
103/// - [`Dt::from_str`](../struct.Dt.html#method.from_str)
104/// - [`Dt::to_str`](../struct.Dt.html#method.to_str)*
105/// - [`Dt::to_str_in_offset`](../struct.Dt.html#method.to_str_in_offset)*
106/// - [`Dt::to_str_in_tz`](../struct.Dt.html#method.to_str_in_tz)*
107/// - [`Dt::to_str_iso8601`](../struct.Dt.html#method.to_str_iso8601)*
108/// - [`Dt::to_str_lite`](../struct.Dt.html#method.to_str_lite)
109/// - [`Dt::to_str_lite_in_offset`](../struct.Dt.html#method.to_str_lite_in_offset)
110/// - [`Dt::to_str_lite_in_tz`](../struct.Dt.html#method.to_str_lite_in_tz)
111///
112/// ### From and to julian dates
113///
114/// - [`Dt::from_jd_f`](../struct.Dt.html#method.from_jd_f)
115/// - [`Dt::from_mjd_f`](../struct.Dt.html#method.from_mjd_f)
116/// - [`Dt::to_jd_f`](../struct.Dt.html#method.to_jd_f)
117/// - [`Dt::to_mjd_f`](../struct.Dt.html#method.to_mjd_f)
118/// - [`Dt::ymd_to_jd`](../struct.Dt.html#method.ymd_to_jd)
119/// - [`Dt::jd_to_ymd`](../struct.Dt.html#method.jd_to_ymd)
120///
121/// ### Conversions, time scales etc.
122///
123/// - [`Dt::target`](../struct.Dt.html#method.target)
124/// - [`Dt::to`](../struct.Dt.html#method.to)
125/// - [`Dt::convert`](../struct.Dt.html#method.convert)
126/// - [`Dt::to_tai`](../struct.Dt.html#method.to_tai)
127/// - [`Dt::from_sec`](../struct.Dt.html#method.from_sec)
128/// - [`Dt::to_sec64`](../struct.Dt.html#method.to_sec64)
129/// - [`Dt::from_attos`](../struct.Dt.html#method.from_attos)
130/// - [`Dt::convert_internal`](../struct.Dt.html#method.convert_internal)
131/// - [`Dt::to_unix`](../struct.Dt.html#method.to_unix)
132/// - [`Dt::to_ntp`](../struct.Dt.html#method.to_ntp)
133/// - [`Dt::to_gps_wk_and_tow`](../struct.Dt.html#method.to_gps_wk_and_tow)
134///
135/// ### Conversions from and to types from other libraries
136///
137/// - [`Dt::to_hifitime_epoch`](../struct.Dt.html#method.to_hifitime_epoch)
138/// - [`Dt::to_jiff_timestamp`](../struct.Dt.html#method.to_jiff_timestamp)
139/// - [`Dt::to_chrono_datetime_utc`](../struct.Dt.html#method.to_chrono_datetime_utc)
140/// - [`Dt::from_hifitime_epoch`](../struct.Dt.html#method.from_hifitime_epoch)
141/// - [`Dt::from_jiff_timestamp`](../struct.Dt.html#method.from_jiff_timestamp)
142/// - [`Dt::from_chrono_datetime_utc`](../struct.Dt.html#method.from_chrono_datetime_utc)
143///
144/// ## Examples
145///
146/// ### Parsing a date
147///
148/// ```rust
149/// use deep_time::{Dt, Scale};
150///
151/// // uses impl FromStr but Dt::parse provides the same functionality
152/// let x: Dt = "2000-01-01 12:00:00".parse().unwrap();
153///
154/// let ymd = x.to_ymd();
155/// assert_eq!(ymd.yr(), 2000);
156/// assert_eq!(ymd.mo(), 1);
157/// assert_eq!(ymd.day(), 1);
158/// assert_eq!(ymd.hr(), 12);
159/// assert_eq!(ymd.min(), 0);
160/// assert_eq!(ymd.sec(), 0);
161/// assert_eq!(ymd.attos(), 0);
162/// ```
163///
164/// ### Outputting a date to string / bytes
165///
166/// ```rust
167/// # #[cfg(all(any(feature = "jiff-tz", feature = "jiff-tz-bundle"), feature = "parse"))]
168/// # {
169/// use deep_time::{Dt, Lang, Scale};
170///
171/// let x: Dt = "2000-01-01 12:00:00".parse().unwrap();
172///
173/// let s = x
174/// .to_str_in_tz("%A, %B %d, %Y %H:%M:%S %Q", "America/New_York", Lang::En)
175/// .unwrap();
176/// let b = x
177/// .to_str_lite_in_tz("%A, %B %d, %Y %H:%M:%S %Q", "America/New_York", Lang::En)
178/// .unwrap();
179///
180/// assert_eq!(s, "Saturday, January 01, 2000 07:00:00 America/New_York");
181/// assert_eq!(b.as_str(), "Saturday, January 01, 2000 07:00:00 America/New_York");
182/// # }
183/// ```
184///
185/// ### Creating a unix timestamp in milliseconds
186///
187/// ```rust
188/// use deep_time::{Dt, Scale};
189///
190/// // this fn converts from UTC and creates a TAI Dt
191/// let dt = Dt::from_ymd(2000, 1, 1, Scale::UTC, 12, 0, 0, 0);
192///
193/// // dt is internally TAI but has a UTC tag
194/// let unix_ms = dt.to_unix().to_ms();
195///
196/// // unix timestamp in ms for 2000-01-01 noon UTC
197/// assert_eq!(unix_ms, 946728000000);
198/// ```
199///
200/// ### Converting time scales
201///
202/// Many functions such as
203/// [`Dt::to_ymd`](../struct.Dt.html#method.to_ymd) will convert to
204/// `TAI` from the [`Dt`]s current `scale` then to the [`Dt`]s `target`
205/// [`Scale`] prior to producing an output.
206///
207/// So you don't necessarily have to convert time scales prior to using
208/// many of the output functions. You just have to change the `target`
209/// time scale.
210///
211/// #### Using the target field
212///
213/// ```rust
214/// use deep_time::{Dt, Lang, Scale};
215///
216/// // Leap seconds were added to the secounds count
217/// // This Dt has attos that are now on the TAI timescale
218/// let dt = Dt::from_ymd(2025, 1, 1, Scale::UTC, 0, 0, 0, 0);
219///
220/// // The internal target is currently UTC so we don't need to do
221/// // anything to output back to UTC and round trip
222/// let bytes = dt.to_str_lite("%d %m %Y %H:%M:%S", Lang::En).unwrap();
223///
224/// assert_eq!(bytes.as_str(), "01 01 2025 00:00:00");
225///
226/// // Perhaps we want to make a GPS timestamp out of our Dt
227/// // If we want it to be on the GPS time scale we have to set the
228/// // target prior to calling to_gps()
229/// let gps = dt.target(Scale::GPS).to_gps().to_sec_f();
230/// ```
231///
232/// #### Converting the internal attos to a new time scale
233///
234/// ```rust
235/// use deep_time::{Dt, Scale};
236///
237/// // this fn converts from UTC and creates a TAI Dt
238/// let dt = Dt::from_ymd(2000, 1, 1, Scale::UTC, 12, 0, 0, 0);
239///
240/// // to tdb
241/// let tdb = dt.to(Scale::TDB);
242///
243/// // then to tt, the current scale is TDB
244/// let tt = tdb.to(Scale::TT);
245///
246/// // then back to TAI
247/// let tai = tt.to(Scale::TAI);
248///
249/// // round trip equality
250/// assert_eq!(dt, tai);
251/// ```
252///
253/// ### Performing some basic calendar aware math
254///
255/// ```rust
256/// use deep_time::{Dt, Scale};
257///
258/// let x = Dt::from_ymd(2000, 2, 29, Scale::UTC, 0, 0, 0, 0).to_ymd();
259/// let x = x.add_yr(1);
260///
261/// assert_eq!(x.day(), 28);
262/// ```
263///
264/// ### Changing a dates format
265///
266/// ```rust
267/// use deep_time::{Dt, Lang, StrPTimeFmt};
268///
269/// let fmt = Dt::parse_fmt("%Y-%m-%dT%H:%M:%S").unwrap();
270///
271/// # #[cfg(feature = "alloc")]
272/// let s = fmt.to_str("2000-01-01T12:00:00", "%d %m %Y %H:%M:%S", false, false, false, Lang::En).unwrap();
273///
274/// # #[cfg(feature = "alloc")]
275/// assert_eq!(s, "01 01 2000 12:00:00", "expected: {}, got: {}", "01 01 2000 12:00:00", s);
276/// ```
277#[derive(Clone, Copy)]
278#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
279#[cfg_attr(feature = "tsify", derive(tsify::Tsify))]
280#[cfg_attr(feature = "defmt", derive(defmt::Format))]
281pub struct Dt {
282 pub attos: i128,
283 pub scale: Scale,
284 pub target: Scale,
285}
286
287impl Dt {
288 /// Returns a new [`Dt`] with the `target` field set to the given
289 /// `t` arg.
290 #[inline(always)]
291 pub const fn target(&self, t: Scale) -> Dt {
292 Dt::new(self.attos, self.scale, t)
293 }
294
295 /// Returns a new [`Dt`] with the `scale` field sr to the given
296 /// `s` arg.
297 ///
298 /// **Does NOT perform any time scale conversions**.
299 #[inline(always)]
300 pub const fn with(&self, s: Scale) -> Dt {
301 Dt::new(self.attos, s, self.target)
302 }
303}
304
305impl Default for Dt {
306 fn default() -> Dt {
307 Self::ZERO
308 }
309}
310
311impl fmt::Display for Dt {
312 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
313 let total = self.to_attos();
314 let precision = f.precision().unwrap_or(18).min(18);
315
316 let is_negative = total < 0;
317 let abs_attos = if is_negative {
318 total.wrapping_neg() as u128
319 } else {
320 total as u128
321 };
322
323 if is_negative {
324 f.write_str("-")?;
325 } else if f.sign_plus() {
326 f.write_str("+")?;
327 }
328
329 let attos_per_sec = ATTOS_PER_SEC as u128;
330 let whole_seconds = abs_attos / attos_per_sec;
331 let fractional_attos = abs_attos % attos_per_sec;
332
333 write!(f, "{}", whole_seconds)?;
334
335 if precision > 0 && fractional_attos > 0 {
336 let scale = 10u128.pow(18 - precision as u32);
337 let frac_value = fractional_attos / scale;
338
339 if frac_value > 0 {
340 f.write_str(".")?;
341
342 let mut digits = [0u8; 18];
343 let mut n = frac_value;
344
345 for i in (0..precision).rev() {
346 digits[i] = (n % 10) as u8;
347 n /= 10;
348 }
349
350 let last = digits[..precision]
351 .iter()
352 .rposition(|&d| d != 0)
353 .unwrap_or(0);
354
355 for &d in &digits[..=last] {
356 write!(f, "{}", d)?;
357 }
358 }
359 }
360
361 Ok(())
362 }
363}
364
365impl fmt::Debug for Dt {
366 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
367 f.debug_struct("Dt")
368 .field("attos", &self.to_attos())
369 .field("scale", &self.scale)
370 .field("target", &self.target)
371 .finish()
372 }
373}
374
375#[cfg(feature = "wire")]
376impl Dt {
377 /// Current wire format version.
378 pub const WIRE_VERSION: u8 = 1;
379
380 /// Size of the canonical wire representation in bytes.
381 pub const WIRE_SIZE: usize = 19;
382
383 /// Serializes this `Dt` into a fixed 18-byte little-endian buffer using the
384 /// `attos: i128` + `scale: Scale` representation.
385 ///
386 /// ## Wire Format
387 ///
388 /// - Byte `0`: Version (`WIRE_VERSION`)
389 /// - Bytes `[1..17]`: total attoseconds as little-endian `i128`
390 /// - Byte `17`: scale as `u8` (enum discriminant)
391 /// - Byte `18`: target as `u8` (enum discriminant)
392 pub fn to_wire_bytes(&self) -> [u8; Self::WIRE_SIZE] {
393 let mut buf = [0u8; Self::WIRE_SIZE];
394 buf[0] = Self::WIRE_VERSION;
395 buf[1..17].copy_from_slice(&self.attos.to_le_bytes());
396 buf[17] = self.target as u8;
397 buf
398 }
399
400 /// Deserializes a [`Dt`] from exactly 18 bytes of wire data.
401 ///
402 /// Returns `None` if the version byte is unknown, the length is wrong,
403 /// or the scale byte is not a valid `Scale` variant.
404 ///
405 /// ## Wire Format
406 ///
407 /// - Byte `0`: Version (`WIRE_VERSION`)
408 /// - Bytes `[1..17]`: total attoseconds as little-endian `i128`
409 /// - Byte `17`: scale as `u8` (enum discriminant)
410 /// - Byte `18`: target as `u8` (enum discriminant)
411 ///
412 /// ## Security
413 ///
414 /// Safe to call with completely untrusted input.
415 pub fn from_wire_bytes(bytes: &[u8]) -> Option<Self> {
416 if bytes.len() != Self::WIRE_SIZE {
417 return None;
418 }
419
420 if bytes[0] != Self::WIRE_VERSION {
421 return None;
422 }
423
424 let attos = i128::from_le_bytes([
425 bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], bytes[8],
426 bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15], bytes[16],
427 ]);
428
429 let scale = Scale::from_u8(bytes[17]);
430 let target = Scale::from_u8(bytes[18]);
431
432 Some(Dt::new(attos, scale, target))
433 }
434}