Skip to main content

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