facet_value/
datetime.rs

1//! DateTime value type for representing temporal data.
2//!
3//! `VDateTime` supports the four datetime categories from TOML:
4//! - Offset Date-Time: `1979-05-27T07:32:00Z` or `1979-05-27T07:32:00+01:30`
5//! - Local Date-Time: `1979-05-27T07:32:00`
6//! - Local Date: `1979-05-27`
7//! - Local Time: `07:32:00`
8
9#[cfg(feature = "alloc")]
10use alloc::alloc::{Layout, alloc, dealloc};
11use core::cmp::Ordering;
12use core::fmt::{self, Debug, Formatter};
13use core::hash::{Hash, Hasher};
14
15use crate::value::{TypeTag, Value};
16
17/// The kind of datetime value.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub enum DateTimeKind {
20    /// Offset date-time with UTC offset in minutes.
21    /// e.g., `1979-05-27T07:32:00Z` (offset=0) or `1979-05-27T07:32:00+05:30` (offset=330)
22    Offset {
23        /// Offset from UTC in minutes. Range: -1440 to +1440 (±24 hours).
24        offset_minutes: i16,
25    },
26
27    /// Local date-time without offset (civil time).
28    /// e.g., `1979-05-27T07:32:00`
29    LocalDateTime,
30
31    /// Local date only.
32    /// e.g., `1979-05-27`
33    LocalDate,
34
35    /// Local time only.
36    /// e.g., `07:32:00`
37    LocalTime,
38}
39
40/// Header for heap-allocated datetime values.
41#[repr(C, align(8))]
42struct DateTimeHeader {
43    /// Year (negative for BCE). For LocalTime, this is 0.
44    year: i32,
45    /// Month (1-12). For LocalTime, this is 0.
46    month: u8,
47    /// Day (1-31). For LocalTime, this is 0.
48    day: u8,
49    /// Hour (0-23). For LocalDate, this is 0.
50    hour: u8,
51    /// Minute (0-59). For LocalDate, this is 0.
52    minute: u8,
53    /// Second (0-59, or 60 for leap second). For LocalDate, this is 0.
54    second: u8,
55    /// Padding for alignment
56    _pad: [u8; 3],
57    /// Nanoseconds (0-999_999_999). For LocalDate, this is 0.
58    nanos: u32,
59    /// The kind of datetime
60    kind: DateTimeKind,
61}
62
63/// A datetime value.
64///
65/// `VDateTime` can represent offset date-times, local date-times, local dates,
66/// or local times. This covers all datetime types in TOML and most other formats.
67#[repr(transparent)]
68#[derive(Clone)]
69pub struct VDateTime(pub(crate) Value);
70
71impl VDateTime {
72    fn layout() -> Layout {
73        Layout::new::<DateTimeHeader>()
74    }
75
76    #[cfg(feature = "alloc")]
77    fn alloc() -> *mut DateTimeHeader {
78        unsafe { alloc(Self::layout()).cast::<DateTimeHeader>() }
79    }
80
81    #[cfg(feature = "alloc")]
82    fn dealloc(ptr: *mut DateTimeHeader) {
83        unsafe {
84            dealloc(ptr.cast::<u8>(), Self::layout());
85        }
86    }
87
88    fn header(&self) -> &DateTimeHeader {
89        unsafe { &*(self.0.heap_ptr() as *const DateTimeHeader) }
90    }
91
92    #[allow(dead_code)]
93    fn header_mut(&mut self) -> &mut DateTimeHeader {
94        unsafe { &mut *(self.0.heap_ptr_mut() as *mut DateTimeHeader) }
95    }
96
97    /// Creates a new offset date-time.
98    ///
99    /// # Arguments
100    /// * `year` - Year (negative for BCE)
101    /// * `month` - Month (1-12)
102    /// * `day` - Day (1-31)
103    /// * `hour` - Hour (0-23)
104    /// * `minute` - Minute (0-59)
105    /// * `second` - Second (0-59, or 60 for leap second)
106    /// * `nanos` - Nanoseconds (0-999_999_999)
107    /// * `offset_minutes` - Offset from UTC in minutes
108    #[cfg(feature = "alloc")]
109    #[must_use]
110    #[allow(clippy::too_many_arguments)]
111    pub fn new_offset(
112        year: i32,
113        month: u8,
114        day: u8,
115        hour: u8,
116        minute: u8,
117        second: u8,
118        nanos: u32,
119        offset_minutes: i16,
120    ) -> Self {
121        unsafe {
122            let ptr = Self::alloc();
123            (*ptr).year = year;
124            (*ptr).month = month;
125            (*ptr).day = day;
126            (*ptr).hour = hour;
127            (*ptr).minute = minute;
128            (*ptr).second = second;
129            (*ptr)._pad = [0; 3];
130            (*ptr).nanos = nanos;
131            (*ptr).kind = DateTimeKind::Offset { offset_minutes };
132            VDateTime(Value::new_ptr(ptr.cast(), TypeTag::DateTime))
133        }
134    }
135
136    /// Creates a new local date-time (no offset).
137    #[cfg(feature = "alloc")]
138    #[must_use]
139    pub fn new_local_datetime(
140        year: i32,
141        month: u8,
142        day: u8,
143        hour: u8,
144        minute: u8,
145        second: u8,
146        nanos: u32,
147    ) -> Self {
148        unsafe {
149            let ptr = Self::alloc();
150            (*ptr).year = year;
151            (*ptr).month = month;
152            (*ptr).day = day;
153            (*ptr).hour = hour;
154            (*ptr).minute = minute;
155            (*ptr).second = second;
156            (*ptr)._pad = [0; 3];
157            (*ptr).nanos = nanos;
158            (*ptr).kind = DateTimeKind::LocalDateTime;
159            VDateTime(Value::new_ptr(ptr.cast(), TypeTag::DateTime))
160        }
161    }
162
163    /// Creates a new local date (no time component).
164    #[cfg(feature = "alloc")]
165    #[must_use]
166    pub fn new_local_date(year: i32, month: u8, day: u8) -> Self {
167        unsafe {
168            let ptr = Self::alloc();
169            (*ptr).year = year;
170            (*ptr).month = month;
171            (*ptr).day = day;
172            (*ptr).hour = 0;
173            (*ptr).minute = 0;
174            (*ptr).second = 0;
175            (*ptr)._pad = [0; 3];
176            (*ptr).nanos = 0;
177            (*ptr).kind = DateTimeKind::LocalDate;
178            VDateTime(Value::new_ptr(ptr.cast(), TypeTag::DateTime))
179        }
180    }
181
182    /// Creates a new local time (no date component).
183    #[cfg(feature = "alloc")]
184    #[must_use]
185    pub fn new_local_time(hour: u8, minute: u8, second: u8, nanos: u32) -> Self {
186        unsafe {
187            let ptr = Self::alloc();
188            (*ptr).year = 0;
189            (*ptr).month = 0;
190            (*ptr).day = 0;
191            (*ptr).hour = hour;
192            (*ptr).minute = minute;
193            (*ptr).second = second;
194            (*ptr)._pad = [0; 3];
195            (*ptr).nanos = nanos;
196            (*ptr).kind = DateTimeKind::LocalTime;
197            VDateTime(Value::new_ptr(ptr.cast(), TypeTag::DateTime))
198        }
199    }
200
201    /// Returns the kind of datetime.
202    #[must_use]
203    pub fn kind(&self) -> DateTimeKind {
204        self.header().kind
205    }
206
207    /// Returns the year. Returns 0 for LocalTime.
208    #[must_use]
209    pub fn year(&self) -> i32 {
210        self.header().year
211    }
212
213    /// Returns the month (1-12). Returns 0 for LocalTime.
214    #[must_use]
215    pub fn month(&self) -> u8 {
216        self.header().month
217    }
218
219    /// Returns the day (1-31). Returns 0 for LocalTime.
220    #[must_use]
221    pub fn day(&self) -> u8 {
222        self.header().day
223    }
224
225    /// Returns the hour (0-23). Returns 0 for LocalDate.
226    #[must_use]
227    pub fn hour(&self) -> u8 {
228        self.header().hour
229    }
230
231    /// Returns the minute (0-59). Returns 0 for LocalDate.
232    #[must_use]
233    pub fn minute(&self) -> u8 {
234        self.header().minute
235    }
236
237    /// Returns the second (0-59, or 60 for leap second). Returns 0 for LocalDate.
238    #[must_use]
239    pub fn second(&self) -> u8 {
240        self.header().second
241    }
242
243    /// Returns the nanoseconds (0-999_999_999). Returns 0 for LocalDate.
244    #[must_use]
245    pub fn nanos(&self) -> u32 {
246        self.header().nanos
247    }
248
249    /// Returns the UTC offset in minutes, if this is an offset datetime.
250    #[must_use]
251    pub fn offset_minutes(&self) -> Option<i16> {
252        match self.kind() {
253            DateTimeKind::Offset { offset_minutes } => Some(offset_minutes),
254            _ => None,
255        }
256    }
257
258    /// Returns true if this datetime has a date component.
259    #[must_use]
260    pub fn has_date(&self) -> bool {
261        !matches!(self.kind(), DateTimeKind::LocalTime)
262    }
263
264    /// Returns true if this datetime has a time component.
265    #[must_use]
266    pub fn has_time(&self) -> bool {
267        !matches!(self.kind(), DateTimeKind::LocalDate)
268    }
269
270    /// Returns true if this datetime has an offset.
271    #[must_use]
272    pub fn has_offset(&self) -> bool {
273        matches!(self.kind(), DateTimeKind::Offset { .. })
274    }
275
276    // === Internal ===
277
278    pub(crate) fn clone_impl(&self) -> Value {
279        #[cfg(feature = "alloc")]
280        {
281            let h = self.header();
282            match h.kind {
283                DateTimeKind::Offset { offset_minutes } => {
284                    Self::new_offset(
285                        h.year,
286                        h.month,
287                        h.day,
288                        h.hour,
289                        h.minute,
290                        h.second,
291                        h.nanos,
292                        offset_minutes,
293                    )
294                    .0
295                }
296                DateTimeKind::LocalDateTime => {
297                    Self::new_local_datetime(
298                        h.year, h.month, h.day, h.hour, h.minute, h.second, h.nanos,
299                    )
300                    .0
301                }
302                DateTimeKind::LocalDate => Self::new_local_date(h.year, h.month, h.day).0,
303                DateTimeKind::LocalTime => {
304                    Self::new_local_time(h.hour, h.minute, h.second, h.nanos).0
305                }
306            }
307        }
308        #[cfg(not(feature = "alloc"))]
309        {
310            panic!("cannot clone VDateTime without alloc feature")
311        }
312    }
313
314    pub(crate) fn drop_impl(&mut self) {
315        #[cfg(feature = "alloc")]
316        unsafe {
317            Self::dealloc(self.0.heap_ptr_mut().cast());
318        }
319    }
320}
321
322// === PartialEq, Eq ===
323
324impl PartialEq for VDateTime {
325    fn eq(&self, other: &Self) -> bool {
326        let (h1, h2) = (self.header(), other.header());
327        h1.kind == h2.kind
328            && h1.year == h2.year
329            && h1.month == h2.month
330            && h1.day == h2.day
331            && h1.hour == h2.hour
332            && h1.minute == h2.minute
333            && h1.second == h2.second
334            && h1.nanos == h2.nanos
335    }
336}
337
338impl Eq for VDateTime {}
339
340// === PartialOrd ===
341
342impl PartialOrd for VDateTime {
343    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
344        let (h1, h2) = (self.header(), other.header());
345
346        // Only compare within the same kind
347        match (&h1.kind, &h2.kind) {
348            (
349                DateTimeKind::Offset { offset_minutes: o1 },
350                DateTimeKind::Offset { offset_minutes: o2 },
351            ) => {
352                // Convert to comparable instant (seconds from epoch-ish)
353                // We don't need actual epoch, just consistent comparison
354                let to_comparable = |h: &DateTimeHeader, offset: i16| -> (i64, u32) {
355                    let days = h.year as i64 * 366 + h.month as i64 * 31 + h.day as i64;
356                    let secs = days * 86400
357                        + h.hour as i64 * 3600
358                        + h.minute as i64 * 60
359                        + h.second as i64
360                        - offset as i64 * 60;
361                    (secs, h.nanos)
362                };
363                let c1 = to_comparable(h1, *o1);
364                let c2 = to_comparable(h2, *o2);
365                c1.partial_cmp(&c2)
366            }
367            (DateTimeKind::LocalDateTime, DateTimeKind::LocalDateTime)
368            | (DateTimeKind::LocalDate, DateTimeKind::LocalDate) => {
369                // Lexicographic comparison
370                (
371                    h1.year, h1.month, h1.day, h1.hour, h1.minute, h1.second, h1.nanos,
372                )
373                    .partial_cmp(&(
374                        h2.year, h2.month, h2.day, h2.hour, h2.minute, h2.second, h2.nanos,
375                    ))
376            }
377            (DateTimeKind::LocalTime, DateTimeKind::LocalTime) => {
378                (h1.hour, h1.minute, h1.second, h1.nanos)
379                    .partial_cmp(&(h2.hour, h2.minute, h2.second, h2.nanos))
380            }
381            _ => None, // Different kinds are not comparable
382        }
383    }
384}
385
386// === Hash ===
387
388impl Hash for VDateTime {
389    fn hash<H: Hasher>(&self, state: &mut H) {
390        let h = self.header();
391        h.kind.hash(state);
392        h.year.hash(state);
393        h.month.hash(state);
394        h.day.hash(state);
395        h.hour.hash(state);
396        h.minute.hash(state);
397        h.second.hash(state);
398        h.nanos.hash(state);
399    }
400}
401
402// === Debug ===
403
404impl Debug for VDateTime {
405    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
406        let h = self.header();
407        match h.kind {
408            DateTimeKind::Offset { offset_minutes } => {
409                write!(
410                    f,
411                    "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
412                    h.year, h.month, h.day, h.hour, h.minute, h.second
413                )?;
414                if h.nanos > 0 {
415                    write!(f, ".{:09}", h.nanos)?;
416                }
417                if offset_minutes == 0 {
418                    write!(f, "Z")
419                } else {
420                    let sign = if offset_minutes >= 0 { '+' } else { '-' };
421                    let abs = offset_minutes.abs();
422                    write!(f, "{}{:02}:{:02}", sign, abs / 60, abs % 60)
423                }
424            }
425            DateTimeKind::LocalDateTime => {
426                write!(
427                    f,
428                    "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
429                    h.year, h.month, h.day, h.hour, h.minute, h.second
430                )?;
431                if h.nanos > 0 {
432                    write!(f, ".{:09}", h.nanos)?;
433                }
434                Ok(())
435            }
436            DateTimeKind::LocalDate => {
437                write!(f, "{:04}-{:02}-{:02}", h.year, h.month, h.day)
438            }
439            DateTimeKind::LocalTime => {
440                write!(f, "{:02}:{:02}:{:02}", h.hour, h.minute, h.second)?;
441                if h.nanos > 0 {
442                    write!(f, ".{:09}", h.nanos)?;
443                }
444                Ok(())
445            }
446        }
447    }
448}
449
450// === From ===
451
452#[cfg(feature = "alloc")]
453impl From<VDateTime> for Value {
454    fn from(dt: VDateTime) -> Self {
455        dt.0
456    }
457}