Skip to main content

deep_time/civil_parts/
mod.rs

1//! Intermediate "parts" of a civil date and time.
2//!
3//! [`Parts`] is produced by the parsers (string formats, ISO-like, CCSDS
4//! binary and text, etc.). It is then typically converted into a
5//! [`Dt`](../struct.Dt.html) or a type from `chrono`/`jiff`.
6//!
7//! It holds the individual components (various ways to express the date,
8//! time-of-day down to attoseconds, offset, scale, weekday/week info, etc.).
9
10mod from_bin_ccsds;
11mod from_str;
12mod from_str_iso;
13mod to_bin_ccsds;
14mod to_deep_time;
15
16#[cfg(feature = "alloc")]
17mod to_str_ccsds;
18
19#[cfg(feature = "chrono")]
20mod to_chrono;
21
22#[cfg(feature = "jiff")]
23mod to_jiff;
24
25use crate::{LiteStr, Scale};
26
27/// Intermediate representation of parsed civil date and time.
28///
29/// After parsing you typically convert the [`Parts`] to a final type
30/// such as
31/// [`Dt`](../struct.Dt.html)
32/// or one from `chrono`/`jiff`.
33///
34/// ## Examples
35///
36/// ```rust
37/// use deep_time::civil_parts::Parts;
38///
39/// let parts = Parts::from_str_iso("2024-06-20T14:30:00Z").unwrap();
40///
41/// // now you can convert to whichever type you need
42/// let dt = parts.to_dt().unwrap();
43/// ```
44#[derive(Debug, Clone, Copy, Default, PartialEq)]
45#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
46#[cfg_attr(feature = "tsify", derive(tsify::Tsify))]
47#[cfg_attr(feature = "defmt", derive(defmt::Format))]
48pub struct Parts {
49    /// Year (can be negative for BCE dates).
50    pub yr: Option<i64>,
51    /// Month of the year (1–12).
52    pub mo: Option<u8>,
53    /// Day of the month (1–31).
54    pub day: Option<u8>,
55    /// Hour of the day (0–23).
56    pub hr: u8,
57    /// Minute of the hour (0–59).
58    pub min: u8,
59    /// Second of the minute (0–60). Value 60 is used for leap seconds.
60    pub sec: u8,
61    /// Attoseconds (0 ≤ value < 10¹⁸).
62    pub attos: u64,
63    /// Timezone offset from UTC.
64    pub offset: Option<Offset>,
65    /// IANA timezone name (e.g. `"America/New_York"`), stored as ASCII.
66    pub iana_name: Option<LiteStr<49>>,
67    /// The time scale this value belongs to (TAI, UTC, etc.).
68    pub scale: Scale,
69    /// Day of the week.
70    pub wkday: Option<Weekday>,
71    /// Day of the year (1–366), corresponding to `%j`.
72    pub day_of_yr: Option<u16>,
73    /// ISO week year (`%G` / `%g`).
74    pub iso_wk_yr: Option<i64>,
75    /// ISO week number (1–53), corresponding to `%V`.
76    pub iso_wk: Option<u8>,
77    /// Week number with Sunday as first day of week (0–53), `%U`.
78    pub wk_sun: Option<u8>,
79    /// Week number with Monday as first day of week (0–53), `%W`.
80    pub wk_mon: Option<u8>,
81    /// AM / PM indicator.
82    pub meridiem: Option<Meridiem>,
83    /// Timestamp in seconds since a known epoch (`%s` = Unix 1970, `%J` = noon 2000).
84    pub timestamp: Option<Timestamp>,
85}
86
87/// Raw parsed components from a decimal style number string.
88#[derive(Clone, Copy)]
89pub(crate) struct ParsedReal {
90    pub(crate) negative: bool,
91    /// Accumulated absolute integer part (u64::MAX on overflow during accumulation).
92    pub(crate) int_u: u64,
93    /// Fractional attoseconds, already left-padded to 18 digits.
94    pub(crate) frac_attos: u64,
95    pub(crate) scale: Scale,
96}
97
98impl Parts {
99    #[inline(always)]
100    pub fn new_utc() -> Parts {
101        Self {
102            scale: Scale::UTC,
103            ..Default::default()
104        }
105    }
106
107    /// Sets the IANA timezone name.
108    #[inline(always)]
109    pub fn set_iana_name(&mut self, name: Option<&str>) {
110        self.iana_name = name.map(LiteStr::new);
111    }
112}
113
114/// Used by [`Timestamp`]
115///
116/// Records the epoch of the timestamp.
117#[derive(Copy, Clone, Debug, PartialEq, Eq)]
118#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
119#[cfg_attr(feature = "tsify", derive(tsify::Tsify))]
120#[cfg_attr(feature = "defmt", derive(defmt::Format))]
121pub enum Epoch {
122    Unix,
123    Noon2000,
124}
125
126/// Timestamp seconds relative to a specific epoch.
127///
128/// Used by the `%s` (Unix epoch) and `%J` (J2000.0 noon 2000-01-01 12:00 TAI) directives.
129#[derive(Debug, Clone, Copy, PartialEq, Eq)]
130#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
131#[cfg_attr(feature = "tsify", derive(tsify::Tsify))]
132#[cfg_attr(feature = "defmt", derive(defmt::Format))]
133pub struct Timestamp {
134    pub attos: i128,
135    pub epoch: Epoch,
136}
137
138/// AM / PM indicator.
139#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
140#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
141#[cfg_attr(feature = "tsify", derive(tsify::Tsify))]
142#[cfg_attr(feature = "defmt", derive(defmt::Format))]
143pub enum Meridiem {
144    #[default]
145    AM,
146    PM,
147}
148
149/// Day of the week. Default is set to Sunday.
150#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
151#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
152#[cfg_attr(feature = "tsify", derive(tsify::Tsify))]
153#[cfg_attr(feature = "defmt", derive(defmt::Format))]
154pub enum Weekday {
155    #[default]
156    Sunday,
157    Monday,
158    Tuesday,
159    Wednesday,
160    Thursday,
161    Friday,
162    Saturday,
163}
164
165impl Weekday {
166    /// Converts a Sunday-based weekday number (0 = Sunday … 6 = Saturday) to `Weekday`.
167    pub const fn from_sunday_0_based(n: u8) -> Option<Self> {
168        match n {
169            0 => Some(Weekday::Sunday),
170            1 => Some(Weekday::Monday),
171            2 => Some(Weekday::Tuesday),
172            3 => Some(Weekday::Wednesday),
173            4 => Some(Weekday::Thursday),
174            5 => Some(Weekday::Friday),
175            6 => Some(Weekday::Saturday),
176            _ => None,
177        }
178    }
179
180    /// Converts a Monday-based weekday number (1 = Monday … 7 = Sunday) to `Weekday`.
181    pub const fn from_monday_1_based(n: u8) -> Option<Self> {
182        match n {
183            1 => Some(Weekday::Monday),
184            2 => Some(Weekday::Tuesday),
185            3 => Some(Weekday::Wednesday),
186            4 => Some(Weekday::Thursday),
187            5 => Some(Weekday::Friday),
188            6 => Some(Weekday::Saturday),
189            7 => Some(Weekday::Sunday),
190            _ => None,
191        }
192    }
193
194    /// Sunday-based weekday number (0 = Sunday … 6 = Saturday).
195    pub const fn wkday_sun_0_based(self) -> u8 {
196        match self {
197            Weekday::Sunday => 0,
198            Weekday::Monday => 1,
199            Weekday::Tuesday => 2,
200            Weekday::Wednesday => 3,
201            Weekday::Thursday => 4,
202            Weekday::Friday => 5,
203            Weekday::Saturday => 6,
204        }
205    }
206
207    /// Sunday-based weekday number (1 = Sunday … 7 = Saturday).
208    pub const fn wkday_sun_1_based(self) -> u8 {
209        match self {
210            Weekday::Sunday => 1,
211            Weekday::Monday => 2,
212            Weekday::Tuesday => 3,
213            Weekday::Wednesday => 4,
214            Weekday::Thursday => 5,
215            Weekday::Friday => 6,
216            Weekday::Saturday => 7,
217        }
218    }
219
220    /// Monday-based weekday number (0 = Monday … 6 = Sunday).
221    pub const fn wkday_mon_0_based(self) -> u8 {
222        match self {
223            Weekday::Monday => 0,
224            Weekday::Tuesday => 1,
225            Weekday::Wednesday => 2,
226            Weekday::Thursday => 3,
227            Weekday::Friday => 4,
228            Weekday::Saturday => 5,
229            Weekday::Sunday => 6,
230        }
231    }
232
233    /// Monday-based weekday number (1 = Monday … 7 = Sunday).
234    pub const fn wkday_mon_1_based(self) -> u8 {
235        match self {
236            Weekday::Monday => 1,
237            Weekday::Tuesday => 2,
238            Weekday::Wednesday => 3,
239            Weekday::Thursday => 4,
240            Weekday::Friday => 5,
241            Weekday::Saturday => 6,
242            Weekday::Sunday => 7,
243        }
244    }
245}
246
247/// Timezone offset representation.
248#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
249#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
250#[cfg_attr(feature = "tsify", derive(tsify::Tsify))]
251#[cfg_attr(feature = "defmt", derive(defmt::Format))]
252pub enum Offset {
253    #[default]
254    None,
255    /// Fixed offset in seconds
256    Fixed(i32),
257}
258
259#[cfg(feature = "wire")]
260impl Meridiem {
261    pub const WIRE_SIZE: usize = 1;
262
263    #[inline]
264    pub const fn to_wire_byte(self) -> u8 {
265        match self {
266            Meridiem::AM => 0,
267            Meridiem::PM => 1,
268        }
269    }
270
271    #[inline]
272    pub const fn from_wire_byte(b: u8) -> Option<Self> {
273        match b {
274            0 => Some(Meridiem::AM),
275            1 => Some(Meridiem::PM),
276            _ => None,
277        }
278    }
279}
280
281#[cfg(feature = "wire")]
282impl Offset {
283    pub const WIRE_SIZE: usize = 5; // tag (1) + i32 (4)
284
285    pub fn to_wire_bytes(&self) -> [u8; Self::WIRE_SIZE] {
286        let mut buf = [0u8; Self::WIRE_SIZE];
287        match self {
288            Offset::None => buf[0] = 0,
289            Offset::Fixed(offset) => {
290                buf[0] = 1;
291                buf[1..5].copy_from_slice(&offset.to_le_bytes());
292            }
293        }
294        buf
295    }
296
297    pub fn from_wire_bytes(bytes: &[u8]) -> Option<Self> {
298        if bytes.len() != Self::WIRE_SIZE {
299            return None;
300        }
301        match bytes[0] {
302            0 => Some(Offset::None),
303            1 => {
304                let offset = i32::from_le_bytes([bytes[1], bytes[2], bytes[3], bytes[4]]);
305                Some(Offset::Fixed(offset))
306            }
307            _ => None,
308        }
309    }
310}
311
312#[cfg(feature = "wire")]
313impl Weekday {
314    pub const WIRE_SIZE: usize = 1;
315
316    #[inline]
317    pub const fn to_wire_byte(self) -> u8 {
318        self.wkday_sun_0_based()
319    }
320
321    #[inline]
322    pub const fn from_wire_byte(b: u8) -> Option<Self> {
323        Self::from_sunday_0_based(b)
324    }
325}
326
327#[cfg(feature = "wire")]
328impl Parts {
329    /// Current wire format version.
330    pub const WIRE_VERSION: u8 = 1;
331
332    /// Total size of the wire representation (120 bytes).
333    /// The timestamp field now uses 17 bytes (tag + i128), using some of the previous slack space.
334    pub const WIRE_SIZE: usize = 120;
335
336    /// Serializes `Parts` into a fixed 120-byte buffer.
337    pub fn to_wire_bytes(&self) -> [u8; Self::WIRE_SIZE] {
338        let mut buf = [0u8; Self::WIRE_SIZE];
339        buf[0] = Self::WIRE_VERSION;
340
341        let mut offset = 1usize;
342
343        // year (sentinel = i64::MIN)
344        let year = self.yr.unwrap_or(i64::MIN);
345        buf[offset..offset + 8].copy_from_slice(&year.to_le_bytes());
346        offset += 8;
347
348        // month
349        buf[offset] = self.mo.unwrap_or(u8::MAX);
350        offset += 1;
351
352        // day
353        buf[offset] = self.day.unwrap_or(u8::MAX);
354        offset += 1;
355
356        // hour
357        buf[offset] = self.hr;
358        offset += 1;
359
360        // minute
361        buf[offset] = self.min;
362        offset += 1;
363
364        // second
365        buf[offset] = self.sec;
366        offset += 1;
367
368        // attos
369        let attos = self.attos;
370        buf[offset..offset + 8].copy_from_slice(&attos.to_le_bytes());
371        offset += 8;
372
373        // offset (5 bytes)
374        let offset_bytes = self.offset.unwrap_or_default().to_wire_bytes();
375        buf[offset..offset + 5].copy_from_slice(&offset_bytes);
376        offset += 5;
377
378        // iana_name (49 bytes)
379        if let Some(name) = &self.iana_name {
380            let name_bytes = name.bytes;
381            buf[offset..offset + 49].copy_from_slice(&name_bytes);
382        }
383        offset += 49;
384
385        // scale
386        buf[offset] = self.scale as u8;
387        offset += 1;
388
389        // weekday
390        buf[offset] = self.wkday.map_or(255, |w| w.to_wire_byte());
391        offset += 1;
392
393        // day_of_year
394        let doy = self.day_of_yr.unwrap_or(u16::MAX);
395        buf[offset..offset + 2].copy_from_slice(&doy.to_le_bytes());
396        offset += 2;
397
398        // iso_week_year
399        let iso_y = self.iso_wk_yr.unwrap_or(i64::MIN);
400        buf[offset..offset + 8].copy_from_slice(&iso_y.to_le_bytes());
401        offset += 8;
402
403        // iso_week
404        buf[offset] = self.iso_wk.unwrap_or(u8::MAX);
405        offset += 1;
406
407        // week_sun
408        buf[offset] = self.wk_sun.unwrap_or(u8::MAX);
409        offset += 1;
410
411        // week_mon
412        buf[offset] = self.wk_mon.unwrap_or(u8::MAX);
413        offset += 1;
414
415        // meridiem
416        buf[offset] = self.meridiem.map_or(255, |m| m.to_wire_byte());
417        offset += 1;
418
419        // timestamp: tag (1 byte) + i128 attos (16 bytes) = 17 bytes total
420        // tag: 0 = none, 1 = Unix, 2 = Noon2000
421        let (tag, attos) = match self.timestamp {
422            None => (0u8, 0i128),
423            Some(ts) => {
424                let t = match ts.epoch {
425                    Epoch::Unix => 1u8,
426                    Epoch::Noon2000 => 2u8,
427                };
428                (t, ts.attos)
429            }
430        };
431        buf[offset] = tag;
432        offset += 1;
433        buf[offset..offset + 16].copy_from_slice(&attos.to_le_bytes());
434        // offset += 16;
435
436        buf
437    }
438
439    /// Deserializes `Parts` from exactly `WIRE_SIZE` bytes.
440    pub fn from_wire_bytes(bytes: &[u8]) -> Option<Self> {
441        if bytes.len() != Self::WIRE_SIZE {
442            return None;
443        }
444        if bytes[0] != Self::WIRE_VERSION {
445            return None;
446        }
447
448        let mut dc = Parts::default();
449        let mut offset = 1usize;
450
451        // year (8 bytes)
452        let year = i64::from_le_bytes(bytes[offset..offset + 8].try_into().ok()?);
453        if year != i64::MIN {
454            dc.yr = Some(year);
455        }
456        offset += 8;
457
458        // month (1 byte)
459        let m = bytes[offset];
460        if m != u8::MAX {
461            dc.mo = Some(m);
462        }
463        offset += 1;
464
465        // day (1 byte)
466        let d = bytes[offset];
467        if d != u8::MAX {
468            dc.day = Some(d);
469        }
470        offset += 1;
471
472        // hour (1 byte)
473        dc.hr = bytes[offset];
474        offset += 1;
475
476        // minute (1 byte)
477        dc.min = bytes[offset];
478        offset += 1;
479
480        // second (1 byte)
481        dc.sec = bytes[offset];
482        offset += 1;
483
484        // attos (8 bytes)
485        let attos = u64::from_le_bytes(bytes[offset..offset + 8].try_into().ok()?);
486        dc.attos = attos;
487        offset += 8;
488
489        // offset (5 bytes)
490        if let Some(off) = Offset::from_wire_bytes(&bytes[offset..offset + 5]) {
491            dc.offset = Some(off);
492        }
493        offset += 5;
494
495        // iana_name (49 bytes)
496        let iana_bytes = &bytes[offset..offset + 49];
497        let name = LiteStr::<49>::from_bytes(iana_bytes);
498        if !name.as_bytes().is_empty() {
499            dc.iana_name = Some(name);
500        }
501        offset += 49;
502
503        // scale (1 byte)
504        dc.scale = Scale::from_u8(bytes[offset]);
505        offset += 1;
506
507        // weekday (1 byte)
508        let wd_byte = bytes[offset];
509        if wd_byte != 255
510            && let Some(wd) = Weekday::from_wire_byte(wd_byte)
511        {
512            dc.wkday = Some(wd);
513        }
514        offset += 1;
515
516        // day_of_year (2 bytes)
517        let doy = u16::from_le_bytes(bytes[offset..offset + 2].try_into().ok()?);
518        if doy != u16::MAX {
519            dc.day_of_yr = Some(doy);
520        }
521        offset += 2;
522
523        // iso_week_year (8 bytes)
524        let iso_y = i64::from_le_bytes(bytes[offset..offset + 8].try_into().ok()?);
525        if iso_y != i64::MIN {
526            dc.iso_wk_yr = Some(iso_y);
527        }
528        offset += 8;
529
530        // iso_week (1 byte)
531        let iw = bytes[offset];
532        if iw != u8::MAX {
533            dc.iso_wk = Some(iw);
534        }
535        offset += 1;
536
537        // week_sun (1 byte)
538        let ws = bytes[offset];
539        if ws != u8::MAX {
540            dc.wk_sun = Some(ws);
541        }
542        offset += 1;
543
544        // week_mon (1 byte)
545        let wm = bytes[offset];
546        if wm != u8::MAX {
547            dc.wk_mon = Some(wm);
548        }
549        offset += 1;
550
551        // meridiem (1 byte)
552        let mer_byte = bytes[offset];
553        if mer_byte != 255
554            && let Some(m) = Meridiem::from_wire_byte(mer_byte)
555        {
556            dc.meridiem = Some(m);
557        }
558        offset += 1;
559
560        // timestamp: tag (1) + i128 attos (16)
561        // tag: 0=none, 1=Unix, 2=Noon2000
562        let tag = bytes[offset];
563        offset += 1;
564
565        if tag != 0 {
566            let attos_arr: [u8; 16] = bytes[offset..offset + 16].try_into().ok()?;
567            let attos = i128::from_le_bytes(attos_arr);
568            // offset += 16;
569
570            let epoch = match tag {
571                1 => Epoch::Unix,
572                2 => Epoch::Noon2000,
573                _ => return None,
574            };
575            dc.timestamp = Some(Timestamp { attos, epoch });
576        } else {
577            // offset += 16;
578        }
579
580        Some(dc)
581    }
582}