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
98///
99/// Functionality:
100/// <https://github.com/ragardner/deep-time#overview>
101///
102/// Feature flags:
103/// <https://github.com/ragardner/deep-time#feature-flags>
104///
105/// Non-exhaustive list of functions:
106///
107/// ### From and to calendar dates
108///
109/// - [`Dt::from_ymd`](../struct.Dt.html#method.from_ymd)
110/// - [`Dt::to_ymd`](../struct.Dt.html#method.to_ymd)
111///
112/// ### From and to str and bytes
113///
114/// Some of these require the alloc feature, they're marked with *
115///
116/// - [`Dt::from_str_parse`](../struct.Dt.html#method.from_str_parse)*
117/// - [`Dt::from_str`](../struct.Dt.html#method.from_str)
118/// - [`Dt::parse`](../struct.Dt.html#method.parse)
119/// - [`Dt::from_strptime`](../struct.Dt.html#method.from_strptime)
120/// - [`Dt::to_str`](../struct.Dt.html#method.to_str)*
121/// - [`Dt::to_str_in_offset`](../struct.Dt.html#method.to_str_in_offset)*
122/// - [`Dt::to_str_in_tz`](../struct.Dt.html#method.to_str_in_tz)*
123/// - [`Dt::to_str_iso8601`](../struct.Dt.html#method.to_str_iso8601)*
124/// - [`Dt::to_str_b`](../struct.Dt.html#method.to_str_b)
125/// - [`Dt::to_str_b_in_offset`](../struct.Dt.html#method.to_str_b_in_offset)
126/// - [`Dt::to_str_b_in_tz`](../struct.Dt.html#method.to_str_b_in_tz)
127///
128/// ### From and to julian dates
129///
130/// - [`Dt::from_jd_f`](../struct.Dt.html#method.from_jd_f)
131/// - [`Dt::from_mjd_f`](../struct.Dt.html#method.from_mjd_f)
132/// - [`Dt::to_jd_f`](../struct.Dt.html#method.to_jd_f)
133/// - [`Dt::to_mjd_f`](../struct.Dt.html#method.to_mjd_f)
134/// - [`Dt::ymd_to_jd`](../struct.Dt.html#method.ymd_to_jd)
135/// - [`Dt::jd_to_ymd`](../struct.Dt.html#method.jd_to_ymd)
136///
137/// ### Conversions, time scales etc.
138///
139/// - [`Dt::target`](../struct.Dt.html#method.target)
140/// - [`Dt::to`](../struct.Dt.html#method.to)
141/// - [`Dt::to_tai`](../struct.Dt.html#method.to_tai)
142/// - [`Dt::convert`](../struct.Dt.html#method.convert)
143/// - [`Dt::from_sec`](../struct.Dt.html#method.from_sec)
144/// - [`Dt::to_sec64_floor`](../struct.Dt.html#method.to_sec64_floor)
145/// - [`Dt::new`](../struct.Dt.html#method.new)
146/// - [`Dt::to_unix`](../struct.Dt.html#method.to_unix)
147/// - [`Dt::to_ntp`](../struct.Dt.html#method.to_ntp)
148/// - [`Dt::to_gps_wk_and_tow`](../struct.Dt.html#method.to_gps_wk_and_tow)
149///
150/// ### Conversions from and to types from other libraries
151///
152/// - [`Dt::to_hifitime_epoch`](../struct.Dt.html#method.to_hifitime_epoch)
153/// - [`Dt::to_jiff_timestamp`](../struct.Dt.html#method.to_jiff_timestamp)
154/// - [`Dt::to_chrono_datetime_utc`](../struct.Dt.html#method.to_chrono_datetime_utc)
155/// - [`Dt::to_time_timestamp`](../struct.Dt.html#method.to_time_timestamp)
156/// - [`Dt::from_hifitime_epoch`](../struct.Dt.html#method.from_hifitime_epoch)
157/// - [`Dt::from_jiff_timestamp`](../struct.Dt.html#method.from_jiff_timestamp)
158/// - [`Dt::from_chrono_datetime_utc`](../struct.Dt.html#method.from_chrono_datetime_utc)
159/// - [`Dt::from_time_timestamp`](../struct.Dt.html#method.from_time_timestamp)
160///
161/// ## Examples
162///
163/// ### Parsing a date
164///
165/// Without alloc
166///
167/// ```rust
168/// use deep_time::{Dt, Scale};
169///
170/// // uses impl FromStr but Dt::parse provides the same functionality
171/// let x: Dt = "2000-01-01 12:00:00".parse().unwrap();
172/// let x = Dt::from_str("2000-01-01 12:00:00").unwrap();
173/// let x = Dt::from_strptime(
174///     "2000-01-01 12:00:00",
175///     "%Y-%m-%d %H:%M:%S",
176///     false,
177///     false,
178///     false,
179/// )
180/// .unwrap();
181///
182/// let ymd = x.to_ymd();
183/// assert_eq!(ymd.yr(), 2000);
184/// assert_eq!(ymd.mo(), 1);
185/// assert_eq!(ymd.day(), 1);
186/// assert_eq!(ymd.hr(), 12);
187/// assert_eq!(ymd.min(), 0);
188/// assert_eq!(ymd.sec(), 0);
189/// assert_eq!(ymd.attos(), 0);
190/// ```
191///
192/// With the lenient, auto-parser (`parse` feature; IANA zones need `jiff-tz`):
193///
194/// ```rust
195/// # #[cfg(all(feature = "parse", any(feature = "jiff-tz", feature = "jiff-tz-bundle")))]
196/// # {
197/// use deep_time::{Dt, ParseCfg, Scale};
198///
199/// let cfg = ParseCfg::default();
200///
201/// // leading junk, dotted date, 12-hour clock, IANA zone in brackets, trailing junk
202/// let dt = Dt::from_str_parse(
203///     "log >>> 15-Aug-2024 2:30pm [America/New_York] done",
204///     &cfg,
205/// )
206/// .unwrap();
207///
208/// // same instant as 18:30 UTC (EDT is UTC−4)
209/// let expected = Dt::from_ymd(2024, 8, 15, Scale::UTC, 18, 30, 0, 0);
210/// assert_eq!(dt, expected);
211/// # }
212/// ```
213///
214/// ### Outputting a date to string / bytes
215///
216/// ```rust
217/// # #[cfg(all(any(feature = "jiff-tz", feature = "jiff-tz-bundle"), feature = "parse"))]
218/// # {
219/// use deep_time::{Dt, Lang, Scale};
220///
221/// let x: Dt = "2000-01-01 12:00:00".parse().unwrap();
222///
223/// let s = x
224///  .to_str_in_tz("%A, %B %d, %Y %H:%M:%S %Q", "America/New_York", Lang::En)
225///  .unwrap();
226/// let b = x
227///  .to_str_b_in_tz("%A, %B %d, %Y %H:%M:%S %Q", "America/New_York", Lang::En)
228///  .unwrap();
229///
230/// assert_eq!(s, "Saturday, January 01, 2000 07:00:00 America/New_York");
231/// assert_eq!(b.as_str(), "Saturday, January 01, 2000 07:00:00 America/New_York");
232/// # }
233/// ```
234///
235/// ### Creating a unix timestamp in milliseconds
236///
237/// ```rust
238/// use deep_time::{Dt, Scale};
239///
240/// // this fn converts from UTC and creates a TAI Dt
241/// let dt = Dt::from_ymd(2000, 1, 1, Scale::UTC, 12, 0, 0, 0);
242///
243/// // dt is internally TAI but has a UTC tag
244/// let unix_ms = dt.to_unix().to_ms().0;
245///
246/// // unix timestamp in ms for 2000-01-01 noon UTC
247/// assert_eq!(unix_ms, 946728000000);
248/// ```
249///
250/// ### Converting time scales
251///
252/// Many functions such as
253/// [`Dt::to_ymd`](../struct.Dt.html#method.to_ymd) will convert to
254/// `TAI` from the [`Dt`]s current `scale` then to the [`Dt`]s `target`
255/// [`Scale`] prior to producing an output.
256///
257/// So you don't necessarily have to convert time scales prior to using
258/// many of the output functions. You just have to change the `target`
259/// time scale.
260///
261/// #### Using the target field
262///
263/// ```rust
264/// use deep_time::{Dt, Lang, Scale};
265///
266/// // Leap seconds were added to the seconds count
267/// // This Dt has attos that are now on the TAI timescale
268/// let dt = Dt::from_ymd(2025, 1, 1, Scale::UTC, 0, 0, 0, 0);
269///
270/// // The internal target is currently UTC so we don't need to do
271/// // anything to output back to UTC and round trip
272/// let bytes = dt.to_str_b("%d %m %Y %H:%M:%S", Lang::En).unwrap();
273///
274/// assert_eq!(bytes.as_str(), "01 01 2025 00:00:00");
275///
276/// // Perhaps we want to make a GPS timestamp out of our Dt
277/// // If we want it to be on the GPS time scale we have to set the
278/// // target prior to calling to_gps()
279/// let gps = dt.target(Scale::GPS).to_gps().to_sec_f();
280/// ```
281///
282/// #### Converting the internal attos to a new time scale
283///
284/// ```rust
285/// use deep_time::{Dt, Scale};
286///
287/// // this fn converts from UTC and creates a TAI Dt
288/// let dt = Dt::from_ymd(2000, 1, 1, Scale::UTC, 12, 0, 0, 0);
289///
290/// // to tdb
291/// let tdb = dt.to(Scale::TDB);
292///
293/// // then to tt, the current scale is TDB
294/// let tt = tdb.to(Scale::TT);
295///
296/// // then back to TAI
297/// let tai = tt.to(Scale::TAI);
298///
299/// // round trip equality
300/// assert_eq!(dt, tai);
301/// ```
302///
303/// ### Performing some basic calendar aware math
304///
305/// ```rust
306/// use deep_time::{Dt, Scale};
307///
308/// let x = Dt::from_ymd(2000, 2, 29, Scale::UTC, 0, 0, 0, 0).to_ymd();
309/// let x = x.add_years(1);
310///
311/// assert_eq!(x.day(), 28);
312/// ```
313///
314/// ### Comparisons
315///
316/// ```rust
317/// use deep_time::macros::from_ymd;
318/// use deep_time::{Dt, Scale};
319///
320/// let a = from_ymd!(2000, 1, 1; 12, on=Scale::TAI);
321/// let mut b = Dt::from_str("2000-01-01T12 TAI").unwrap();
322///
323/// // same instant but on the TT time scale
324/// b = b.to(Scale::TT);
325///
326/// // comparisons only use the attos field
327/// // changing b to TT has changed its attos
328/// assert_ne!(a, b);
329///
330/// // to check if two Dt's are the same instant
331/// // they must be on the same time scale and
332/// // from the same epoch
333/// b = b.to(Scale::TAI);
334/// assert_eq!(a, b);
335///
336/// // Dt also allows various mathematical operations
337/// b = b.to(Scale::UTC);
338/// let diff = (a - b).to_sec();
339/// assert_eq!(diff, 32);
340/// ```
341///
342/// #### Sorting
343///
344/// ```rust
345/// # #[cfg(feature = "alloc")]
346/// # {
347/// use deep_time::macros::from_ymd;
348/// use deep_time::{Dt, Scale};
349///
350/// let mut times = vec![
351///     from_ymd!(2000, 1, 3),
352///     from_ymd!(2000, 1, 1),
353///     from_ymd!(2000, 1, 2),
354/// ];
355///
356/// // sort uses Ord, which only looks at attos (not scale / target)
357/// times.sort();
358///
359/// assert_eq!(times[0], from_ymd!(2000, 1, 1));
360/// assert_eq!(times[1], from_ymd!(2000, 1, 2));
361/// assert_eq!(times[2], from_ymd!(2000, 1, 3));
362/// # }
363/// ```
364#[derive(Clone, Copy)]
365#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
366#[cfg_attr(feature = "tsify", derive(tsify::Tsify))]
367#[cfg_attr(feature = "defmt", derive(defmt::Format))]
368pub struct Dt {
369    /// Signed attosecond count.
370    ///
371    /// [`Dt`] can represent a duration or an instant. As a duration, `attos` is
372    /// simply an elapsed span. As an instant, it is an offset from some epoch —
373    /// any epoch may apply depending on how the value is constructed and used.
374    ///
375    /// Calendar and conversion APIs commonly interpret it relative to the library
376    /// epoch (2000-01-01 noon), but the field itself is only a count of attoseconds.
377    pub attos: i128,
378    /// The current time scale of this object.
379    pub scale: Scale,
380    /// Target time scale used by many output functions such as
381    /// [`Dt::to_ymd`](../struct.Dt.html#method.to_ymd)
382    /// and
383    /// [`Dt::to_unix`](../struct.Dt.html#method.to_unix).
384    ///
385    /// These functions convert to this scale before producing output.
386    pub target: Scale,
387}
388
389impl Dt {
390    /// Returns a new [`Dt`] with the `target` field set to the given
391    /// `t` arg.
392    #[inline(always)]
393    pub const fn target(&self, t: Scale) -> Dt {
394        Dt::new(self.attos, self.scale, t)
395    }
396
397    /// Returns a new [`Dt`] with the `scale` field set to the given
398    /// `s` arg.
399    ///
400    /// **Does NOT perform any time scale conversions**.
401    #[inline(always)]
402    pub const fn with(&self, s: Scale) -> Dt {
403        Dt::new(self.attos, s, self.target)
404    }
405}
406
407impl Default for Dt {
408    fn default() -> Dt {
409        Self::ZERO
410    }
411}
412
413impl fmt::Display for Dt {
414    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
415        let total = self.to_attos();
416        let precision = f.precision().unwrap_or(18).min(18);
417
418        let is_negative = total < 0;
419        let abs_attos = if is_negative {
420            total.wrapping_neg() as u128
421        } else {
422            total as u128
423        };
424
425        if is_negative {
426            f.write_str("-")?;
427        } else if f.sign_plus() {
428            f.write_str("+")?;
429        }
430
431        let attos_per_sec = ATTOS_PER_SEC as u128;
432        let whole_seconds = abs_attos / attos_per_sec;
433        let fractional_attos = abs_attos % attos_per_sec;
434
435        write!(f, "{}", whole_seconds)?;
436
437        if precision > 0 && fractional_attos > 0 {
438            let scale = 10u128.pow(18 - precision as u32);
439            let frac_value = fractional_attos / scale;
440
441            if frac_value > 0 {
442                f.write_str(".")?;
443
444                let mut digits = [0u8; 18];
445                let mut n = frac_value;
446
447                for i in (0..precision).rev() {
448                    digits[i] = (n % 10) as u8;
449                    n /= 10;
450                }
451
452                let last = digits[..precision]
453                    .iter()
454                    .rposition(|&d| d != 0)
455                    .unwrap_or(0);
456
457                for &d in &digits[..=last] {
458                    write!(f, "{}", d)?;
459                }
460            }
461        }
462
463        Ok(())
464    }
465}
466
467impl fmt::Debug for Dt {
468    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
469        f.debug_struct("Dt")
470            .field("attos", &self.to_attos())
471            .field("scale", &self.scale)
472            .field("target", &self.target)
473            .finish()
474    }
475}
476
477#[cfg(feature = "wire")]
478impl Dt {
479    /// Current wire format version.
480    pub const WIRE_VERSION: u8 = 1;
481
482    /// Size of the canonical wire representation in bytes.
483    pub const WIRE_SIZE: usize = 19;
484
485    /// Serializes this `Dt` into a fixed 19-byte little-endian buffer using the
486    /// `attos: i128` + `scale: Scale` + `target: Scale` representation.
487    ///
488    /// ## Wire Format
489    ///
490    /// - Byte `0`: Version (`WIRE_VERSION`)
491    /// - Bytes `[1..17]`: total attoseconds as little-endian `i128`
492    /// - Byte `17`: scale as `u8`
493    /// - Byte `18`: target as `u8`
494    pub fn to_wire_bytes(&self) -> [u8; Self::WIRE_SIZE] {
495        let mut buf = [0u8; Self::WIRE_SIZE];
496        buf[0] = Self::WIRE_VERSION;
497        buf[1..17].copy_from_slice(&self.attos.to_le_bytes());
498        buf[17] = self.scale as u8;
499        buf[18] = self.target as u8;
500        buf
501    }
502
503    /// Deserializes a [`Dt`] from exactly 19 bytes of wire data.
504    ///
505    /// ## Errors
506    ///
507    /// Returns `None` only when:
508    /// - `bytes` is not exactly [`WIRE_SIZE`](Self::WIRE_SIZE) long, or
509    /// - the version byte is not [`WIRE_VERSION`](Self::WIRE_VERSION).
510    ///
511    /// Scale and target never cause failure: each is passed through
512    /// [`Scale::from_u8`], which substitutes [`Scale::Custom`] for any
513    /// unrecognized value.
514    ///
515    /// ## Wire Format
516    ///
517    /// - Byte `0`: Version (`WIRE_VERSION`)
518    /// - Bytes `[1..17]`: total attoseconds as little-endian `i128`
519    /// - Byte `17`: scale as `u8`
520    /// - Byte `18`: target as `u8`
521    ///
522    /// ## Security
523    ///
524    /// Safe to call with completely untrusted input.
525    pub fn from_wire_bytes(bytes: &[u8]) -> Option<Self> {
526        if bytes.len() != Self::WIRE_SIZE {
527            return None;
528        }
529
530        if bytes[0] != Self::WIRE_VERSION {
531            return None;
532        }
533
534        let attos = i128::from_le_bytes([
535            bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], bytes[8],
536            bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15], bytes[16],
537        ]);
538
539        let scale = Scale::from_u8(bytes[17]);
540        let target = Scale::from_u8(bytes[18]);
541
542        Some(Dt::new(attos, scale, target))
543    }
544}