Skip to main content

deep_time/dt/
mod.rs

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