Skip to main content

toml_spanner/
time.rs

1use std::{mem::MaybeUninit, str::FromStr};
2
3#[cfg(test)]
4#[path = "./time_tests.rs"]
5mod tests;
6
7/// A calendar date with year, month, and day components.
8///
9/// Represents the date portion of a TOML datetime value. Field ranges are
10/// validated during parsing:
11///
12/// - `year`: 0–9999
13/// - `month`: 1–12
14/// - `day`: 1–31 (upper bound depends on month and leap year rules)
15///
16/// # Examples
17///
18/// ```
19/// use toml_spanner::{Arena, DateTime};
20///
21/// let dt: DateTime = "2026-03-15".parse().unwrap();
22/// let date = dt.date().unwrap();
23/// assert_eq!(date.year, 2026);
24/// assert_eq!(date.month, 3);
25/// assert_eq!(date.day, 15);
26/// ```
27#[derive(Clone, Copy, Debug, PartialEq, Eq)]
28pub struct Date {
29    /// Calendar year (0–9999).
30    pub year: u16,
31    /// Month of the year (1–12).
32    pub month: u8,
33    /// Day of the month (1–31).
34    pub day: u8,
35}
36
37/// A UTC offset attached to an offset date-time.
38///
39/// TOML offset date-times include a timezone offset suffix such as `Z`,
40/// `+05:30`, or `-08:00`. This enum represents that offset.
41///
42/// # Examples
43///
44/// ```
45/// use toml_spanner::{DateTime, TimeOffset};
46///
47/// let dt: DateTime = "2026-01-04T12:00:00Z".parse().unwrap();
48/// assert_eq!(dt.offset(), Some(TimeOffset::Z));
49///
50/// let dt: DateTime = "2026-01-04T12:00:00+05:30".parse().unwrap();
51/// assert_eq!(dt.offset(), Some(TimeOffset::Custom { minutes: 330 }));
52///
53/// let dt: DateTime = "2026-01-04T12:00:00-08:00".parse().unwrap();
54/// assert_eq!(dt.offset(), Some(TimeOffset::Custom { minutes: -480 }));
55/// ```
56#[derive(Clone, Copy, Debug, PartialEq, Eq)]
57pub enum TimeOffset {
58    /// UTC offset `Z`.
59    Z,
60    /// Fixed offset from UTC in minutes (e.g. `+05:30` = 330, `-08:00` = -480).
61    Custom {
62        /// Minutes from UTC (positive = east, negative = west).
63        minutes: i16,
64    },
65}
66
67/// Represents the time of day portion of a TOML datetime value.
68///
69/// Field ranges are
70/// validated during parsing:
71///
72/// - `hour`: 0–23
73/// - `minute`: 0–59
74/// - `second`: 0–60 (60 is permitted for leap seconds)
75/// - `nanosecond`: 0–999999999
76///
77/// When seconds are omitted in the source (e.g. `12:30`), `second` defaults
78/// to 0. Use [`has_seconds`](Self::has_seconds) to distinguish this from an
79/// explicit `:00`.
80///
81/// # Examples
82///
83/// ```
84/// use toml_spanner::DateTime;
85///
86/// let dt: DateTime = "14:30:05.123".parse().unwrap();
87/// let time = dt.time().unwrap();
88/// assert_eq!(time.hour, 14);
89/// assert_eq!(time.minute, 30);
90/// assert_eq!(time.second, 5);
91/// assert_eq!(time.nanosecond, 123000000);
92/// assert_eq!(time.subsecond_precision(), 3);
93/// ```
94#[derive(Clone, Copy)]
95pub struct Time {
96    flags: u8,
97    /// Hour of the day (0–23).
98    pub hour: u8,
99    /// Minute of the hour (0–59).
100    pub minute: u8,
101    /// Second of the minute (0–60).
102    pub second: u8,
103    /// Sub-second component in nanoseconds (0–999999999).
104    pub nanosecond: u32,
105}
106
107impl std::fmt::Debug for Time {
108    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109        f.debug_struct("Time")
110            .field("hour", &self.hour)
111            .field("minute", &self.minute)
112            .field("second", &self.second)
113            .field("nanosecond", &self.nanosecond)
114            .finish()
115    }
116}
117
118impl PartialEq for Time {
119    fn eq(&self, other: &Self) -> bool {
120        self.hour == other.hour
121            && self.minute == other.minute
122            && self.second == other.second
123            && self.nanosecond == other.nanosecond
124    }
125}
126
127impl Eq for Time {}
128
129impl Time {
130    /// Returns the number of fractional-second digits present in the source.
131    ///
132    /// Returns 0 when no fractional part was written (e.g. `12:30:00`),
133    /// and 1–9 for `.1` through `.123456789`.
134    pub fn subsecond_precision(&self) -> u8 {
135        self.flags >> NANO_SHIFT
136    }
137    /// Returns `true` if seconds were explicitly written in the source.
138    ///
139    /// When the input omits seconds (e.g. `12:30`), [`second`](Self::second)
140    /// is set to 0 but this method returns `false`.
141    pub fn has_seconds(&self) -> bool {
142        self.flags & HAS_SECONDS != 0
143    }
144}
145
146/// Container for temporal values for TOML format, based on RFC 3339.
147///
148/// General bounds are in forced during parsing but leniently, so things like exact
149/// leap second rules are not enforced, you should generally being converting
150/// these time values, to a more complete time library like jiff before use.
151///
152/// The `DateTime` type is essentially more compact version of:
153/// ```
154/// use toml_spanner::{Date, Time, TimeOffset};
155/// struct DateTime {
156///     date: Option<Date>,
157///     time: Option<Time>,
158///     offset: Option<TimeOffset>,
159/// }
160/// ```
161/// For more details on support formats inside TOML documents please reference the [TOML v1.1.0 Specification](https://toml.io/en/v1.1.0#offset-date-time).
162///
163/// Mapping [`DateTime`] to the TOML time kinds works like the following:
164///
165/// ```rust
166/// #[rustfmt::skip]
167/// fn datetime_to_toml_kind(value: &toml_spanner::DateTime) -> &'static str {
168///     match (value.date(),value.time(),value.offset()) {
169///           (Some(_date), Some(_time), Some(_offset)) => "Offset Date-Time",
170///           (Some(_date), Some(_time), None         ) => "Local Date-Time",
171///           (Some(_date), None       , None         ) => "Local Date",
172///           (None       , Some(_time), None         ) => "Local Time",
173///         _ => unreachable!("for a DateTime produced from the toml-spanner::parse"),
174///     }
175/// }
176/// ```
177///
178/// # Constructing a `DateTime`
179/// Generally, you should be parsing `DateTime` values from a TOML document, but for testing purposes,
180/// `FromStr` is also implemented allowing for `"2026-01-04".parse::<DateTime>()`.
181///
182/// ```
183/// use toml_spanner::{Date, Time, TimeOffset, DateTime};
184/// let value: DateTime = "2026-01-04T12:30:45Z".parse().unwrap();
185/// assert_eq!(value.date(), Some(Date { year: 2026, month: 1, day: 4 }));
186/// assert_eq!(value.time().unwrap().minute, 30);
187/// assert_eq!(value.offset(), Some(TimeOffset::Z));
188/// ```
189///
190/// <details>
191/// <summary>Toggle Jiff Conversions Examples</summary>
192///
193/// ```ignore
194/// use toml_spanner::{FromToml, Error as TomlError, Span as TomlSpan};
195///
196/// fn extract_date(
197///     datetime: &toml_spanner::DateTime,
198///     span: TomlSpan,
199/// ) -> Result<jiff::civil::Date, TomlError> {
200///     let Some(date) = datetime.date() else {
201///         return Err(TomlError::custom("Missing date component", span));
202///     };
203///     // toml_spanner guartees the following inclusive ranges
204///     // year: 0-9999, month: 1-12, day: 1-31
205///     // making the as casts safe.
206///     match jiff::civil::Date::new(date.year as i16, date.month as i8, date.day as i8) {
207///         Ok(value) => Ok(value),
208///         Err(err) => Err(TomlError::custom(format!("Invalid date: {err}"), span)),
209///     }
210/// }
211///
212/// fn extract_time(
213///     datetime: &toml_spanner::DateTime,
214///     span: TomlSpan,
215/// ) -> Result<jiff::civil::Time, TomlError> {
216///     let Some(time) = datetime.time() else {
217///         return Err(TomlError::custom("Missing time component", span));
218///     };
219///     // toml_spanner guartees the following inclusive ranges
220///     // hour: 0-23, minute: 0-59, second: 0-60, nanosecond: 0-999999999
221///     // making the as casts safe.
222///     match jiff::civil::Time::new(
223///         time.hour as i8,
224///         time.minute as i8,
225///         time.second as i8,
226///         time.nanosecond as i32,
227///     ) {
228///         Ok(value) => Ok(value),
229///         Err(err) => Err(TomlError::custom(format!("Invalid time: {err}"), span)),
230///     }
231/// }
232///
233/// fn extract_timezone(
234///     datetime: &toml_spanner::DateTime,
235///     span: TomlSpan,
236/// ) -> Result<jiff::tz::TimeZone, TomlError> {
237///     let Some(offset) = datetime.offset() else {
238///         return Err(TomlError::custom("Missing offset component", span));
239///     };
240///     match offset {
241///         toml_spanner::TimeOffset::Z => Ok(jiff::tz::TimeZone::UTC),
242///         toml_spanner::TimeOffset::Custom { minutes } => {
243///             match jiff::tz::Offset::from_seconds(minutes as i32 * 60) {
244///                 Ok(jiff_offset) => Ok(jiff::tz::TimeZone::fixed(jiff_offset)),
245///                 Err(err) => Err(TomlError::custom(format!("Invalid offset: {err}"), span)),
246///             }
247///         }
248///     }
249/// }
250///
251/// fn to_jiff_date(item: &toml_spanner::Item<'_>) -> Result<jiff::civil::Date, TomlError> {
252///     let Some(datetime) = item.as_datetime() else {
253///         return Err(item.expected(&"date"));
254///     };
255///
256///     if datetime.time().is_some() {
257///         return Err(TomlError::custom(
258///             "Expected lone date but found time",
259///             item.span(),
260///         ));
261///     };
262///
263///     extract_date(datetime, item.span())
264/// }
265///
266/// fn to_jiff_datetime(item: &toml_spanner::Item<'_>) -> Result<jiff::civil::DateTime, TomlError> {
267///     let Some(datetime) = item.as_datetime() else {
268///         return Err(item.expected(&"civil datetime"));
269///     };
270///
271///     if datetime.offset().is_some() {
272///         return Err(TomlError::custom(
273///             "Expected naive timestamp but found offset",
274///             item.span(),
275///         ));
276///     };
277///
278///     Ok(jiff::civil::DateTime::from_parts(
279///         extract_date(datetime, item.span())?,
280///         extract_time(datetime, item.span())?,
281///     ))
282/// }
283///
284/// fn to_jiff_timestamp(item: &toml_spanner::Item<'_>) -> Result<jiff::Timestamp, TomlError> {
285///     let Some(datetime) = item.as_datetime() else {
286///         return Err(item.expected(&"timestamp"));
287///     };
288///     let civil = jiff::civil::DateTime::from_parts(
289///         extract_date(datetime, item.span())?,
290///         extract_time(datetime, item.span())?,
291///     );
292///     let timezone = extract_timezone(datetime, item.span())?;
293///     match timezone.to_timestamp(civil) {
294///         Ok(value) => Ok(value),
295///         Err(err) => Err(TomlError::custom(
296///             format!("Invalid timestamp: {err}"),
297///             item.span(),
298///         )),
299///     }
300/// }
301///
302/// #[derive(Debug)]
303/// pub struct TimeConfig {
304///     pub date: jiff::civil::Date,
305///     pub datetime: jiff::civil::DateTime,
306///     pub timestamp: jiff::Timestamp,
307/// }
308///
309/// impl<'de> FromToml<'de> for TimeConfig {
310///     fn from_toml(
311///         ctx: &mut toml_spanner::Context<'de>,
312///         value: &toml_spanner::Item<'de>,
313///     ) -> Result<Self, toml_spanner::Failed> {
314///         let mut th = value.table_helper(ctx)?;
315///         let config = TimeConfig {
316///             date: th.required_mapped("date", to_jiff_date)?,
317///             datetime: th.required_mapped("datetime", to_jiff_datetime)?,
318///             timestamp: th.required_mapped("timestamp", to_jiff_timestamp)?,
319///         };
320///         Ok(config)
321///     }
322/// }
323///
324/// fn main() {
325///     let arena = toml_spanner::Arena::new();
326///
327///     let toml_doc = r#"
328///         date = 1997-02-28
329///         datetime = 2066-01-30T14:45:00
330///         timestamp = 3291-12-01T00:45:00Z
331///     "#;
332///     let mut doc = toml_spanner::parse(toml_doc, &arena).unwrap();
333///     let config: TimeConfig = doc.to().unwrap();
334///     println!("{:#?}", config);
335/// }
336/// ```
337///
338/// </details>
339#[derive(Clone, Copy)]
340#[repr(C, align(8))]
341pub struct DateTime {
342    date: Date,
343
344    flags: u8,
345
346    hour: u8,
347    minute: u8,
348    seconds: u8,
349
350    nanos: u32,
351    offset_minutes: i16,
352}
353
354impl std::fmt::Debug for DateTime {
355    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
356        f.debug_struct("DateTime")
357            .field("date", &self.date())
358            .field("time", &self.time())
359            .field("offset", &self.offset())
360            .finish()
361    }
362}
363
364impl PartialEq for DateTime {
365    fn eq(&self, other: &Self) -> bool {
366        // The obvious implementation ends up with huge amount of binary bloat.
367        // this unsafety dropped llvm-lines by 350.
368        #[repr(C)]
369        struct Raw {
370            header: u64,
371            offset: u32,
372            nanos: i16,
373        }
374        // Safety: DateTime and Raw have identical layouts, so transmuting between them is safe.
375        let rhs = unsafe { &*(self as *const _ as *const Raw) };
376        let lhs = unsafe { &*(other as *const _ as *const Raw) };
377        (rhs.header == lhs.header) & (rhs.offset == lhs.offset) & (rhs.nanos == lhs.nanos)
378    }
379}
380
381impl Eq for DateTime {}
382
383const HAS_DATE: u8 = 1 << 0;
384const HAS_TIME: u8 = 1 << 1;
385const HAS_SECONDS: u8 = 1 << 2;
386const NANO_SHIFT: u8 = 4;
387
388fn is_leap_year(year: u16) -> bool {
389    (((year as u64 * 1073750999) as u32) & 3221352463) <= 126976
390}
391
392fn days_in_month(year: u16, month: u8) -> u8 {
393    const DAYS: [u8; 13] = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
394    if month == 2 && is_leap_year(year) {
395        29
396    } else {
397        DAYS[month as usize]
398    }
399}
400
401/// Error returned when parsing a [`DateTime`] from a string via [`FromStr`].
402#[non_exhaustive]
403#[derive(Debug)]
404pub enum DateTimeError {
405    /// The input string is not a valid TOML datetime.
406    Invalid,
407}
408
409impl std::fmt::Display for DateTimeError {
410    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
411        <DateTimeError as std::fmt::Debug>::fmt(self, f)
412    }
413}
414
415impl std::error::Error for DateTimeError {}
416
417impl FromStr for DateTime {
418    type Err = DateTimeError;
419
420    fn from_str(s: &str) -> Result<Self, Self::Err> {
421        DateTime::munch(s.as_bytes())
422            .ok()
423            .filter(|(amount, _)| *amount == s.len())
424            .map(|(_, dt)| dt)
425            .ok_or(DateTimeError::Invalid)
426    }
427}
428
429impl DateTime {
430    /// Maximum number of bytes produced by [`DateTime::format`].
431    ///
432    /// Use this to size the [`MaybeUninit`] buffer passed to [`DateTime::format`].
433    ///
434    /// [`MaybeUninit`]: std::mem::MaybeUninit
435    pub const MAX_FORMAT_LEN: usize = 40;
436    /// Returns the time component, or [`None`] for a local-date value.
437    pub fn time(&self) -> Option<Time> {
438        if self.flags & HAS_TIME != 0 {
439            Some(Time {
440                flags: self.flags,
441                hour: self.hour,
442                minute: self.minute,
443                second: self.seconds,
444                nanosecond: self.nanos,
445            })
446        } else {
447            None
448        }
449    }
450    pub(crate) fn munch(input: &[u8]) -> Result<(usize, DateTime), &'static str> {
451        enum State {
452            Year,
453            Month,
454            Day,
455            Hour,
456            Minute,
457            Second,
458            Frac,
459            OffHour,
460            OffMin,
461        }
462        let mut n = 0;
463        while n < input.len() && input[n].is_ascii_digit() {
464            n += 1;
465        }
466        let mut state = match input.get(n) {
467            Some(b':') if n == 2 => State::Hour,
468            Some(b'-') if n >= 2 => State::Year,
469            _ => return Err(""),
470        };
471
472        let mut value = DateTime {
473            date: Date {
474                year: 0,
475                month: 0,
476                day: 0,
477            },
478            flags: 0,
479            hour: 0,
480            minute: 0,
481            seconds: 0,
482            offset_minutes: i16::MIN,
483            nanos: 0,
484        };
485
486        let mut current = 0u32;
487        let mut len = 0u32;
488        let mut off_sign: i16 = 1;
489        let mut off_hour: u8 = 0;
490        let mut i = 0usize;
491        let valid: bool;
492
493        'outer: loop {
494            let byte = input.get(i).copied().unwrap_or(0);
495            if byte.is_ascii_digit() {
496                len += 1;
497                if len <= 9 {
498                    current = current * 10 + (byte - b'0') as u32;
499                }
500                i += 1;
501                continue;
502            }
503            'next: {
504                match state {
505                    State::Year => {
506                        if len != 4 {
507                            return Err("expected 4-digit year");
508                        }
509                        if byte != b'-' {
510                            return Err("");
511                        }
512                        value.date.year = current as u16;
513                        state = State::Month;
514                        break 'next;
515                    }
516                    State::Month => {
517                        let m = current as u8;
518                        if len != 2 {
519                            return Err("expected 2-digit month");
520                        }
521                        if byte != b'-' {
522                            return Err("");
523                        }
524                        if m < 1 || m > 12 {
525                            return Err("month is out of range");
526                        }
527                        value.date.month = m;
528                        state = State::Day;
529                        break 'next;
530                    }
531                    State::Day => {
532                        let d = current as u8;
533                        if len != 2 {
534                            return Err("expected 2-digit day");
535                        }
536                        if d < 1 || d > days_in_month(value.date.year, value.date.month) {
537                            return Err("day is out of range");
538                        }
539                        value.date.day = d;
540                        value.flags |= HAS_DATE;
541                        if byte == b'T'
542                            || byte == b't'
543                            || (byte == b' '
544                                && input.get(i + 1).is_some_and(|b| b.is_ascii_digit()))
545                        {
546                            state = State::Hour;
547                            break 'next;
548                        } else {
549                            valid = true;
550                            break 'outer;
551                        }
552                    }
553                    State::Hour => {
554                        let h = current as u8;
555                        if len != 2 {
556                            return Err("expected 2-digit hour");
557                        }
558                        if byte != b':' {
559                            return Err("incomplete time");
560                        }
561                        if h > 23 {
562                            return Err("hour is out of range");
563                        }
564                        value.hour = h;
565                        state = State::Minute;
566                        break 'next;
567                    }
568                    State::Minute => {
569                        let m = current as u8;
570                        if len != 2 {
571                            return Err("expected 2-digit minute");
572                        }
573                        if m > 59 {
574                            return Err("minute is out of range");
575                        }
576                        value.minute = m;
577                        value.flags |= HAS_TIME;
578                        if byte == b':' {
579                            state = State::Second;
580                            break 'next;
581                        }
582                    }
583                    State::Second => {
584                        let s = current as u8;
585                        if len != 2 {
586                            return Err("expected 2-digit second");
587                        }
588                        if s > 60 {
589                            return Err("second is out of range");
590                        }
591                        value.seconds = s;
592                        value.flags |= HAS_SECONDS;
593                        if byte == b'.' {
594                            state = State::Frac;
595                            break 'next;
596                        }
597                    }
598                    State::Frac => {
599                        if len == 0 {
600                            return Err("expected fractional digits after decimal point");
601                        }
602                        let digit_count = if len > 9 { 9u8 } else { len as u8 };
603                        let mut nanos = current;
604                        let mut s = digit_count;
605                        while s < 9 {
606                            nanos *= 10;
607                            s += 1;
608                        }
609                        value.nanos = nanos;
610                        value.flags |= digit_count << NANO_SHIFT;
611                    }
612                    State::OffHour => {
613                        let h = current as u8;
614                        if len != 2 {
615                            return Err("expected 2-digit offset hour");
616                        }
617                        if byte != b':' {
618                            return Err("incomplete offset");
619                        }
620                        if h > 23 {
621                            return Err("offset hour is out of range");
622                        }
623                        off_hour = h;
624                        state = State::OffMin;
625                        break 'next;
626                    }
627                    State::OffMin => {
628                        if len != 2 {
629                            return Err("expected 2-digit offset minute");
630                        }
631                        if current > 59 {
632                            return Err("offset minute is out of range");
633                        }
634                        value.offset_minutes = off_sign * (off_hour as i16 * 60 + current as i16);
635                        valid = true;
636                        break 'outer;
637                    }
638                }
639                match byte {
640                    b'Z' | b'z' => {
641                        value.offset_minutes = i16::MAX;
642                        i += 1;
643                        valid = true;
644                        break 'outer;
645                    }
646                    b'+' => {
647                        off_sign = 1;
648                        state = State::OffHour;
649                    }
650                    b'-' => {
651                        off_sign = -1;
652                        state = State::OffHour;
653                    }
654                    _ => {
655                        valid = true;
656                        break 'outer;
657                    }
658                }
659            }
660            i += 1;
661            current = 0;
662            len = 0;
663        }
664        if !valid || (value.flags & HAS_DATE == 0 && value.offset_minutes != i16::MIN) {
665            return Err("");
666        }
667        Ok((i, value))
668    }
669
670    /// Formats this datetime into the provided buffer and returns the result as a `&str`.
671    ///
672    /// The output follows RFC 3339 formatting and matches the TOML serialization
673    /// of the value. The caller must supply an uninitializebuffer of [`MAX_FORMAT_LEN`](Self::MAX_FORMAT_LEN) bytes;
674    /// the returned `&str` borrows from that buffer, starting from the beginning.
675    ///
676    /// # Examples
677    ///
678    /// ```
679    /// use std::mem::MaybeUninit;
680    /// use toml_spanner::DateTime;
681    ///
682    /// let dt: DateTime = "2026-01-04T12:30:45Z".parse().unwrap();
683    /// let mut buf = MaybeUninit::uninit();
684    /// assert_eq!(dt.format(&mut buf), "2026-01-04T12:30:45Z");
685    /// assert_eq!(size_of_val(&buf), DateTime::MAX_FORMAT_LEN);
686    /// ```
687    pub fn format<'a>(&self, buf: &'a mut MaybeUninit<[u8; DateTime::MAX_FORMAT_LEN]>) -> &'a str {
688        #[inline(always)]
689        fn write_byte(
690            buf: &mut [MaybeUninit<u8>; DateTime::MAX_FORMAT_LEN],
691            pos: &mut usize,
692            b: u8,
693        ) {
694            buf[*pos].write(b);
695            *pos += 1;
696        }
697
698        #[inline(always)]
699        fn write_2(
700            buf: &mut [MaybeUninit<u8>; DateTime::MAX_FORMAT_LEN],
701            pos: &mut usize,
702            val: u8,
703        ) {
704            buf[*pos].write(b'0' + val / 10);
705            buf[*pos + 1].write(b'0' + val % 10);
706            *pos += 2;
707        }
708
709        #[inline(always)]
710        fn write_4(
711            buf: &mut [MaybeUninit<u8>; DateTime::MAX_FORMAT_LEN],
712            pos: &mut usize,
713            val: u16,
714        ) {
715            buf[*pos].write(b'0' + (val / 1000) as u8);
716            buf[*pos + 1].write(b'0' + ((val / 100) % 10) as u8);
717            buf[*pos + 2].write(b'0' + ((val / 10) % 10) as u8);
718            buf[*pos + 3].write(b'0' + (val % 10) as u8);
719            *pos += 4;
720        }
721
722        #[inline(always)]
723        fn write_frac(
724            buf: &mut [MaybeUninit<u8>; DateTime::MAX_FORMAT_LEN],
725            pos: &mut usize,
726            nanos: u32,
727            digit_count: u8,
728        ) {
729            let mut val = nanos;
730            let mut i: usize = 8;
731            loop {
732                buf[*pos + i].write(b'0' + (val % 10) as u8);
733                val /= 10;
734                if i == 0 {
735                    break;
736                }
737                i -= 1;
738            }
739            *pos += digit_count as usize;
740        }
741
742        // SAFETY: MaybeUninit<u8> has identical layout to u8
743        let buf: &mut [MaybeUninit<u8>; Self::MAX_FORMAT_LEN] = unsafe {
744            &mut *buf
745                .as_mut_ptr()
746                .cast::<[MaybeUninit<u8>; Self::MAX_FORMAT_LEN]>()
747        };
748        let mut pos: usize = 0;
749
750        if self.flags & HAS_DATE != 0 {
751            write_4(buf, &mut pos, self.date.year);
752            write_byte(buf, &mut pos, b'-');
753            write_2(buf, &mut pos, self.date.month);
754            write_byte(buf, &mut pos, b'-');
755            write_2(buf, &mut pos, self.date.day);
756
757            if self.flags & HAS_TIME != 0 {
758                write_byte(buf, &mut pos, b'T');
759            }
760        }
761
762        if self.flags & HAS_TIME != 0 {
763            write_2(buf, &mut pos, self.hour);
764            write_byte(buf, &mut pos, b':');
765            write_2(buf, &mut pos, self.minute);
766            write_byte(buf, &mut pos, b':');
767            write_2(buf, &mut pos, self.seconds);
768
769            if self.flags & HAS_SECONDS != 0 {
770                let digit_count = (self.flags >> NANO_SHIFT) & 0xF;
771                if digit_count > 0 {
772                    write_byte(buf, &mut pos, b'.');
773                    write_frac(buf, &mut pos, self.nanos, digit_count);
774                }
775            }
776
777            if self.offset_minutes != i16::MIN {
778                if self.offset_minutes == i16::MAX {
779                    write_byte(buf, &mut pos, b'Z');
780                } else {
781                    let (sign, abs) = if self.offset_minutes < 0 {
782                        (b'-', (-self.offset_minutes) as u16)
783                    } else {
784                        (b'+', self.offset_minutes as u16)
785                    };
786                    write_byte(buf, &mut pos, sign);
787                    write_2(buf, &mut pos, (abs / 60) as u8);
788                    write_byte(buf, &mut pos, b':');
789                    write_2(buf, &mut pos, (abs % 60) as u8);
790                }
791            }
792        }
793
794        // SAFETY: buf[..pos] has been fully initialized by the write calls above,
795        // and all written bytes are valid ASCII digits/punctuation.
796        unsafe {
797            std::str::from_utf8_unchecked(std::slice::from_raw_parts(buf.as_ptr().cast(), pos))
798        }
799    }
800
801    /// Returns the date component, or [`None`] for a local-time value.
802    pub fn date(&self) -> Option<Date> {
803        if self.flags & HAS_DATE != 0 {
804            Some(self.date)
805        } else {
806            None
807        }
808    }
809
810    /// Returns the UTC offset, or [`None`] for local date-times and local times.
811    pub fn offset(&self) -> Option<TimeOffset> {
812        match self.offset_minutes {
813            i16::MAX => Some(TimeOffset::Z),
814            i16::MIN => None,
815            minutes => Some(TimeOffset::Custom { minutes }),
816        }
817    }
818}