Skip to main content

deep_time/gregorian_time/
mod.rs

1use crate::{AsciiStr, Dt, Weekday};
2
3mod to_str;
4
5/// Combined Gregorian date + wall time with subsecond precision.
6#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
7pub struct YmdHms {
8    pub yr: i64,
9    pub mo: u8,
10    pub day: u8,
11    pub hr: u8,
12    pub min: u8,
13    pub sec: u8,    // 0–60 (60 only during leap seconds)
14    pub attos: u64, // attoseconds (0 ≤ subsec < 10¹⁸)
15    pub unix_attosec: i128,
16}
17
18/// UTC Civil calendar and time-of-day components of a [`Dt`].
19#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
20#[cfg_attr(feature = "js", derive(tsify::Tsify))]
21#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22pub struct GregorianTime {
23    /// UNIX attoseconds counting from 1970 epoch
24    pub(crate) unix_attosec: i128,
25    /// Gregorian year (proleptic Gregorian calendar, supports negative years and year 0).
26    pub(crate) yr: i64,
27    /// Gregorian month in the range [1, 12].
28    pub(crate) mo: u8,
29    /// Gregorian day of the month in the range [1, 31].
30    pub(crate) day: u8,
31    /// Hour of the day in the range [0, 23].
32    pub(crate) hr: u8,
33    /// Minute in the range [0, 59].
34    pub(crate) min: u8,
35    /// Second in the range [0, 60] (60 only during UTC leap seconds).
36    pub(crate) sec: u8,
37    /// Fractional part of the second expressed in attoseconds (u64).
38    pub(crate) attos: u64,
39    /// ISO 8601 week year.
40    pub(crate) iso_yr: i64,
41    /// ISO 8601 week number in the range [1, 53].
42    pub(crate) iso_wk: u8,
43    /// ISO 8601 weekday enum e.g. Monday/Tuesday/...
44    pub(crate) iso_wkday: Weekday,
45    /// Ordinal day of the year (1-based).
46    pub(crate) day_of_yr: u16,
47    /// Weekday number (0 = Sunday … 6 = Saturday).
48    pub(crate) wkday: u8,
49    /// Sunday based week of year (Range: `0..=53`).
50    pub(crate) wk_of_yr_sun: u8,
51    /// Monday based week of year (Range: `0..=53`).
52    pub(crate) wk_of_yr_mon: u8,
53    /// Used for formatting (strftime).
54    /// A stored offset in seconds, used within the crate.
55    pub(crate) offset_sec: Option<i32>,
56    /// A stored IANA name, used within the crate, %Q.
57    pub(crate) tz: Option<AsciiStr<49>>,
58    /// UTC, EST, %Z
59    pub(crate) tz_abbrev: Option<AsciiStr<49>>,
60}
61
62impl GregorianTime {
63    /// Creates a new `GregorianTime` with all fields specified.
64    /// This isn't the recommended way to make a `GregorianTime`.
65    /// It's safer to use `Dt::to_gregorian_time()`.
66    #[inline]
67    pub const fn new(
68        unix_attosec: i128,
69        yr: i64,
70        mo: u8,
71        day: u8,
72        hr: u8,
73        min: u8,
74        sec: u8,
75        attos: u64,
76        iso_yr: i64,
77        iso_wk: u8,
78        iso_wkday: Weekday,
79        day_of_yr: u16,
80        wkday: u8,
81        wk_of_yr_sun: u8,
82        wk_of_yr_mon: u8,
83    ) -> Self {
84        Self {
85            unix_attosec,
86            yr,
87            mo,
88            day,
89            hr,
90            min,
91            sec,
92            attos,
93            iso_yr,
94            iso_wk,
95            iso_wkday,
96            day_of_yr,
97            wkday,
98            wk_of_yr_sun,
99            wk_of_yr_mon,
100            offset_sec: None,
101            tz: None,
102            tz_abbrev: None,
103        }
104    }
105
106    /// UNIX attoseconds since 1970 epoch
107    #[inline]
108    pub const fn unix_attosec(&self) -> i128 {
109        self.unix_attosec
110    }
111
112    /// Returns the Unix timestamp since 1970-01-01 00:00:00 UTC as a tuple of
113    /// `(whole_seconds, attoseconds)`.
114    ///
115    /// - `whole_seconds` can be negative (for dates before 1970).
116    /// - The fractional part (`attoseconds`) is always in the range `0..=999_999_999_999_999_999`.
117    #[inline]
118    pub const fn unix_timestamp(&self) -> (i64, u64) {
119        const ATTOS_PER_SEC_I128: i128 = 1_000_000_000_000_000_000;
120        let total = self.unix_attosec;
121        let secs = (total / ATTOS_PER_SEC_I128) as i64;
122        let frac = (total % ATTOS_PER_SEC_I128).unsigned_abs() as u64;
123        (secs, frac)
124    }
125
126    /// Gregorian year (proleptic Gregorian calendar, supports negative years and year 0).
127    #[inline]
128    pub const fn yr(&self) -> i64 {
129        self.yr
130    }
131
132    /// Gregorian month in the range [1, 12].
133    #[inline]
134    pub const fn mo(&self) -> u8 {
135        self.mo
136    }
137
138    /// Gregorian day of the month in the range [1, 31].
139    #[inline]
140    pub const fn day(&self) -> u8 {
141        self.day
142    }
143
144    /// Hour of the day in the range [0, 23].
145    #[inline]
146    pub const fn hr(&self) -> u8 {
147        self.hr
148    }
149
150    /// Minute in the range [0, 59].
151    #[inline]
152    pub const fn min(&self) -> u8 {
153        self.min
154    }
155
156    /// Second in the range [0, 60] (60 only during UTC leap seconds).
157    #[inline]
158    pub const fn sec(&self) -> u8 {
159        self.sec
160    }
161
162    /// Fractional part of the second expressed in attoseconds (`0 ≤ attos < 10¹⁸`).
163    #[inline]
164    pub const fn attos(&self) -> u64 {
165        self.attos
166    }
167
168    /// ISO 8601 week year.
169    #[inline]
170    pub const fn iso_yr(&self) -> i64 {
171        self.iso_yr
172    }
173
174    /// ISO 8601 week number in the range [1, 53].
175    #[inline]
176    pub const fn iso_wk(&self) -> u8 {
177        self.iso_wk
178    }
179
180    /// ISO 8601 weekday (Monday-based [`Weekday`] enum).
181    #[inline]
182    pub const fn iso_wkday(&self) -> Weekday {
183        self.iso_wkday
184    }
185
186    /// Ordinal day of the year (1-based).
187    #[inline]
188    pub const fn day_of_yr(&self) -> u16 {
189        self.day_of_yr
190    }
191
192    /// Weekday number (0 = Sunday … 6 = Saturday).
193    #[inline]
194    pub const fn wkday_sun(&self) -> u8 {
195        self.wkday
196    }
197
198    /// ISO 8601 weekday (0 = Monday ... 6 = Sunday).
199    #[inline]
200    pub const fn wkday_mon(&self) -> u8 {
201        self.iso_wkday.wk_mon()
202    }
203
204    /// Sunday based week of year (Range: `0..=53`).
205    #[inline]
206    pub const fn wk_of_yr_sun(&self) -> u8 {
207        self.wk_of_yr_sun
208    }
209
210    /// Monday based week of year (Range: `0..=53`).
211    #[inline]
212    pub const fn wk_of_yr_mon(&self) -> u8 {
213        self.wk_of_yr_mon
214    }
215
216    #[inline]
217    pub const fn offset_sec(&self) -> Option<i32> {
218        self.offset_sec
219    }
220
221    #[inline]
222    pub const fn tz(&self) -> Option<&AsciiStr<49>> {
223        self.tz.as_ref()
224    }
225
226    #[inline]
227    pub const fn tz_abbrev(&self) -> Option<&AsciiStr<49>> {
228        self.tz_abbrev.as_ref()
229    }
230
231    #[inline]
232    pub(crate) fn set_offset(&mut self, offset_sec: Option<i32>) -> &mut Self {
233        self.offset_sec = offset_sec;
234        self
235    }
236
237    #[inline]
238    pub(crate) fn set_tz(&mut self, tz: Option<&str>) -> &mut Self {
239        self.tz = tz.and_then(|s| AsciiStr::try_from_str(s).ok());
240        self
241    }
242
243    #[inline]
244    pub(crate) fn set_tz_abbrev(&mut self, tz_abbrev: Option<&str>) -> &mut Self {
245        self.tz_abbrev = tz_abbrev.and_then(|s| AsciiStr::try_from_str(s).ok());
246        self
247    }
248
249    /// Reconstructs a [`Dt`] from these **UTC** civil components.
250    ///
251    /// Round-tripping with `Dt::to_gregorian_time`.
252    #[inline]
253    pub const fn to_time_point(&self) -> Dt {
254        Dt::from_ymdhms(self.yr, self.mo, self.day, self.hr, self.min, self.sec, 0)
255    }
256}
257
258#[cfg(feature = "wire")]
259impl GregorianTime {
260    /// Current wire format version.
261    pub const WIRE_VERSION: u8 = 1;
262
263    /// Size of the canonical wire representation in bytes (158 bytes).
264    pub const WIRE_SIZE: usize = 158;
265
266    /// Serializes this `GregorianTime` into a fixed 158-byte buffer.
267    ///
268    /// # Wire Format (Version 1)
269    ///
270    /// - Byte `0`: Version (`WIRE_VERSION`)
271    /// - Bytes `1..17`: `unix_attosec` (`i128`)
272    /// - Bytes `17..25`: `yr` (`i64`)
273    /// - Bytes `25..30`: `mo`, `day`, `hr`, `min`, `sec` (`u8` × 5)
274    /// - Bytes `30..38`: `attos` (`u64`)
275    /// - Bytes `38..46`: `iso_yr` (`i64`)
276    /// - Bytes `46..48`: `iso_wk` + `iso_wkday` (`u8` × 2)
277    /// - Bytes `48..50`: `day_of_yr` (`u16`)
278    /// - Byte `50`: `wkday` (`u8`)
279    /// - Bytes `51..53`: `wk_of_yr_sun` + `wk_of_yr_mon` (`u8` × 2)
280    /// - Bytes `53..58`: `offset_sec` (tag byte + `i32`)
281    /// - Bytes `58..108`: `tz` (tag byte + `AsciiStr<49>`)
282    /// - Bytes `108..158`: `tz_abbrev` (tag byte + `AsciiStr<49>`)
283    pub fn to_wire_bytes(&self) -> [u8; Self::WIRE_SIZE] {
284        let mut buf = [0u8; Self::WIRE_SIZE];
285        buf[0] = Self::WIRE_VERSION;
286        let mut offset = 1usize;
287
288        // unix_attosec (16 bytes)
289        buf[offset..offset + 16].copy_from_slice(&self.unix_attosec.to_le_bytes());
290        offset += 16;
291
292        // yr (8 bytes)
293        buf[offset..offset + 8].copy_from_slice(&self.yr.to_le_bytes());
294        offset += 8;
295
296        // mo, day, hr, min, sec (5 bytes)
297        buf[offset] = self.mo;
298        offset += 1;
299        buf[offset] = self.day;
300        offset += 1;
301        buf[offset] = self.hr;
302        offset += 1;
303        buf[offset] = self.min;
304        offset += 1;
305        buf[offset] = self.sec;
306        offset += 1;
307
308        // attos (8 bytes)
309        buf[offset..offset + 8].copy_from_slice(&self.attos.to_le_bytes());
310        offset += 8;
311
312        // iso_yr (8 bytes)
313        buf[offset..offset + 8].copy_from_slice(&self.iso_yr.to_le_bytes());
314        offset += 8;
315
316        // iso_wk + iso_wkday (2 bytes)
317        buf[offset] = self.iso_wk;
318        offset += 1;
319        buf[offset] = self.iso_wkday.to_wire_byte();
320        offset += 1;
321
322        // day_of_yr (2 bytes)
323        buf[offset..offset + 2].copy_from_slice(&self.day_of_yr.to_le_bytes());
324        offset += 2;
325
326        // wkday (1 byte)
327        buf[offset] = self.wkday;
328        offset += 1;
329
330        // wk_of_yr_sun + wk_of_yr_mon (2 bytes)
331        buf[offset] = self.wk_of_yr_sun;
332        offset += 1;
333        buf[offset] = self.wk_of_yr_mon;
334        offset += 1;
335
336        // offset_sec (Option<i32>) — 5 bytes
337        if let Some(val) = self.offset_sec {
338            buf[offset] = 1;
339            buf[offset + 1..offset + 5].copy_from_slice(&val.to_le_bytes());
340        } else {
341            buf[offset] = 0;
342        }
343        offset += 5;
344
345        // tz (Option<AsciiStr<49>>) — 50 bytes
346        if let Some(tz) = &self.tz {
347            buf[offset] = 1;
348            let tz_bytes = tz.to_wire_bytes();
349            buf[offset + 1..offset + 1 + AsciiStr::<49>::WIRE_SIZE].copy_from_slice(&tz_bytes);
350        } else {
351            buf[offset] = 0;
352        }
353        offset += 1 + AsciiStr::<49>::WIRE_SIZE;
354
355        // tz_abbrev (Option<AsciiStr<49>>) — 50 bytes
356        if let Some(abbrev) = &self.tz_abbrev {
357            buf[offset] = 1;
358            let abbrev_bytes = abbrev.to_wire_bytes();
359            buf[offset + 1..offset + 1 + AsciiStr::<49>::WIRE_SIZE].copy_from_slice(&abbrev_bytes);
360        } else {
361            buf[offset] = 0;
362        }
363
364        buf
365    }
366
367    /// Deserializes a `GregorianTime` from exactly 158 bytes of wire data.
368    ///
369    /// Returns `None` if the version is unknown or any field is invalid.
370    ///
371    /// ## Security
372    ///
373    /// Safe for untrusted input. Fixed-size format with strict validation.
374    /// No allocation or `unsafe` code used.
375    pub fn from_wire_bytes(bytes: &[u8]) -> Option<Self> {
376        if bytes.len() != Self::WIRE_SIZE {
377            return None;
378        }
379        if bytes[0] != Self::WIRE_VERSION {
380            return None;
381        }
382
383        let mut offset = 1usize;
384
385        // unix_attosec (16 bytes)
386        let unix_attosec = i128::from_le_bytes(bytes[offset..offset + 16].try_into().ok()?);
387        offset += 16;
388
389        // yr (8 bytes)
390        let yr = i64::from_le_bytes(bytes[offset..offset + 8].try_into().ok()?);
391        offset += 8;
392
393        // mo, day, hr, min, sec (5 bytes)
394        let mo = bytes[offset];
395        offset += 1;
396        let day = bytes[offset];
397        offset += 1;
398        let hr = bytes[offset];
399        offset += 1;
400        let min = bytes[offset];
401        offset += 1;
402        let sec = bytes[offset];
403        offset += 1;
404
405        // attos (8 bytes)
406        let attos = u64::from_le_bytes(bytes[offset..offset + 8].try_into().ok()?);
407        offset += 8;
408
409        // iso_yr (8 bytes)
410        let iso_yr = i64::from_le_bytes(bytes[offset..offset + 8].try_into().ok()?);
411        offset += 8;
412
413        // iso_wk + iso_wkday (2 bytes)
414        let iso_wk = bytes[offset];
415        offset += 1;
416        let iso_wkday = Weekday::from_wire_byte(bytes[offset])?;
417        offset += 1;
418
419        // day_of_yr (2 bytes)
420        let day_of_yr = u16::from_le_bytes(bytes[offset..offset + 2].try_into().ok()?);
421        offset += 2;
422
423        // wkday (1 byte)
424        let wkday = bytes[offset];
425        offset += 1;
426
427        // wk_of_yr_sun + wk_of_yr_mon (2 bytes)
428        let wk_of_yr_sun = bytes[offset];
429        offset += 1;
430        let wk_of_yr_mon = bytes[offset];
431        offset += 1;
432
433        // offset_sec (Option<i32>) — 5 bytes
434        let offset_sec = if bytes[offset] == 1 {
435            Some(i32::from_le_bytes(
436                bytes[offset + 1..offset + 5].try_into().ok()?,
437            ))
438        } else {
439            None
440        };
441        offset += 5;
442
443        // tz (Option<AsciiStr<49>>) — 50 bytes
444        let tz = if bytes[offset] == 1 {
445            AsciiStr::<49>::from_wire_bytes(
446                &bytes[offset + 1..offset + 1 + AsciiStr::<49>::WIRE_SIZE],
447            )
448        } else {
449            None
450        };
451        offset += 1 + AsciiStr::<49>::WIRE_SIZE;
452
453        // tz_abbrev (Option<AsciiStr<49>>) — 50 bytes
454        let tz_abbrev = if bytes[offset] == 1 {
455            AsciiStr::<49>::from_wire_bytes(
456                &bytes[offset + 1..offset + 1 + AsciiStr::<49>::WIRE_SIZE],
457            )
458        } else {
459            None
460        };
461
462        Some(Self {
463            unix_attosec,
464            yr,
465            mo,
466            day,
467            hr,
468            min,
469            sec,
470            attos,
471            iso_yr,
472            iso_wk,
473            iso_wkday,
474            day_of_yr,
475            wkday,
476            wk_of_yr_sun,
477            wk_of_yr_mon,
478            offset_sec,
479            tz,
480            tz_abbrev,
481        })
482    }
483}