Skip to main content

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