Skip to main content

deep_time/time_parts/
mod.rs

1mod from_ccsds_bin;
2mod from_ccsds_str;
3mod from_str;
4mod to_ccsds_bin;
5mod to_deep_time;
6
7#[cfg(feature = "alloc")]
8mod to_ccsds_str;
9
10#[cfg(feature = "chrono")]
11mod to_chrono;
12
13#[cfg(feature = "jiff")]
14mod to_jiff;
15
16use crate::{AsciiStr, Scale};
17
18/// A flexible, partially-filled representation of a civil datetime.
19///
20/// `TimeParts` is the central intermediate type used throughout the library
21/// for parsing, formatting, and converting between different time representations
22/// (CCSDS, ISO-like strings, `chrono`, `jiff`, `Dt`, etc.).
23///
24/// Most fields are optional, allowing partial dates/times. It also carries
25/// metadata such as the time `scale`, IANA zone name, leap-second flag,
26/// and various weekday/week-number representations.
27///
28/// - Convert to [`Dt`] using [`TimeParts::to_dt`].
29/// - Conversions to types from other crates require relevant features to
30///   be enabled.
31#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
32#[cfg_attr(feature = "js", derive(tsify::Tsify))]
33#[derive(Debug, Clone, Copy, Default, PartialEq)]
34pub struct TimeParts {
35    /// Year (can be negative for BCE dates).
36    pub year: Option<i64>,
37    /// Month of the year (1–12).
38    pub month: Option<u8>,
39    /// Day of the month (1–31).
40    pub day: Option<u8>,
41    /// Hour of the day (0–23).
42    pub hour: Option<u8>,
43    /// Minute of the hour (0–59).
44    pub minute: Option<u8>,
45    /// Second of the minute (0–60). Value 60 is used for leap seconds.
46    pub second: Option<u8>,
47    /// Attoseconds (0 ≤ value < 10¹⁸).
48    pub attos: Option<u64>,
49    /// Timezone offset from UTC.
50    pub offset: Option<Offset>,
51    /// IANA timezone name (e.g. `"America/New_York"`), stored as ASCII.
52    pub iana_name: Option<AsciiStr<49>>,
53    /// Whether this instant represents a leap second.
54    pub is_leap_second: bool,
55    /// The time scale this value belongs to (TAI, UTC, etc.).
56    pub scale: Scale,
57    /// Day of the week.
58    pub weekday: Option<Weekday>,
59    /// Day of the year (1–366), corresponding to `%j`.
60    pub day_of_year: Option<u16>,
61    /// ISO week year (`%G` / `%g`).
62    pub iso_week_year: Option<i64>,
63    /// ISO week number (1–53), corresponding to `%V`.
64    pub iso_week: Option<u8>,
65    /// Week number with Sunday as first day of week (0–53), `%U`.
66    pub week_sun: Option<u8>,
67    /// Week number with Monday as first day of week (0–53), `%W`.
68    pub week_mon: Option<u8>,
69    /// AM / PM indicator.
70    pub meridiem: Option<Meridiem>,
71    /// Unix timestamp in seconds (`%s`).
72    pub unix_timestamp_seconds: Option<i64>,
73}
74
75impl TimeParts {
76    #[inline]
77    pub fn new_utc() -> Self {
78        Self {
79            scale: Scale::UTC,
80            ..Default::default()
81        }
82    }
83
84    /// Sets the IANA timezone name safely.
85    ///
86    /// Uses `AsciiStr::try_from_str` internally. If the name is non-ASCII
87    /// or longer than 49 bytes it is silently dropped (no panics).
88    #[inline]
89    pub fn set_iana_name(&mut self, name: Option<&str>) -> &mut Self {
90        self.iana_name = name.and_then(|s| AsciiStr::try_from_str(s).ok());
91        self
92    }
93}
94
95/// AM / PM indicator.
96#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
97#[cfg_attr(feature = "js", derive(tsify::Tsify))]
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
99pub enum Meridiem {
100    #[default]
101    AM,
102    PM,
103}
104
105#[cfg(feature = "wire")]
106impl Meridiem {
107    pub const WIRE_SIZE: usize = 1;
108
109    #[inline]
110    pub const fn to_wire_byte(self) -> u8 {
111        match self {
112            Meridiem::AM => 0,
113            Meridiem::PM => 1,
114        }
115    }
116
117    #[inline]
118    pub const fn from_wire_byte(b: u8) -> Option<Self> {
119        match b {
120            0 => Some(Meridiem::AM),
121            1 => Some(Meridiem::PM),
122            _ => None,
123        }
124    }
125}
126
127/// Day of the week. Default is set to Sunday.
128#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
129#[cfg_attr(feature = "js", derive(tsify::Tsify))]
130#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
131pub enum Weekday {
132    #[default]
133    Sunday,
134    Monday,
135    Tuesday,
136    Wednesday,
137    Thursday,
138    Friday,
139    Saturday,
140}
141
142impl Weekday {
143    /// Converts a Sunday-based weekday number (0 = Sunday … 6 = Saturday) to `Weekday`.
144    #[inline]
145    pub const fn from_sunday_zero_offset(n: i8) -> Option<Self> {
146        match n {
147            0 => Some(Weekday::Sunday),
148            1 => Some(Weekday::Monday),
149            2 => Some(Weekday::Tuesday),
150            3 => Some(Weekday::Wednesday),
151            4 => Some(Weekday::Thursday),
152            5 => Some(Weekday::Friday),
153            6 => Some(Weekday::Saturday),
154            _ => None,
155        }
156    }
157
158    /// Converts a Monday-based weekday number (1 = Monday … 7 = Sunday) to `Weekday`.
159    #[inline]
160    pub const fn from_monday_one_offset(n: i8) -> Option<Self> {
161        match n {
162            1 => Some(Weekday::Monday),
163            2 => Some(Weekday::Tuesday),
164            3 => Some(Weekday::Wednesday),
165            4 => Some(Weekday::Thursday),
166            5 => Some(Weekday::Friday),
167            6 => Some(Weekday::Saturday),
168            7 => Some(Weekday::Sunday),
169            _ => None,
170        }
171    }
172
173    /// Sunday-based weekday number (0 = Sunday … 6 = Saturday).
174    #[inline]
175    pub const fn wk_sun(self) -> u8 {
176        match self {
177            Weekday::Sunday => 0,
178            Weekday::Monday => 1,
179            Weekday::Tuesday => 2,
180            Weekday::Wednesday => 3,
181            Weekday::Thursday => 4,
182            Weekday::Friday => 5,
183            Weekday::Saturday => 6,
184        }
185    }
186
187    /// Monday-based weekday number (1 = Monday … 7 = Sunday).
188    #[inline]
189    pub const fn wk_mon(self) -> u8 {
190        match self {
191            Weekday::Monday => 1,
192            Weekday::Tuesday => 2,
193            Weekday::Wednesday => 3,
194            Weekday::Thursday => 4,
195            Weekday::Friday => 5,
196            Weekday::Saturday => 6,
197            Weekday::Sunday => 7,
198        }
199    }
200}
201
202#[cfg(feature = "wire")]
203impl Weekday {
204    pub const WIRE_SIZE: usize = 1;
205
206    #[inline]
207    pub const fn to_wire_byte(self) -> u8 {
208        self.wk_sun()
209    }
210
211    #[inline]
212    pub const fn from_wire_byte(b: u8) -> Option<Self> {
213        Self::from_sunday_zero_offset(b as i8)
214    }
215}
216
217/// Timezone offset representation.
218#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
219#[cfg_attr(feature = "js", derive(tsify::Tsify))]
220#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
221pub enum Offset {
222    #[default]
223    Utc,
224    None,
225    /// Fixed offset from UTC in seconds
226    Fixed(i32),
227}
228
229#[cfg(feature = "wire")]
230impl Offset {
231    pub const WIRE_SIZE: usize = 5; // tag (1) + i32 (4)
232
233    pub fn to_wire_bytes(&self) -> [u8; Self::WIRE_SIZE] {
234        let mut buf = [0u8; Self::WIRE_SIZE];
235        match self {
236            Offset::Utc => buf[0] = 0,
237            Offset::None => buf[0] = 1,
238            Offset::Fixed(offset) => {
239                buf[0] = 2;
240                buf[1..5].copy_from_slice(&offset.to_le_bytes());
241            }
242        }
243        buf
244    }
245
246    pub fn from_wire_bytes(bytes: &[u8]) -> Option<Self> {
247        if bytes.len() != Self::WIRE_SIZE {
248            return None;
249        }
250        match bytes[0] {
251            0 => Some(Offset::Utc),
252            1 => Some(Offset::None),
253            2 => {
254                let offset = i32::from_le_bytes([bytes[1], bytes[2], bytes[3], bytes[4]]);
255                Some(Offset::Fixed(offset))
256            }
257            _ => None,
258        }
259    }
260}
261
262#[cfg(feature = "wire")]
263impl TimeParts {
264    /// Current wire format version.
265    pub const WIRE_VERSION: u8 = 1;
266
267    /// Total size of the wire representation (120 bytes).
268    pub const WIRE_SIZE: usize = 120;
269
270    /// Serializes `TimeParts` into a fixed 120-byte buffer.
271    ///
272    /// Layout:
273    /// - Byte 0: Version (`WIRE_VERSION`)
274    /// - Bytes 1..120: Data (119 bytes)
275    pub fn to_wire_bytes(&self) -> [u8; Self::WIRE_SIZE] {
276        let mut buf = [0u8; Self::WIRE_SIZE];
277        buf[0] = Self::WIRE_VERSION;
278
279        let mut offset = 1usize;
280
281        // year (sentinel = i64::MIN)
282        let year = self.year.unwrap_or(i64::MIN);
283        buf[offset..offset + 8].copy_from_slice(&year.to_le_bytes());
284        offset += 8;
285
286        // month
287        buf[offset] = self.month.unwrap_or(u8::MAX);
288        offset += 1;
289
290        // day
291        buf[offset] = self.day.unwrap_or(u8::MAX);
292        offset += 1;
293
294        // hour
295        buf[offset] = self.hour.unwrap_or(u8::MAX);
296        offset += 1;
297
298        // minute
299        buf[offset] = self.minute.unwrap_or(u8::MAX);
300        offset += 1;
301
302        // second
303        buf[offset] = self.second.unwrap_or(u8::MAX);
304        offset += 1;
305
306        // attos
307        let attos = self.attos.unwrap_or(u64::MAX);
308        buf[offset..offset + 8].copy_from_slice(&attos.to_le_bytes());
309        offset += 8;
310
311        // offset (5 bytes)
312        let offset_bytes = self.offset.unwrap_or_default().to_wire_bytes();
313        buf[offset..offset + 5].copy_from_slice(&offset_bytes);
314        offset += 5;
315
316        // iana_name (49 bytes)
317        if let Some(name) = &self.iana_name {
318            let name_bytes = name.to_wire_bytes();
319            buf[offset..offset + 49].copy_from_slice(&name_bytes);
320        }
321        offset += 49;
322
323        // is_leap_second
324        buf[offset] = if self.is_leap_second { 1 } else { 0 };
325        offset += 1;
326
327        // scale
328        buf[offset] = self.scale as u8;
329        offset += 1;
330
331        // weekday
332        buf[offset] = self.weekday.map_or(255, |w| w.to_wire_byte());
333        offset += 1;
334
335        // day_of_year
336        let doy = self.day_of_year.unwrap_or(u16::MAX);
337        buf[offset..offset + 2].copy_from_slice(&doy.to_le_bytes());
338        offset += 2;
339
340        // iso_week_year
341        let iso_y = self.iso_week_year.unwrap_or(i64::MIN);
342        buf[offset..offset + 8].copy_from_slice(&iso_y.to_le_bytes());
343        offset += 8;
344
345        // iso_week
346        buf[offset] = self.iso_week.unwrap_or(u8::MAX);
347        offset += 1;
348
349        // week_sun
350        buf[offset] = self.week_sun.unwrap_or(u8::MAX);
351        offset += 1;
352
353        // week_mon
354        buf[offset] = self.week_mon.unwrap_or(u8::MAX);
355        offset += 1;
356
357        // meridiem
358        buf[offset] = self.meridiem.map_or(255, |m| m.to_wire_byte());
359        offset += 1;
360
361        // unix_timestamp_seconds
362        let unix = self.unix_timestamp_seconds.unwrap_or(i64::MIN);
363        buf[offset..offset + 8].copy_from_slice(&unix.to_le_bytes());
364
365        buf
366    }
367
368    /// Deserializes `TimeParts` from exactly 120 bytes.
369    ///
370    /// Returns `None` if the version byte is unknown or the data is invalid.
371    pub fn from_wire_bytes(bytes: &[u8]) -> Option<Self> {
372        if bytes.len() != Self::WIRE_SIZE {
373            return None;
374        }
375        if bytes[0] != Self::WIRE_VERSION {
376            return None;
377        }
378
379        let mut dc = TimeParts::default();
380        let mut offset = 1usize;
381
382        // year (8 bytes)
383        let year = i64::from_le_bytes(bytes[offset..offset + 8].try_into().ok()?);
384        if year != i64::MIN {
385            dc.year = Some(year);
386        }
387        offset += 8;
388
389        // month (1 byte)
390        let m = bytes[offset];
391        if m != u8::MAX {
392            dc.month = Some(m);
393        }
394        offset += 1;
395
396        // day (1 byte)
397        let d = bytes[offset];
398        if d != u8::MAX {
399            dc.day = Some(d);
400        }
401        offset += 1;
402
403        // hour (1 byte)
404        let h = bytes[offset];
405        if h != u8::MAX {
406            dc.hour = Some(h);
407        }
408        offset += 1;
409
410        // minute (1 byte)
411        let min = bytes[offset];
412        if min != u8::MAX {
413            dc.minute = Some(min);
414        }
415        offset += 1;
416
417        // second (1 byte)
418        let sec = bytes[offset];
419        if sec != u8::MAX {
420            dc.second = Some(sec);
421        }
422        offset += 1;
423
424        // attos (8 bytes)
425        let attos = u64::from_le_bytes(bytes[offset..offset + 8].try_into().ok()?);
426        if attos != u64::MAX {
427            dc.attos = Some(attos);
428        }
429        offset += 8;
430
431        // offset (5 bytes) — already nice
432        if let Some(offset) = Offset::from_wire_bytes(&bytes[offset..offset + 5]) {
433            dc.offset = Some(offset);
434        }
435        offset += 5;
436
437        // iana_name (49 bytes) — already nice
438        let iana_bytes = &bytes[offset..offset + 49];
439        if let Some(name) = AsciiStr::<49>::from_wire_bytes(iana_bytes)
440            && !name.is_empty()
441        {
442            dc.iana_name = Some(name);
443        }
444        offset += 49;
445
446        // is_leap_second (1 byte)
447        dc.is_leap_second = bytes[offset] != 0;
448        offset += 1;
449
450        // scale (1 byte)
451        dc.scale = Scale::from_u8(bytes[offset]);
452        offset += 1;
453
454        // weekday (1 byte)
455        let wd_byte = bytes[offset];
456        if wd_byte != 255
457            && let Some(wd) = Weekday::from_wire_byte(wd_byte)
458        {
459            dc.weekday = Some(wd);
460        }
461        offset += 1;
462
463        // day_of_year (2 bytes)
464        let doy = u16::from_le_bytes(bytes[offset..offset + 2].try_into().ok()?);
465        if doy != u16::MAX {
466            dc.day_of_year = Some(doy);
467        }
468        offset += 2;
469
470        // iso_week_year (8 bytes)
471        let iso_y = i64::from_le_bytes(bytes[offset..offset + 8].try_into().ok()?);
472        if iso_y != i64::MIN {
473            dc.iso_week_year = Some(iso_y);
474        }
475        offset += 8;
476
477        // iso_week (1 byte)
478        let iw = bytes[offset];
479        if iw != u8::MAX {
480            dc.iso_week = Some(iw);
481        }
482        offset += 1;
483
484        // week_sun (1 byte)
485        let ws = bytes[offset];
486        if ws != u8::MAX {
487            dc.week_sun = Some(ws);
488        }
489        offset += 1;
490
491        // week_mon (1 byte)
492        let wm = bytes[offset];
493        if wm != u8::MAX {
494            dc.week_mon = Some(wm);
495        }
496        offset += 1;
497
498        // meridiem (1 byte)
499        let mer_byte = bytes[offset];
500        if mer_byte != 255
501            && let Some(m) = Meridiem::from_wire_byte(mer_byte)
502        {
503            dc.meridiem = Some(m);
504        }
505
506        offset += 1;
507
508        // unix_timestamp_seconds (8 bytes)
509        let unix = i64::from_le_bytes(bytes[offset..offset + 8].try_into().ok()?);
510        if unix != i64::MIN {
511            dc.unix_timestamp_seconds = Some(unix);
512        }
513
514        Some(dc)
515    }
516}