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