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::{Deserialize, 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> Deserialize<'de> for TimeConfig {
310/// fn deserialize(
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 root = toml_spanner::parse(toml_doc, &arena).unwrap();
333/// let config: TimeConfig = root.deserialize().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 offset_minutes: i16,
351 nanos: u32,
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
364const HAS_DATE: u8 = 1 << 0;
365const HAS_TIME: u8 = 1 << 1;
366const HAS_SECONDS: u8 = 1 << 2;
367const NANO_SHIFT: u8 = 4;
368
369fn is_leap_year(year: u16) -> bool {
370 (((year as u64 * 1073750999) as u32) & 3221352463) <= 126976
371}
372
373fn days_in_month(year: u16, month: u8) -> u8 {
374 const DAYS: [u8; 13] = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
375 if month == 2 && is_leap_year(year) {
376 29
377 } else {
378 DAYS[month as usize]
379 }
380}
381
382/// Error returned when parsing a [`DateTime`] from a string via [`FromStr`].
383#[non_exhaustive]
384#[derive(Debug)]
385pub enum DateTimeError {
386 /// The input string is not a valid TOML datetime.
387 Invalid,
388}
389
390impl std::fmt::Display for DateTimeError {
391 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
392 <DateTimeError as std::fmt::Debug>::fmt(self, f)
393 }
394}
395
396impl std::error::Error for DateTimeError {}
397
398impl FromStr for DateTime {
399 type Err = DateTimeError;
400
401 fn from_str(s: &str) -> Result<Self, Self::Err> {
402 DateTime::munch(s.as_bytes())
403 .filter(|(amount, _)| *amount == s.len())
404 .map(|(_, dt)| dt)
405 .ok_or(DateTimeError::Invalid)
406 }
407}
408
409impl DateTime {
410 /// Maximum number of bytes produced by [`DateTime::format`].
411 ///
412 /// Use this to size the [`MaybeUninit`] buffer passed to [`DateTime::format`].
413 ///
414 /// [`MaybeUninit`]: std::mem::MaybeUninit
415 pub const MAX_FORMAT_LEN: usize = 40;
416 /// Returns the time component, or [`None`] for a local-date value.
417 pub fn time(&self) -> Option<Time> {
418 if self.flags & HAS_TIME != 0 {
419 Some(Time {
420 flags: self.flags,
421 hour: self.hour,
422 minute: self.minute,
423 second: self.seconds,
424 nanosecond: self.nanos,
425 })
426 } else {
427 None
428 }
429 }
430 pub(crate) fn munch(input: &[u8]) -> Option<(usize, DateTime)> {
431 enum State {
432 Year,
433 Month,
434 Day,
435 Hour,
436 Minute,
437 Second,
438 Frac,
439 OffHour,
440 OffMin,
441 }
442 let mut state = match input {
443 [_, _, b':', _, _, ..] => State::Hour,
444 [_, _, _, _, b'-', _, _, b'-', ..] => State::Year,
445 _ => return None,
446 };
447
448 let mut value = DateTime {
449 date: Date {
450 year: 0,
451 month: 0,
452 day: 0,
453 },
454 flags: 0,
455 hour: 0,
456 minute: 0,
457 seconds: 0,
458 offset_minutes: i16::MIN,
459 nanos: 0,
460 };
461
462 let mut current = 0u32;
463 let mut len = 0u32;
464 let mut off_sign: i16 = 1;
465 let mut off_hour: u8 = 0;
466 let mut i = 0usize;
467 let mut valid = false;
468
469 'outer: loop {
470 let byte = input.get(i).copied().unwrap_or(0);
471 if byte.is_ascii_digit() {
472 len += 1;
473 if len <= 9 {
474 current = current * 10 + (byte - b'0') as u32;
475 }
476 i += 1;
477 continue;
478 }
479 'next: {
480 match state {
481 State::Year => {
482 if len != 4 || byte != b'-' {
483 break 'outer;
484 }
485 value.date.year = current as u16;
486 state = State::Month;
487 break 'next;
488 }
489 State::Month => {
490 let m = current as u8;
491 if len != 2 || byte != b'-' || m < 1 || m > 12 {
492 break 'outer;
493 }
494 value.date.month = m;
495 state = State::Day;
496 break 'next;
497 }
498 State::Day => {
499 let d = current as u8;
500 if len != 2 || d < 1 || d > days_in_month(value.date.year, value.date.month)
501 {
502 break 'outer;
503 }
504 value.date.day = d;
505 value.flags |= HAS_DATE;
506 if byte == b'T'
507 || byte == b't'
508 || (byte == b' '
509 && input.get(i + 1).is_some_and(|b| b.is_ascii_digit()))
510 {
511 state = State::Hour;
512 break 'next;
513 } else {
514 valid = true;
515 break 'outer;
516 }
517 }
518 State::Hour => {
519 let h = current as u8;
520 if len != 2 || byte != b':' || h > 23 {
521 break 'outer;
522 }
523 value.hour = h;
524 state = State::Minute;
525 break 'next;
526 }
527 State::Minute => {
528 let m = current as u8;
529 if len != 2 || m > 59 {
530 break 'outer;
531 }
532 value.minute = m;
533 value.flags |= HAS_TIME;
534 if byte == b':' {
535 state = State::Second;
536 break 'next;
537 } else {
538 // fallthorugh to check offset
539 }
540 }
541 State::Second => {
542 let s = current as u8;
543 // Note: Second is allowed to be 60, for leap second rule.
544 if len != 2 || s > 60 {
545 break 'outer;
546 }
547 value.seconds = s;
548 value.flags |= HAS_SECONDS;
549 if byte == b'.' {
550 state = State::Frac;
551 break 'next;
552 } else {
553 // fallthrough to check outer
554 }
555 }
556 State::Frac => {
557 if len == 0 {
558 break 'outer;
559 }
560 let digit_count = if len > 9 { 9u8 } else { len as u8 };
561 let mut nanos = current;
562 let mut s = digit_count;
563 while s < 9 {
564 nanos *= 10;
565 s += 1;
566 }
567 value.nanos = nanos;
568 value.flags |= digit_count << NANO_SHIFT;
569 // fallthrough to check outer
570 }
571 State::OffHour => {
572 let h = current as u8;
573 if len != 2 || byte != b':' || h > 23 {
574 break 'outer;
575 }
576 off_hour = h;
577 state = State::OffMin;
578 break 'next;
579 }
580 State::OffMin => {
581 if len != 2 || current > 59 {
582 break 'outer;
583 }
584 value.offset_minutes = off_sign * (off_hour as i16 * 60 + current as i16);
585 valid = true;
586 break 'outer;
587 }
588 }
589 match byte {
590 b'Z' | b'z' => {
591 value.offset_minutes = i16::MAX;
592 i += 1;
593 valid = true;
594 break 'outer;
595 }
596 b'+' => {
597 off_sign = 1;
598 state = State::OffHour;
599 }
600 b'-' => {
601 off_sign = -1;
602 state = State::OffHour;
603 }
604 _ => {
605 valid = true;
606 break 'outer;
607 }
608 }
609 }
610 i += 1;
611 current = 0;
612 len = 0;
613 }
614 if !valid || (value.flags & HAS_DATE == 0 && value.offset_minutes != i16::MIN) {
615 return None;
616 }
617 Some((i, value))
618 }
619
620 /// Formats this datetime into the provided buffer and returns the result as a `&str`.
621 ///
622 /// The output follows RFC 3339 formatting and matches the TOML serialization
623 /// of the value. The caller must supply an uninitializebuffer of [`MAX_FORMAT_LEN`](Self::MAX_FORMAT_LEN) bytes;
624 /// the returned `&str` borrows from that buffer, starting from the beginning.
625 ///
626 /// # Examples
627 ///
628 /// ```
629 /// use std::mem::MaybeUninit;
630 /// use toml_spanner::DateTime;
631 ///
632 /// let dt: DateTime = "2026-01-04T12:30:45Z".parse().unwrap();
633 /// let mut buf = MaybeUninit::uninit();
634 /// assert_eq!(dt.format(&mut buf), "2026-01-04T12:30:45Z");
635 /// assert_eq!(size_of_val(&buf), DateTime::MAX_FORMAT_LEN);
636 /// ```
637 pub fn format<'a>(&self, buf: &'a mut MaybeUninit<[u8; DateTime::MAX_FORMAT_LEN]>) -> &'a str {
638 #[inline(always)]
639 fn write_byte(
640 buf: &mut [MaybeUninit<u8>; DateTime::MAX_FORMAT_LEN],
641 pos: &mut usize,
642 b: u8,
643 ) {
644 buf[*pos].write(b);
645 *pos += 1;
646 }
647
648 #[inline(always)]
649 fn write_2(
650 buf: &mut [MaybeUninit<u8>; DateTime::MAX_FORMAT_LEN],
651 pos: &mut usize,
652 val: u8,
653 ) {
654 buf[*pos].write(b'0' + val / 10);
655 buf[*pos + 1].write(b'0' + val % 10);
656 *pos += 2;
657 }
658
659 #[inline(always)]
660 fn write_4(
661 buf: &mut [MaybeUninit<u8>; DateTime::MAX_FORMAT_LEN],
662 pos: &mut usize,
663 val: u16,
664 ) {
665 buf[*pos].write(b'0' + (val / 1000) as u8);
666 buf[*pos + 1].write(b'0' + ((val / 100) % 10) as u8);
667 buf[*pos + 2].write(b'0' + ((val / 10) % 10) as u8);
668 buf[*pos + 3].write(b'0' + (val % 10) as u8);
669 *pos += 4;
670 }
671
672 #[inline(always)]
673 fn write_frac(
674 buf: &mut [MaybeUninit<u8>; DateTime::MAX_FORMAT_LEN],
675 pos: &mut usize,
676 nanos: u32,
677 digit_count: u8,
678 ) {
679 let mut val = nanos;
680 let mut i: usize = 8;
681 loop {
682 buf[*pos + i].write(b'0' + (val % 10) as u8);
683 val /= 10;
684 if i == 0 {
685 break;
686 }
687 i -= 1;
688 }
689 *pos += digit_count as usize;
690 }
691
692 // SAFETY: MaybeUninit<u8> has identical layout to u8
693 let buf: &mut [MaybeUninit<u8>; Self::MAX_FORMAT_LEN] = unsafe {
694 &mut *buf
695 .as_mut_ptr()
696 .cast::<[MaybeUninit<u8>; Self::MAX_FORMAT_LEN]>()
697 };
698 let mut pos: usize = 0;
699
700 if self.flags & HAS_DATE != 0 {
701 write_4(buf, &mut pos, self.date.year);
702 write_byte(buf, &mut pos, b'-');
703 write_2(buf, &mut pos, self.date.month);
704 write_byte(buf, &mut pos, b'-');
705 write_2(buf, &mut pos, self.date.day);
706
707 if self.flags & HAS_TIME != 0 {
708 write_byte(buf, &mut pos, b'T');
709 }
710 }
711
712 if self.flags & HAS_TIME != 0 {
713 write_2(buf, &mut pos, self.hour);
714 write_byte(buf, &mut pos, b':');
715 write_2(buf, &mut pos, self.minute);
716 write_byte(buf, &mut pos, b':');
717 write_2(buf, &mut pos, self.seconds);
718
719 if self.flags & HAS_SECONDS != 0 {
720 let digit_count = (self.flags >> NANO_SHIFT) & 0xF;
721 if digit_count > 0 {
722 write_byte(buf, &mut pos, b'.');
723 write_frac(buf, &mut pos, self.nanos, digit_count);
724 }
725 }
726
727 if self.offset_minutes != i16::MIN {
728 if self.offset_minutes == i16::MAX {
729 write_byte(buf, &mut pos, b'Z');
730 } else {
731 let (sign, abs) = if self.offset_minutes < 0 {
732 (b'-', (-self.offset_minutes) as u16)
733 } else {
734 (b'+', self.offset_minutes as u16)
735 };
736 write_byte(buf, &mut pos, sign);
737 write_2(buf, &mut pos, (abs / 60) as u8);
738 write_byte(buf, &mut pos, b':');
739 write_2(buf, &mut pos, (abs % 60) as u8);
740 }
741 }
742 }
743
744 // SAFETY: buf[..pos] has been fully initialized by the write calls above,
745 // and all written bytes are valid ASCII digits/punctuation.
746 unsafe {
747 std::str::from_utf8_unchecked(std::slice::from_raw_parts(buf.as_ptr().cast(), pos))
748 }
749 }
750
751 /// Returns the date component, or [`None`] for a local-time value.
752 pub fn date(&self) -> Option<Date> {
753 if self.flags & HAS_DATE != 0 {
754 Some(self.date)
755 } else {
756 None
757 }
758 }
759
760 /// Returns the UTC offset, or [`None`] for local date-times and local times.
761 pub fn offset(&self) -> Option<TimeOffset> {
762 match self.offset_minutes {
763 i16::MAX => Some(TimeOffset::Z),
764 i16::MIN => None,
765 minutes => Some(TimeOffset::Custom { minutes }),
766 }
767 }
768}