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