Skip to main content

whichtime_sys/
components.rs

1//! Option C: Array-based components with bitflags
2//!
3//! This replaces HashMap<Component, i32> with a fixed-size array and bitflags
4//! for tracking which components are known vs implied.
5//!
6//! Benefits:
7//! - Copy semantics (no heap allocation, no cloning cost)
8//! - Cache-friendly (entire struct fits in ~64 bytes)
9//! - Branch-free access patterns
10
11use bitflags::bitflags;
12use serde::{Deserialize, Serialize};
13
14/// Date/time components that can be parsed
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
16#[repr(u8)]
17pub enum Component {
18    Year = 0,
19    Month = 1,
20    Day = 2,
21    Weekday = 3,
22    Hour = 4,
23    Minute = 5,
24    Second = 6,
25    Millisecond = 7,
26    Meridiem = 8,
27    TimezoneOffset = 9,
28}
29
30impl Component {
31    /// Total number of components
32    pub const COUNT: usize = 10;
33
34    /// Get the corresponding flag for this component
35    #[inline]
36    pub const fn flag(self) -> ComponentFlags {
37        ComponentFlags::from_bits_truncate(1 << (self as u8))
38    }
39
40    /// Get component name as string
41    pub const fn as_str(self) -> &'static str {
42        match self {
43            Component::Year => "year",
44            Component::Month => "month",
45            Component::Day => "day",
46            Component::Weekday => "weekday",
47            Component::Hour => "hour",
48            Component::Minute => "minute",
49            Component::Second => "second",
50            Component::Millisecond => "millisecond",
51            Component::Meridiem => "meridiem",
52            Component::TimezoneOffset => "timezoneOffset",
53        }
54    }
55}
56
57bitflags! {
58    /// Bitflags for tracking which components are set
59    #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
60    pub struct ComponentFlags: u16 {
61        const YEAR = 1 << 0;
62        const MONTH = 1 << 1;
63        const DAY = 1 << 2;
64        const WEEKDAY = 1 << 3;
65        const HOUR = 1 << 4;
66        const MINUTE = 1 << 5;
67        const SECOND = 1 << 6;
68        const MILLISECOND = 1 << 7;
69        const MERIDIEM = 1 << 8;
70        const TIMEZONE_OFFSET = 1 << 9;
71    }
72}
73
74/// Fast, stack-allocated date/time components
75///
76/// This is the optimized replacement for HashMap-based ParsedComponents.
77/// Total size: 10 * 4 (values) + 2 (known) + 2 (implied) = 44 bytes
78#[derive(Debug, Clone, Copy, Default)]
79pub struct FastComponents {
80    /// Component values indexed by Component enum
81    values: [i32; Component::COUNT],
82    /// Flags for known (certain) components
83    known: ComponentFlags,
84    /// Flags for implied components  
85    implied: ComponentFlags,
86}
87
88impl FastComponents {
89    /// Create new empty components
90    #[inline]
91    pub const fn new() -> Self {
92        Self {
93            values: [0; Component::COUNT],
94            known: ComponentFlags::empty(),
95            implied: ComponentFlags::empty(),
96        }
97    }
98
99    /// Create components with default implied values from a reference date
100    pub fn with_defaults(reference: &crate::ReferenceWithTimezone) -> Self {
101        use chrono::Datelike;
102
103        let date = reference.instant;
104        let mut components = Self::new();
105
106        // Set default implied values
107        components.imply(Component::Year, date.year());
108        components.imply(Component::Month, date.month() as i32);
109        components.imply(Component::Day, date.day() as i32);
110        components.imply(Component::Hour, 12);
111        components.imply(Component::Minute, 0);
112        components.imply(Component::Second, 0);
113        components.imply(Component::Millisecond, 0);
114
115        components
116    }
117
118    /// Check if a component is certain (known)
119    #[inline]
120    pub fn is_certain(&self, comp: Component) -> bool {
121        self.known.contains(comp.flag())
122    }
123
124    /// Check if a component has any value (known or implied)
125    #[inline]
126    pub fn has(&self, comp: Component) -> bool {
127        let flag = comp.flag();
128        self.known.contains(flag) || self.implied.contains(flag)
129    }
130
131    /// Get a component value (known or implied)
132    #[inline]
133    pub fn get(&self, comp: Component) -> Option<i32> {
134        if self.has(comp) {
135            Some(self.values[comp as usize])
136        } else {
137            None
138        }
139    }
140
141    /// Assign a value to a component (makes it certain/known)
142    #[inline]
143    pub fn assign(&mut self, comp: Component, value: i32) -> &mut Self {
144        let idx = comp as usize;
145        let flag = comp.flag();
146        self.values[idx] = value;
147        self.known.insert(flag);
148        self.implied.remove(flag);
149        self
150    }
151
152    /// Imply a value for a component (only if not already known)
153    #[inline]
154    pub fn imply(&mut self, comp: Component, value: i32) -> &mut Self {
155        if !self.known.contains(comp.flag()) {
156            let idx = comp as usize;
157            self.values[idx] = value;
158            self.implied.insert(comp.flag());
159        }
160        self
161    }
162
163    /// Delete a component (remove from both known and implied)
164    #[inline]
165    pub fn delete(&mut self, comp: Component) -> &mut Self {
166        let flag = comp.flag();
167        self.known.remove(flag);
168        self.implied.remove(flag);
169        self.values[comp as usize] = 0;
170        self
171    }
172
173    /// Check if only date components are certain (no time)
174    #[inline]
175    pub fn is_only_date(&self) -> bool {
176        !self.is_certain(Component::Hour)
177            && !self.is_certain(Component::Minute)
178            && !self.is_certain(Component::Second)
179    }
180
181    /// Check if only time components are certain (no date)
182    #[inline]
183    pub fn is_only_time(&self) -> bool {
184        !self.is_certain(Component::Weekday)
185            && !self.is_certain(Component::Day)
186            && !self.is_certain(Component::Month)
187            && !self.is_certain(Component::Year)
188    }
189
190    /// Check if only weekday is certain
191    #[inline]
192    pub fn is_only_weekday_component(&self) -> bool {
193        self.is_certain(Component::Weekday)
194            && !self.is_certain(Component::Day)
195            && !self.is_certain(Component::Month)
196    }
197
198    /// Check if date has unknown year
199    #[inline]
200    pub fn is_date_with_unknown_year(&self) -> bool {
201        self.is_certain(Component::Month) && !self.is_certain(Component::Year)
202    }
203
204    /// Get all certain components as a list
205    pub fn get_certain_components(&self) -> Vec<Component> {
206        let mut result = Vec::with_capacity(Component::COUNT);
207        for i in 0..Component::COUNT {
208            let comp = unsafe { std::mem::transmute::<u8, Component>(i as u8) };
209            if self.is_certain(comp) {
210                result.push(comp);
211            }
212        }
213        result
214    }
215
216    /// Check if the date components are valid
217    pub fn is_valid_date(&self) -> bool {
218        use chrono::NaiveDate;
219
220        let year = match self.get(Component::Year) {
221            Some(y) => y,
222            None => return false,
223        };
224        let month = match self.get(Component::Month) {
225            Some(m) => m as u32,
226            None => return false,
227        };
228        let day = match self.get(Component::Day) {
229            Some(d) => d as u32,
230            None => return false,
231        };
232
233        // Check if date is valid
234        if NaiveDate::from_ymd_opt(year, month, day).is_none() {
235            return false;
236        }
237
238        // Check time components if they exist
239        if let Some(hour) = self.get(Component::Hour)
240            && !(0..24).contains(&hour)
241        {
242            return false;
243        }
244        if let Some(minute) = self.get(Component::Minute)
245            && !(0..60).contains(&minute)
246        {
247            return false;
248        }
249        if let Some(second) = self.get(Component::Second)
250            && !(0..60).contains(&second)
251        {
252            return false;
253        }
254
255        true
256    }
257
258    /// Convert to a chrono DateTime
259    pub fn to_datetime(
260        &self,
261        reference: &crate::ReferenceWithTimezone,
262    ) -> Option<chrono::DateTime<chrono::Local>> {
263        use chrono::{Local, NaiveDate, TimeZone};
264
265        let year = self.get(Component::Year)?;
266        let month = self.get(Component::Month)? as u32;
267        let day = self.get(Component::Day)? as u32;
268        let hour = self.get(Component::Hour).unwrap_or(12) as u32;
269        let minute = self.get(Component::Minute).unwrap_or(0) as u32;
270        let second = self.get(Component::Second).unwrap_or(0) as u32;
271
272        let naive_dt =
273            NaiveDate::from_ymd_opt(year, month, day)?.and_hms_opt(hour, minute, second)?;
274
275        let local_dt = Local.from_local_datetime(&naive_dt).single()?;
276
277        // Apply timezone offset adjustment if needed
278        if let Some(offset) = self.get(Component::TimezoneOffset) {
279            let adjustment = reference.get_system_timezone_adjustment(Some(local_dt), Some(offset));
280            Some(local_dt + chrono::Duration::minutes(adjustment as i64))
281        } else {
282            Some(local_dt)
283        }
284    }
285}
286
287#[cfg(test)]
288mod tests {
289    use super::*;
290
291    #[test]
292    fn test_component_flag() {
293        assert_eq!(Component::Year.flag(), ComponentFlags::YEAR);
294        assert_eq!(Component::Month.flag(), ComponentFlags::MONTH);
295        assert_eq!(Component::Hour.flag(), ComponentFlags::HOUR);
296    }
297
298    #[test]
299    fn test_fast_components_assign_and_get() {
300        let mut comp = FastComponents::new();
301
302        comp.assign(Component::Year, 2024);
303        comp.assign(Component::Month, 11);
304        comp.assign(Component::Day, 25);
305
306        assert_eq!(comp.get(Component::Year), Some(2024));
307        assert_eq!(comp.get(Component::Month), Some(11));
308        assert_eq!(comp.get(Component::Day), Some(25));
309        assert_eq!(comp.get(Component::Hour), None);
310
311        assert!(comp.is_certain(Component::Year));
312        assert!(!comp.is_certain(Component::Hour));
313    }
314
315    #[test]
316    fn test_fast_components_imply() {
317        let mut comp = FastComponents::new();
318
319        comp.imply(Component::Hour, 12);
320        assert_eq!(comp.get(Component::Hour), Some(12));
321        assert!(!comp.is_certain(Component::Hour));
322
323        // Assign should override imply
324        comp.assign(Component::Hour, 15);
325        assert_eq!(comp.get(Component::Hour), Some(15));
326        assert!(comp.is_certain(Component::Hour));
327
328        // Imply should not override assign
329        comp.imply(Component::Hour, 9);
330        assert_eq!(comp.get(Component::Hour), Some(15));
331    }
332
333    #[test]
334    fn test_fast_components_copy() {
335        let mut a = FastComponents::new();
336        a.assign(Component::Year, 2024);
337
338        // Copy should be cheap (no allocation)
339        let b = a;
340
341        assert_eq!(b.get(Component::Year), Some(2024));
342    }
343
344    #[test]
345    fn test_size() {
346        // Ensure the struct is reasonably sized (fits in cache line)
347        assert!(std::mem::size_of::<FastComponents>() <= 64);
348    }
349}