toml_datetime/
datetime.rs

1use std::error;
2use std::fmt;
3use std::str::{self, FromStr};
4
5#[cfg(feature = "serde")]
6use serde::{de, ser};
7
8/// A parsed TOML datetime value
9///
10/// This structure is intended to represent the datetime primitive type that can
11/// be encoded into TOML documents. This type is a parsed version that contains
12/// all metadata internally.
13///
14/// Currently this type is intentionally conservative and only supports
15/// `to_string` as an accessor. Over time though it's intended that it'll grow
16/// more support!
17///
18/// Note that if you're using `Deserialize` to deserialize a TOML document, you
19/// can use this as a placeholder for where you're expecting a datetime to be
20/// specified.
21///
22/// Also note though that while this type implements `Serialize` and
23/// `Deserialize` it's only recommended to use this type with the TOML format,
24/// otherwise encoded in other formats it may look a little odd.
25///
26/// Depending on how the option values are used, this struct will correspond
27/// with one of the following four datetimes from the [TOML v1.0.0 spec]:
28///
29/// | `date`    | `time`    | `offset`  | TOML type          |
30/// | --------- | --------- | --------- | ------------------ |
31/// | `Some(_)` | `Some(_)` | `Some(_)` | [Offset Date-Time] |
32/// | `Some(_)` | `Some(_)` | `None`    | [Local Date-Time]  |
33/// | `Some(_)` | `None`    | `None`    | [Local Date]       |
34/// | `None`    | `Some(_)` | `None`    | [Local Time]       |
35///
36/// **1. Offset Date-Time**: If all the optional values are used, `Datetime`
37/// corresponds to an [Offset Date-Time]. From the TOML v1.0.0 spec:
38///
39/// > To unambiguously represent a specific instant in time, you may use an
40/// > RFC 3339 formatted date-time with offset.
41/// >
42/// > ```toml
43/// > odt1 = 1979-05-27T07:32:00Z
44/// > odt2 = 1979-05-27T00:32:00-07:00
45/// > odt3 = 1979-05-27T00:32:00.999999-07:00
46/// > ```
47/// >
48/// > For the sake of readability, you may replace the T delimiter between date
49/// > and time with a space character (as permitted by RFC 3339 section 5.6).
50/// >
51/// > ```toml
52/// > odt4 = 1979-05-27 07:32:00Z
53/// > ```
54///
55/// **2. Local Date-Time**: If `date` and `time` are given but `offset` is
56/// `None`, `Datetime` corresponds to a [Local Date-Time]. From the spec:
57///
58/// > If you omit the offset from an RFC 3339 formatted date-time, it will
59/// > represent the given date-time without any relation to an offset or
60/// > timezone. It cannot be converted to an instant in time without additional
61/// > information. Conversion to an instant, if required, is implementation-
62/// > specific.
63/// >
64/// > ```toml
65/// > ldt1 = 1979-05-27T07:32:00
66/// > ldt2 = 1979-05-27T00:32:00.999999
67/// > ```
68///
69/// **3. Local Date**: If only `date` is given, `Datetime` corresponds to a
70/// [Local Date]; see the docs for [`Date`].
71///
72/// **4. Local Time**: If only `time` is given, `Datetime` corresponds to a
73/// [Local Time]; see the docs for [`Time`].
74///
75/// [TOML v1.0.0 spec]: https://toml.io/en/v1.0.0
76/// [Offset Date-Time]: https://toml.io/en/v1.0.0#offset-date-time
77/// [Local Date-Time]: https://toml.io/en/v1.0.0#local-date-time
78/// [Local Date]: https://toml.io/en/v1.0.0#local-date
79/// [Local Time]: https://toml.io/en/v1.0.0#local-time
80#[derive(PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Debug)]
81pub struct Datetime {
82    /// Optional date.
83    /// Required for: *Offset Date-Time*, *Local Date-Time*, *Local Date*.
84    pub date: Option<Date>,
85
86    /// Optional time.
87    /// Required for: *Offset Date-Time*, *Local Date-Time*, *Local Time*.
88    pub time: Option<Time>,
89
90    /// Optional offset.
91    /// Required for: *Offset Date-Time*.
92    pub offset: Option<Offset>,
93}
94
95/// Error returned from parsing a `Datetime` in the `FromStr` implementation.
96#[derive(Debug, Clone)]
97#[non_exhaustive]
98pub struct DatetimeParseError {}
99
100// Currently serde itself doesn't have a datetime type, so we map our `Datetime`
101// to a special value in the serde data model. Namely one with these special
102// fields/struct names.
103//
104// In general the TOML encoder/decoder will catch this and not literally emit
105// these strings but rather emit datetimes as they're intended.
106#[doc(hidden)]
107#[cfg(feature = "serde")]
108pub const FIELD: &str = "$__toml_private_datetime";
109#[doc(hidden)]
110#[cfg(feature = "serde")]
111pub const NAME: &str = "$__toml_private_Datetime";
112
113/// A parsed TOML date value
114///
115/// May be part of a [`Datetime`]. Alone, `Date` corresponds to a [Local Date].
116/// From the TOML v1.0.0 spec:
117///
118/// > If you include only the date portion of an RFC 3339 formatted date-time,
119/// > it will represent that entire day without any relation to an offset or
120/// > timezone.
121/// >
122/// > ```toml
123/// > ld1 = 1979-05-27
124/// > ```
125///
126/// [Local Date]: https://toml.io/en/v1.0.0#local-date
127#[derive(PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Debug)]
128pub struct Date {
129    /// Year: four digits
130    pub year: u16,
131    /// Month: 1 to 12
132    pub month: u8,
133    /// Day: 1 to {28, 29, 30, 31} (based on month/year)
134    pub day: u8,
135}
136
137/// A parsed TOML time value
138///
139/// May be part of a [`Datetime`]. Alone, `Time` corresponds to a [Local Time].
140/// From the TOML v1.0.0 spec:
141///
142/// > If you include only the time portion of an RFC 3339 formatted date-time,
143/// > it will represent that time of day without any relation to a specific
144/// > day or any offset or timezone.
145/// >
146/// > ```toml
147/// > lt1 = 07:32:00
148/// > lt2 = 00:32:00.999999
149/// > ```
150/// >
151/// > Millisecond precision is required. Further precision of fractional
152/// > seconds is implementation-specific. If the value contains greater
153/// > precision than the implementation can support, the additional precision
154/// > must be truncated, not rounded.
155///
156/// [Local Time]: https://toml.io/en/v1.0.0#local-time
157#[derive(PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Debug)]
158pub struct Time {
159    /// Hour: 0 to 23
160    pub hour: u8,
161    /// Minute: 0 to 59
162    pub minute: u8,
163    /// Second: 0 to {58, 59, 60} (based on leap second rules)
164    pub second: u8,
165    /// Nanosecond: 0 to `999_999_999`
166    pub nanosecond: u32,
167}
168
169/// A parsed TOML time offset
170///
171#[derive(PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Debug)]
172pub enum Offset {
173    /// > A suffix which, when applied to a time, denotes a UTC offset of 00:00;
174    /// > often spoken "Zulu" from the ICAO phonetic alphabet representation of
175    /// > the letter "Z". --- [RFC 3339 section 2]
176    ///
177    /// [RFC 3339 section 2]: https://datatracker.ietf.org/doc/html/rfc3339#section-2
178    Z,
179
180    /// Offset between local time and UTC
181    Custom {
182        /// Minutes: -`1_440..1_440`
183        minutes: i16,
184    },
185}
186
187impl Datetime {
188    #[cfg(feature = "serde")]
189    fn type_name(&self) -> &'static str {
190        match (
191            self.date.is_some(),
192            self.time.is_some(),
193            self.offset.is_some(),
194        ) {
195            (true, true, true) => "offset datetime",
196            (true, true, false) => "local datetime",
197            (true, false, false) => Date::type_name(),
198            (false, true, false) => Time::type_name(),
199            _ => unreachable!("unsupported datetime combination"),
200        }
201    }
202}
203
204impl Date {
205    #[cfg(feature = "serde")]
206    fn type_name() -> &'static str {
207        "local date"
208    }
209}
210
211impl Time {
212    #[cfg(feature = "serde")]
213    fn type_name() -> &'static str {
214        "local time"
215    }
216}
217
218impl From<Date> for Datetime {
219    fn from(other: Date) -> Self {
220        Datetime {
221            date: Some(other),
222            time: None,
223            offset: None,
224        }
225    }
226}
227
228impl From<Time> for Datetime {
229    fn from(other: Time) -> Self {
230        Datetime {
231            date: None,
232            time: Some(other),
233            offset: None,
234        }
235    }
236}
237
238impl fmt::Display for Datetime {
239    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
240        if let Some(ref date) = self.date {
241            write!(f, "{}", date)?;
242        }
243        if let Some(ref time) = self.time {
244            if self.date.is_some() {
245                write!(f, "T")?;
246            }
247            write!(f, "{}", time)?;
248        }
249        if let Some(ref offset) = self.offset {
250            write!(f, "{}", offset)?;
251        }
252        Ok(())
253    }
254}
255
256impl fmt::Display for Date {
257    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
258        write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
259    }
260}
261
262impl fmt::Display for Time {
263    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
264        write!(f, "{:02}:{:02}:{:02}", self.hour, self.minute, self.second)?;
265        if self.nanosecond != 0 {
266            let s = format!("{:09}", self.nanosecond);
267            write!(f, ".{}", s.trim_end_matches('0'))?;
268        }
269        Ok(())
270    }
271}
272
273impl fmt::Display for Offset {
274    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
275        match *self {
276            Offset::Z => write!(f, "Z"),
277            Offset::Custom { mut minutes } => {
278                let mut sign = '+';
279                if minutes < 0 {
280                    minutes *= -1;
281                    sign = '-';
282                }
283                let hours = minutes / 60;
284                let minutes = minutes % 60;
285                write!(f, "{}{:02}:{:02}", sign, hours, minutes)
286            }
287        }
288    }
289}
290
291impl FromStr for Datetime {
292    type Err = DatetimeParseError;
293
294    fn from_str(date: &str) -> Result<Datetime, DatetimeParseError> {
295        // Accepted formats:
296        //
297        // 0000-00-00T00:00:00.00Z
298        // 0000-00-00T00:00:00.00
299        // 0000-00-00
300        // 00:00:00.00
301        if date.len() < 3 {
302            return Err(DatetimeParseError {});
303        }
304        let mut offset_allowed = true;
305        let mut chars = date.chars();
306
307        // First up, parse the full date if we can
308        let full_date = if chars.clone().nth(2) == Some(':') {
309            offset_allowed = false;
310            None
311        } else {
312            let y1 = u16::from(digit(&mut chars)?);
313            let y2 = u16::from(digit(&mut chars)?);
314            let y3 = u16::from(digit(&mut chars)?);
315            let y4 = u16::from(digit(&mut chars)?);
316
317            match chars.next() {
318                Some('-') => {}
319                _ => return Err(DatetimeParseError {}),
320            }
321
322            let m1 = digit(&mut chars)?;
323            let m2 = digit(&mut chars)?;
324
325            match chars.next() {
326                Some('-') => {}
327                _ => return Err(DatetimeParseError {}),
328            }
329
330            let d1 = digit(&mut chars)?;
331            let d2 = digit(&mut chars)?;
332
333            let date = Date {
334                year: y1 * 1000 + y2 * 100 + y3 * 10 + y4,
335                month: m1 * 10 + m2,
336                day: d1 * 10 + d2,
337            };
338
339            if date.month < 1 || date.month > 12 {
340                return Err(DatetimeParseError {});
341            }
342            let is_leap_year =
343                (date.year % 4 == 0) && ((date.year % 100 != 0) || (date.year % 400 == 0));
344            let max_days_in_month = match date.month {
345                2 if is_leap_year => 29,
346                2 => 28,
347                4 | 6 | 9 | 11 => 30,
348                _ => 31,
349            };
350            if date.day < 1 || date.day > max_days_in_month {
351                return Err(DatetimeParseError {});
352            }
353
354            Some(date)
355        };
356
357        // Next parse the "partial-time" if available
358        let next = chars.clone().next();
359        let partial_time = if full_date.is_some()
360            && (next == Some('T') || next == Some('t') || next == Some(' '))
361        {
362            chars.next();
363            true
364        } else {
365            full_date.is_none()
366        };
367
368        let time = if partial_time {
369            let h1 = digit(&mut chars)?;
370            let h2 = digit(&mut chars)?;
371            match chars.next() {
372                Some(':') => {}
373                _ => return Err(DatetimeParseError {}),
374            }
375            let m1 = digit(&mut chars)?;
376            let m2 = digit(&mut chars)?;
377            match chars.next() {
378                Some(':') => {}
379                _ => return Err(DatetimeParseError {}),
380            }
381            let s1 = digit(&mut chars)?;
382            let s2 = digit(&mut chars)?;
383
384            let mut nanosecond = 0;
385            if chars.clone().next() == Some('.') {
386                chars.next();
387                let whole = chars.as_str();
388
389                let mut end = whole.len();
390                for (i, byte) in whole.bytes().enumerate() {
391                    #[allow(clippy::single_match_else)]
392                    match byte {
393                        b'0'..=b'9' => {
394                            if i < 9 {
395                                let p = 10_u32.pow(8 - i as u32);
396                                nanosecond += p * u32::from(byte - b'0');
397                            }
398                        }
399                        _ => {
400                            end = i;
401                            break;
402                        }
403                    }
404                }
405                if end == 0 {
406                    return Err(DatetimeParseError {});
407                }
408                chars = whole[end..].chars();
409            }
410
411            let time = Time {
412                hour: h1 * 10 + h2,
413                minute: m1 * 10 + m2,
414                second: s1 * 10 + s2,
415                nanosecond,
416            };
417
418            if time.hour > 24 {
419                return Err(DatetimeParseError {});
420            }
421            if time.minute > 59 {
422                return Err(DatetimeParseError {});
423            }
424            // 00-58, 00-59, 00-60 based on leap second rules
425            if time.second > 60 {
426                return Err(DatetimeParseError {});
427            }
428            if time.nanosecond > 999_999_999 {
429                return Err(DatetimeParseError {});
430            }
431
432            Some(time)
433        } else {
434            offset_allowed = false;
435            None
436        };
437
438        // And finally, parse the offset
439        let offset = if offset_allowed {
440            let next = chars.clone().next();
441            if next == Some('Z') || next == Some('z') {
442                chars.next();
443                Some(Offset::Z)
444            } else if next.is_none() {
445                None
446            } else {
447                let sign = match next {
448                    Some('+') => 1,
449                    Some('-') => -1,
450                    _ => return Err(DatetimeParseError {}),
451                };
452                chars.next();
453                let h1 = digit(&mut chars)? as i16;
454                let h2 = digit(&mut chars)? as i16;
455                match chars.next() {
456                    Some(':') => {}
457                    _ => return Err(DatetimeParseError {}),
458                }
459                let m1 = digit(&mut chars)? as i16;
460                let m2 = digit(&mut chars)? as i16;
461
462                let hours = h1 * 10 + h2;
463                let minutes = m1 * 10 + m2;
464
465                let total_minutes = sign * (hours * 60 + minutes);
466
467                if !((-24 * 60)..=(24 * 60)).contains(&total_minutes) {
468                    return Err(DatetimeParseError {});
469                }
470
471                Some(Offset::Custom {
472                    minutes: total_minutes,
473                })
474            }
475        } else {
476            None
477        };
478
479        // Return an error if we didn't hit eof, otherwise return our parsed
480        // date
481        if chars.next().is_some() {
482            return Err(DatetimeParseError {});
483        }
484
485        Ok(Datetime {
486            date: full_date,
487            time,
488            offset,
489        })
490    }
491}
492
493fn digit(chars: &mut str::Chars<'_>) -> Result<u8, DatetimeParseError> {
494    match chars.next() {
495        Some(c) if c.is_ascii_digit() => Ok(c as u8 - b'0'),
496        _ => Err(DatetimeParseError {}),
497    }
498}
499
500#[cfg(feature = "serde")]
501impl ser::Serialize for Datetime {
502    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
503    where
504        S: ser::Serializer,
505    {
506        use serde::ser::SerializeStruct;
507
508        let mut s = serializer.serialize_struct(NAME, 1)?;
509        s.serialize_field(FIELD, &self.to_string())?;
510        s.end()
511    }
512}
513
514#[cfg(feature = "serde")]
515impl ser::Serialize for Date {
516    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
517    where
518        S: ser::Serializer,
519    {
520        Datetime::from(*self).serialize(serializer)
521    }
522}
523
524#[cfg(feature = "serde")]
525impl ser::Serialize for Time {
526    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
527    where
528        S: ser::Serializer,
529    {
530        Datetime::from(*self).serialize(serializer)
531    }
532}
533
534#[cfg(feature = "serde")]
535impl<'de> de::Deserialize<'de> for Datetime {
536    fn deserialize<D>(deserializer: D) -> Result<Datetime, D::Error>
537    where
538        D: de::Deserializer<'de>,
539    {
540        struct DatetimeVisitor;
541
542        impl<'de> de::Visitor<'de> for DatetimeVisitor {
543            type Value = Datetime;
544
545            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
546                formatter.write_str("a TOML datetime")
547            }
548
549            fn visit_map<V>(self, mut visitor: V) -> Result<Datetime, V::Error>
550            where
551                V: de::MapAccess<'de>,
552            {
553                let value = visitor.next_key::<DatetimeKey>()?;
554                if value.is_none() {
555                    return Err(de::Error::custom("datetime key not found"));
556                }
557                let v: DatetimeFromString = visitor.next_value()?;
558                Ok(v.value)
559            }
560        }
561
562        static FIELDS: [&str; 1] = [FIELD];
563        deserializer.deserialize_struct(NAME, &FIELDS, DatetimeVisitor)
564    }
565}
566
567#[cfg(feature = "serde")]
568impl<'de> de::Deserialize<'de> for Date {
569    fn deserialize<D>(deserializer: D) -> Result<Date, D::Error>
570    where
571        D: de::Deserializer<'de>,
572    {
573        match Datetime::deserialize(deserializer)? {
574            Datetime {
575                date: Some(date),
576                time: None,
577                offset: None,
578            } => Ok(date),
579            datetime => Err(de::Error::invalid_type(
580                de::Unexpected::Other(datetime.type_name()),
581                &Self::type_name(),
582            )),
583        }
584    }
585}
586
587#[cfg(feature = "serde")]
588impl<'de> de::Deserialize<'de> for Time {
589    fn deserialize<D>(deserializer: D) -> Result<Time, D::Error>
590    where
591        D: de::Deserializer<'de>,
592    {
593        match Datetime::deserialize(deserializer)? {
594            Datetime {
595                date: None,
596                time: Some(time),
597                offset: None,
598            } => Ok(time),
599            datetime => Err(de::Error::invalid_type(
600                de::Unexpected::Other(datetime.type_name()),
601                &Self::type_name(),
602            )),
603        }
604    }
605}
606
607#[cfg(feature = "serde")]
608struct DatetimeKey;
609
610#[cfg(feature = "serde")]
611impl<'de> de::Deserialize<'de> for DatetimeKey {
612    fn deserialize<D>(deserializer: D) -> Result<DatetimeKey, D::Error>
613    where
614        D: de::Deserializer<'de>,
615    {
616        struct FieldVisitor;
617
618        impl<'de> de::Visitor<'de> for FieldVisitor {
619            type Value = ();
620
621            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
622                formatter.write_str("a valid datetime field")
623            }
624
625            fn visit_str<E>(self, s: &str) -> Result<(), E>
626            where
627                E: de::Error,
628            {
629                if s == FIELD {
630                    Ok(())
631                } else {
632                    Err(de::Error::custom("expected field with custom name"))
633                }
634            }
635        }
636
637        deserializer.deserialize_identifier(FieldVisitor)?;
638        Ok(DatetimeKey)
639    }
640}
641
642#[doc(hidden)]
643#[cfg(feature = "serde")]
644pub struct DatetimeFromString {
645    pub value: Datetime,
646}
647
648#[cfg(feature = "serde")]
649impl<'de> de::Deserialize<'de> for DatetimeFromString {
650    fn deserialize<D>(deserializer: D) -> Result<DatetimeFromString, D::Error>
651    where
652        D: de::Deserializer<'de>,
653    {
654        struct Visitor;
655
656        impl<'de> de::Visitor<'de> for Visitor {
657            type Value = DatetimeFromString;
658
659            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
660                formatter.write_str("string containing a datetime")
661            }
662
663            fn visit_str<E>(self, s: &str) -> Result<DatetimeFromString, E>
664            where
665                E: de::Error,
666            {
667                match s.parse() {
668                    Ok(date) => Ok(DatetimeFromString { value: date }),
669                    Err(e) => Err(de::Error::custom(e)),
670                }
671            }
672        }
673
674        deserializer.deserialize_str(Visitor)
675    }
676}
677
678impl fmt::Display for DatetimeParseError {
679    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
680        "failed to parse datetime".fmt(f)
681    }
682}
683
684impl error::Error for DatetimeParseError {}