Skip to main content

hadris_udf/
time.rs

1//! UDF timestamp handling
2
3/// UDF timestamp structure
4///
5/// Represents date and time in UDF format (ECMA-167 1/7.3)
6#[repr(C)]
7#[derive(Debug, Clone, Copy, Default, bytemuck::Zeroable, bytemuck::Pod)]
8pub struct UdfTimestamp {
9    /// Type and timezone
10    /// Bits 0-11: Timezone offset in minutes from UTC (-1440 to 1440)
11    /// Bits 12-15: Type (0=UTC, 1=local, 2=agreement)
12    pub type_and_tz: u16,
13    /// Year (1-9999)
14    pub year: u16,
15    /// Month (1-12)
16    pub month: u8,
17    /// Day (1-31)
18    pub day: u8,
19    /// Hour (0-23)
20    pub hour: u8,
21    /// Minute (0-59)
22    pub minute: u8,
23    /// Second (0-59)
24    pub second: u8,
25    /// Centiseconds (0-99)
26    pub centiseconds: u8,
27    /// Hundreds of microseconds (0-99)
28    pub hundreds_of_microseconds: u8,
29    /// Microseconds (0-99)
30    pub microseconds: u8,
31}
32
33impl UdfTimestamp {
34    #[cfg(all(feature = "alloc", any(feature = "sync", feature = "async")))]
35    pub(crate) fn into_native(mut self) -> Self {
36        self.type_and_tz = self.type_and_tz.to_le();
37        self.year = self.year.to_le();
38        self
39    }
40
41    /// Get the timezone type
42    pub fn timezone_type(&self) -> TimezoneType {
43        match (self.type_and_tz >> 12) & 0x0F {
44            0 => TimezoneType::Utc,
45            1 => TimezoneType::Local,
46            2 => TimezoneType::Agreement,
47            _ => TimezoneType::Reserved,
48        }
49    }
50
51    /// Get the timezone offset in minutes from UTC
52    ///
53    /// Returns None if the timezone is not specified
54    pub fn timezone_offset(&self) -> Option<i16> {
55        let tz_type = self.timezone_type();
56        if matches!(tz_type, TimezoneType::Utc | TimezoneType::Local) {
57            let offset = (self.type_and_tz & 0x0FFF) as i16;
58            // Sign extend from 12 bits
59            let offset = if offset & 0x0800 != 0 {
60                offset | !0x0FFF
61            } else {
62                offset
63            };
64            Some(offset)
65        } else {
66            None
67        }
68    }
69
70    /// Check if this timestamp is valid
71    pub fn is_valid(&self) -> bool {
72        self.month >= 1
73            && self.month <= 12
74            && self.day >= 1
75            && self.day <= 31
76            && self.hour <= 23
77            && self.minute <= 59
78            && self.second <= 59
79            && self.centiseconds <= 99
80            && self.hundreds_of_microseconds <= 99
81            && self.microseconds <= 99
82    }
83}
84
85impl core::fmt::Display for UdfTimestamp {
86    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
87        write!(
88            f,
89            "{:04}-{:02}-{:02} {:02}:{:02}:{:02}",
90            self.year, self.month, self.day, self.hour, self.minute, self.second
91        )
92    }
93}
94
95/// Timezone type
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub enum TimezoneType {
98    /// Coordinated Universal Time
99    Utc,
100    /// Local time
101    Local,
102    /// Agreed upon by sender and receiver
103    Agreement,
104    /// Reserved for future use
105    Reserved,
106}
107
108impl core::fmt::Display for TimezoneType {
109    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
110        match self {
111            Self::Utc => write!(f, "UTC"),
112            Self::Local => write!(f, "Local"),
113            Self::Agreement => write!(f, "Agreement"),
114            Self::Reserved => write!(f, "Reserved"),
115        }
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    static_assertions::const_assert_eq!(size_of::<UdfTimestamp>(), 12);
124
125    #[test]
126    fn test_timestamp_default() {
127        let ts = UdfTimestamp::default();
128        assert!(!ts.is_valid()); // Month and day are 0
129    }
130
131    #[test]
132    fn test_timestamp_valid() {
133        let ts = UdfTimestamp {
134            type_and_tz: 0x1000, // UTC
135            year: 2024,
136            month: 1,
137            day: 15,
138            hour: 10,
139            minute: 30,
140            second: 45,
141            centiseconds: 50,
142            hundreds_of_microseconds: 25,
143            microseconds: 10,
144        };
145        assert!(ts.is_valid());
146        assert_eq!(ts.timezone_type(), TimezoneType::Local);
147    }
148
149    #[test]
150    fn test_timezone_offset() {
151        // UTC+0
152        let ts = UdfTimestamp {
153            type_and_tz: 0x0000, // UTC, offset 0
154            ..Default::default()
155        };
156        assert_eq!(ts.timezone_offset(), Some(0));
157
158        // UTC+5:30 (330 minutes)
159        let ts = UdfTimestamp {
160            type_and_tz: 0x014A, // UTC, offset 330
161            ..Default::default()
162        };
163        assert_eq!(ts.timezone_offset(), Some(330));
164    }
165}