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/// ### Display / `.to_string()`
215///
216/// - **`{}`** — `[seconds scale>target]`, e.g. `[86400s TAI>UTC]`. At full
217///   precision this form round-trips through
218///   [`Dt::from_str`](../struct.Dt.html#method.from_str) (and with `parse`,
219///   through [`Dt::parse`](../struct.Dt.html#method.parse) /
220///   `Dt::from_str_parse`). Parsing it does not convert scales.
221/// - **`{:#}`** — formats [`Dt::to_ymd`](../struct.Dt.html#method.to_ymd) as
222///   `YYYY-MM-DDTHH:MM:SS[.frac] TARGET`, e.g. `2000-01-01T12:00:00 UTC`.
223/// - **`{:.n}`** / **`{:#.n}`** — at most `n` fractional digits (clamped to 18),
224///   truncated, trailing zeros trimmed.
225///
226/// ```rust
227/// # #[cfg(feature = "std")]
228/// # {
229/// use deep_time::{Dt, Scale};
230/// use deep_time::macros::from_sec;
231///
232/// assert_eq!(Dt::ZERO.to_string(), "[0s TAI>TAI]");
233///
234/// let dt = from_sec!(86400, on = Scale::TAI, target = Scale::UTC);
235/// assert_eq!(dt.to_string(), "[86400s TAI>UTC]");
236///
237/// let back = Dt::from_str(&dt.to_string()).unwrap();
238/// assert_eq!(back.attos, dt.attos);
239/// assert_eq!(back.scale, Scale::TAI);
240/// assert_eq!(back.target, Scale::UTC);
241///
242/// assert_eq!(format!("{dt:#}"), "2000-01-02T11:59:28 UTC");
243/// # }
244/// ```
245///
246/// ### Outputting a date to string / bytes
247///
248/// ```rust
249/// # #[cfg(all(any(feature = "jiff-tz", feature = "jiff-tz-bundle"), feature = "parse"))]
250/// # {
251/// use deep_time::{Dt, Lang, Scale};
252///
253/// let x: Dt = "2000-01-01 12:00:00".parse().unwrap();
254///
255/// let s = x
256///  .to_str_in_tz("%A, %B %d, %Y %H:%M:%S %Q", "America/New_York", Lang::En)
257///  .unwrap();
258/// let b = x
259///  .to_str_b_in_tz("%A, %B %d, %Y %H:%M:%S %Q", "America/New_York", Lang::En)
260///  .unwrap();
261///
262/// assert_eq!(s, "Saturday, January 01, 2000 07:00:00 America/New_York");
263/// assert_eq!(b.as_str(), "Saturday, January 01, 2000 07:00:00 America/New_York");
264/// # }
265/// ```
266///
267/// ### Creating a unix timestamp in milliseconds
268///
269/// ```rust
270/// use deep_time::{Dt, Scale};
271///
272/// // this fn converts from UTC and creates a TAI Dt
273/// let dt = Dt::from_ymd(2000, 1, 1, Scale::UTC, 12, 0, 0, 0);
274///
275/// // dt is internally TAI but has a UTC tag
276/// let unix_ms = dt.to_unix().to_ms().0;
277///
278/// // unix timestamp in ms for 2000-01-01 noon UTC
279/// assert_eq!(unix_ms, 946728000000);
280/// ```
281///
282/// ### Converting time scales
283///
284/// Many functions such as
285/// [`Dt::to_ymd`](../struct.Dt.html#method.to_ymd) will convert to
286/// `TAI` from the [`Dt`]s current `scale` then to the [`Dt`]s `target`
287/// [`Scale`] prior to producing an output.
288///
289/// So you don't necessarily have to convert time scales prior to using
290/// many of the output functions. You just have to change the `target`
291/// time scale.
292///
293/// #### Using the target field
294///
295/// ```rust
296/// use deep_time::{Dt, Lang, Scale};
297///
298/// // Leap seconds were added to the seconds count
299/// // This Dt has attos that are now on the TAI timescale
300/// let dt = Dt::from_ymd(2025, 1, 1, Scale::UTC, 0, 0, 0, 0);
301///
302/// // The internal target is currently UTC so we don't need to do
303/// // anything to output back to UTC and round trip
304/// let bytes = dt.to_str_b("%d %m %Y %H:%M:%S", Lang::En).unwrap();
305///
306/// assert_eq!(bytes.as_str(), "01 01 2025 00:00:00");
307///
308/// // Perhaps we want to make a GPS timestamp out of our Dt
309/// // If we want it to be on the GPS time scale we have to set the
310/// // target prior to calling to_gps()
311/// let gps = dt.target(Scale::GPS).to_gps().to_sec_f();
312/// ```
313///
314/// #### Converting the internal attos to a new time scale
315///
316/// ```rust
317/// use deep_time::{Dt, Scale};
318///
319/// // this fn converts from UTC and creates a TAI Dt
320/// let dt = Dt::from_ymd(2000, 1, 1, Scale::UTC, 12, 0, 0, 0);
321///
322/// // to tdb
323/// let tdb = dt.to(Scale::TDB);
324///
325/// // then to tt, the current scale is TDB
326/// let tt = tdb.to(Scale::TT);
327///
328/// // then back to TAI
329/// let tai = tt.to(Scale::TAI);
330///
331/// // round trip equality
332/// assert_eq!(dt, tai);
333/// ```
334///
335/// ### Performing some basic calendar aware math
336///
337/// ```rust
338/// use deep_time::{Dt, Scale};
339///
340/// let x = Dt::from_ymd(2000, 2, 29, Scale::UTC, 0, 0, 0, 0).to_ymd();
341/// let x = x.add_years(1);
342///
343/// assert_eq!(x.day(), 28);
344/// ```
345///
346/// ### Comparisons
347///
348/// ```rust
349/// use deep_time::macros::from_ymd;
350/// use deep_time::{Dt, Scale};
351///
352/// let a = from_ymd!(2000, 1, 1; 12, on=Scale::TAI);
353/// let mut b = Dt::from_str("2000-01-01T12 TAI").unwrap();
354///
355/// // same instant but on the TT time scale
356/// b = b.to(Scale::TT);
357///
358/// // comparisons only use the attos field
359/// // changing b to TT has changed its attos
360/// assert_ne!(a, b);
361///
362/// // to check if two Dt's are the same instant
363/// // they must be on the same time scale and
364/// // from the same epoch
365/// b = b.to(Scale::TAI);
366/// assert_eq!(a, b);
367///
368/// // Dt also allows various mathematical operations
369/// b = b.to(Scale::UTC);
370/// let diff = (a - b).to_sec();
371/// assert_eq!(diff, 32);
372/// ```
373///
374/// #### Sorting
375///
376/// ```rust
377/// # #[cfg(feature = "alloc")]
378/// # {
379/// use deep_time::macros::from_ymd;
380/// use deep_time::{Dt, Scale};
381///
382/// let mut times = vec![
383///     from_ymd!(2000, 1, 3),
384///     from_ymd!(2000, 1, 1),
385///     from_ymd!(2000, 1, 2),
386/// ];
387///
388/// // sort uses Ord, which only looks at attos (not scale / target)
389/// times.sort();
390///
391/// assert_eq!(times[0], from_ymd!(2000, 1, 1));
392/// assert_eq!(times[1], from_ymd!(2000, 1, 2));
393/// assert_eq!(times[2], from_ymd!(2000, 1, 3));
394/// # }
395/// ```
396#[derive(Clone, Copy)]
397#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
398#[cfg_attr(feature = "tsify", derive(tsify::Tsify))]
399#[cfg_attr(feature = "defmt", derive(defmt::Format))]
400pub struct Dt {
401    /// Signed attosecond count.
402    ///
403    /// [`Dt`] can represent a duration or an instant. As a duration, `attos` is
404    /// simply an elapsed span. As an instant, it is an offset from some epoch —
405    /// any epoch may apply depending on how the value is constructed and used.
406    ///
407    /// Calendar and conversion APIs commonly interpret it relative to the library
408    /// epoch (2000-01-01 noon), but the field itself is only a count of attoseconds.
409    pub attos: i128,
410    /// The current time scale of this object.
411    pub scale: Scale,
412    /// Target time scale used by many output functions such as
413    /// [`Dt::to_ymd`](../struct.Dt.html#method.to_ymd)
414    /// and
415    /// [`Dt::to_unix`](../struct.Dt.html#method.to_unix).
416    ///
417    /// These functions convert to this scale before producing output.
418    pub target: Scale,
419}
420
421impl Dt {
422    /// Returns a new [`Dt`] with the `target` field set to the given
423    /// `t` arg.
424    #[inline(always)]
425    pub const fn target(&self, t: Scale) -> Dt {
426        Dt::new(self.attos, self.scale, t)
427    }
428
429    /// Returns a new [`Dt`] with the `scale` field set to the given
430    /// `s` arg.
431    ///
432    /// **Does NOT perform any time scale conversions**.
433    #[inline(always)]
434    pub const fn with(&self, s: Scale) -> Dt {
435        Dt::new(self.attos, s, self.target)
436    }
437}
438
439impl Default for Dt {
440    fn default() -> Dt {
441        Self::ZERO
442    }
443}
444
445/// Formats a [`Dt`].
446///
447/// ## Forms
448///
449/// - **`{}`** — `[seconds scale>target]`, e.g. `[86400s TAI>UTC]`, `[-1.5s TT>GPS]`.
450/// - **`{:#}`** — formats [`Dt::to_ymd`](../struct.Dt.html#method.to_ymd) as
451///   `YYYY-MM-DDTHH:MM:SS[.frac] TARGET`, e.g. `2000-01-01T12:00:00 UTC`.
452///
453/// ## Fractional precision
454///
455/// `{:.n}` / `{:#.n}` keep at most `n` fractional digits (`n` clamped to
456/// `0..=18`), truncated (not rounded), trailing zeros trimmed. `{:.0}` /
457/// `{:#.0}` omit the fractional part. On the default form, `{:+}` forces a
458/// leading `+` when non-negative.
459///
460/// ## Examples
461///
462/// ```rust
463/// use core::fmt::Write;
464/// use deep_time::{BufStr, Dt, Scale};
465/// use deep_time::macros::from_sec;
466///
467/// let mut s = BufStr::<64>::default();
468/// write!(&mut s, "{}", Dt::ZERO).unwrap();
469/// assert_eq!(s.as_str(), "[0s TAI>TAI]");
470///
471/// let dt = from_sec!(86400, on = Scale::TAI, target = Scale::UTC);
472/// s = BufStr::<64>::default();
473/// write!(&mut s, "{}", dt).unwrap();
474/// assert_eq!(s.as_str(), "[86400s TAI>UTC]");
475///
476/// let dt = Dt::new(
477///     -1_500_000_000_000_000_000,
478///     Scale::TT,
479///     Scale::GPS,
480/// );
481/// s = BufStr::<64>::default();
482/// write!(&mut s, "{}", dt).unwrap();
483/// assert_eq!(s.as_str(), "[-1.5s TT>GPS]");
484///
485/// s = BufStr::<64>::default();
486/// write!(&mut s, "{:.0}", dt).unwrap();
487/// assert_eq!(s.as_str(), "[-1s TT>GPS]");
488/// s = BufStr::<64>::default();
489/// write!(&mut s, "{:.1}", dt).unwrap();
490/// assert_eq!(s.as_str(), "[-1.5s TT>GPS]");
491///
492/// let noon = Dt::from_ymd(2000, 1, 1, Scale::UTC, 12, 0, 0, 0);
493/// s = BufStr::<64>::default();
494/// write!(&mut s, "{noon:#}").unwrap();
495/// assert_eq!(s.as_str(), "2000-01-01T12:00:00 UTC");
496/// ```
497impl fmt::Display for Dt {
498    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
499        if f.alternate() {
500            return fmt::Display::fmt(&self.to_ymd(), f);
501        }
502
503        const MAX_FRAC: usize = 18;
504
505        let total = self.to_attos();
506        let is_negative = total < 0;
507        let abs_attos = if is_negative {
508            total.wrapping_neg() as u128
509        } else {
510            total as u128
511        };
512
513        let attos_per_sec = ATTOS_PER_SEC as u128;
514        let whole_seconds = abs_attos / attos_per_sec;
515        let fractional_attos = abs_attos % attos_per_sec;
516
517        // Build 18 fractional digits (attosecond places).
518        let mut digits = [0u8; MAX_FRAC];
519        if fractional_attos > 0 {
520            let mut n = fractional_attos;
521            for i in (0..MAX_FRAC).rev() {
522                digits[i] = (n % 10) as u8;
523                n /= 10;
524            }
525        }
526
527        // Format precision: max fractional digits (default = full attosecond).
528        // Truncate only — never round / never bump whole seconds.
529        let max_frac = f.precision().unwrap_or(MAX_FRAC).min(MAX_FRAC);
530
531        let frac_last = if max_frac > 0 {
532            digits[..max_frac].iter().rposition(|&d| d != 0)
533        } else {
534            None
535        };
536        // Avoid printing `-0s` when a negative value truncates to zero magnitude.
537        let shown_zero = whole_seconds == 0 && frac_last.is_none();
538
539        f.write_str("[")?;
540
541        if is_negative && !shown_zero {
542            f.write_str("-")?;
543        } else if f.sign_plus() {
544            f.write_str("+")?;
545        }
546
547        write!(f, "{whole_seconds}")?;
548
549        if let Some(last) = frac_last {
550            f.write_str(".")?;
551            for &d in &digits[..=last] {
552                write!(f, "{d}")?;
553            }
554        }
555
556        write!(f, "s {}>{}]", self.scale.abbrev(), self.target.abbrev())
557    }
558}
559
560impl fmt::Debug for Dt {
561    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
562        f.debug_struct("Dt")
563            .field("attos", &self.to_attos())
564            .field("scale", &self.scale)
565            .field("target", &self.target)
566            .finish()
567    }
568}
569
570#[cfg(feature = "wire")]
571impl Dt {
572    /// Current wire format version.
573    pub const WIRE_VERSION: u8 = 1;
574
575    /// Size of the canonical wire representation in bytes.
576    pub const WIRE_SIZE: usize = 19;
577
578    /// Serializes this `Dt` into a fixed 19-byte little-endian buffer using the
579    /// `attos: i128` + `scale: Scale` + `target: Scale` representation.
580    ///
581    /// ## Wire Format
582    ///
583    /// - Byte `0`: Version (`WIRE_VERSION`)
584    /// - Bytes `[1..17]`: total attoseconds as little-endian `i128`
585    /// - Byte `17`: scale as `u8`
586    /// - Byte `18`: target as `u8`
587    pub fn to_wire_bytes(&self) -> [u8; Self::WIRE_SIZE] {
588        let mut buf = [0u8; Self::WIRE_SIZE];
589        buf[0] = Self::WIRE_VERSION;
590        buf[1..17].copy_from_slice(&self.attos.to_le_bytes());
591        buf[17] = self.scale as u8;
592        buf[18] = self.target as u8;
593        buf
594    }
595
596    /// Deserializes a [`Dt`] from exactly 19 bytes of wire data.
597    ///
598    /// ## Errors
599    ///
600    /// Returns `None` only when:
601    /// - `bytes` is not exactly [`WIRE_SIZE`](Self::WIRE_SIZE) long, or
602    /// - the version byte is not [`WIRE_VERSION`](Self::WIRE_VERSION).
603    ///
604    /// Scale and target never cause failure: each is passed through
605    /// [`Scale::from_u8`], which substitutes [`Scale::Custom`] for any
606    /// unrecognized value.
607    ///
608    /// ## Wire Format
609    ///
610    /// - Byte `0`: Version (`WIRE_VERSION`)
611    /// - Bytes `[1..17]`: total attoseconds as little-endian `i128`
612    /// - Byte `17`: scale as `u8`
613    /// - Byte `18`: target as `u8`
614    ///
615    /// ## Security
616    ///
617    /// Safe to call with completely untrusted input.
618    pub fn from_wire_bytes(bytes: &[u8]) -> Option<Self> {
619        if bytes.len() != Self::WIRE_SIZE {
620            return None;
621        }
622
623        if bytes[0] != Self::WIRE_VERSION {
624            return None;
625        }
626
627        let attos = i128::from_le_bytes([
628            bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], bytes[8],
629            bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15], bytes[16],
630        ]);
631
632        let scale = Scale::from_u8(bytes[17]);
633        let target = Scale::from_u8(bytes[18]);
634
635        Some(Dt::new(attos, scale, target))
636    }
637}