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)]
97pub struct DatetimeParseError {
98    _private: (),
99}
100
101// Currently serde itself doesn't have a datetime type, so we map our `Datetime`
102// to a special value in the serde data model. Namely one with these special
103// fields/struct names.
104//
105// In general the TOML encoder/decoder will catch this and not literally emit
106// these strings but rather emit datetimes as they're intended.
107#[doc(hidden)]
108#[cfg(feature = "serde")]
109pub const FIELD: &str = "$__toml_private_datetime";
110#[doc(hidden)]
111#[cfg(feature = "serde")]
112pub const NAME: &str = "$__toml_private_Datetime";
113
114/// A parsed TOML date value
115///
116/// May be part of a [`Datetime`]. Alone, `Date` corresponds to a [Local Date].
117/// From the TOML v1.0.0 spec:
118///
119/// > If you include only the date portion of an RFC 3339 formatted date-time,
120/// > it will represent that entire day without any relation to an offset or
121/// > timezone.
122/// >
123/// > ```toml
124/// > ld1 = 1979-05-27
125/// > ```
126///
127/// [Local Date]: https://toml.io/en/v1.0.0#local-date
128#[derive(PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Debug)]
129pub struct Date {
130    /// Year: four digits
131    pub year: u16,
132    /// Month: 1 to 12
133    pub month: u8,
134    /// Day: 1 to {28, 29, 30, 31} (based on month/year)
135    pub day: u8,
136}
137
138/// A parsed TOML time value
139///
140/// May be part of a [`Datetime`]. Alone, `Time` corresponds to a [Local Time].
141/// From the TOML v1.0.0 spec:
142///
143/// > If you include only the time portion of an RFC 3339 formatted date-time,
144/// > it will represent that time of day without any relation to a specific
145/// > day or any offset or timezone.
146/// >
147/// > ```toml
148/// > lt1 = 07:32:00
149/// > lt2 = 00:32:00.999999
150/// > ```
151/// >
152/// > Millisecond precision is required. Further precision of fractional
153/// > seconds is implementation-specific. If the value contains greater
154/// > precision than the implementation can support, the additional precision
155/// > must be truncated, not rounded.
156///
157/// [Local Time]: https://toml.io/en/v1.0.0#local-time
158#[derive(PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Debug)]
159pub struct Time {
160    /// Hour: 0 to 23
161    pub hour: u8,
162    /// Minute: 0 to 59
163    pub minute: u8,
164    /// Second: 0 to {58, 59, 60} (based on leap second rules)
165    pub second: u8,
166    /// Nanosecond: 0 to 999_999_999
167    pub nanosecond: u32,
168}
169
170/// A parsed TOML time offset
171///
172#[derive(PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Debug)]
173pub enum Offset {
174    /// > A suffix which, when applied to a time, denotes a UTC offset of 00:00;
175    /// > often spoken "Zulu" from the ICAO phonetic alphabet representation of
176    /// > the letter "Z". --- [RFC 3339 section 2]
177    ///
178    /// [RFC 3339 section 2]: https://datatracker.ietf.org/doc/html/rfc3339#section-2
179    Z,
180
181    /// Offset between local time and UTC
182    Custom {
183        /// Hours: -12 to +12
184        hours: i8,
185
186        /// Minutes: 0 to 59
187        minutes: u8,
188    },
189}
190
191impl From<Date> for Datetime {
192    fn from(other: Date) -> Self {
193        Datetime {
194            date: Some(other),
195            time: None,
196            offset: None,
197        }
198    }
199}
200
201impl From<Time> for Datetime {
202    fn from(other: Time) -> Self {
203        Datetime {
204            date: None,
205            time: Some(other),
206            offset: None,
207        }
208    }
209}
210
211impl fmt::Display for Datetime {
212    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
213        if let Some(ref date) = self.date {
214            write!(f, "{}", date)?;
215        }
216        if let Some(ref time) = self.time {
217            if self.date.is_some() {
218                write!(f, "T")?;
219            }
220            write!(f, "{}", time)?;
221        }
222        if let Some(ref offset) = self.offset {
223            write!(f, "{}", offset)?;
224        }
225        Ok(())
226    }
227}
228
229impl fmt::Display for Date {
230    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
231        write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
232    }
233}
234
235impl fmt::Display for Time {
236    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
237        write!(f, "{:02}:{:02}:{:02}", self.hour, self.minute, self.second)?;
238        if self.nanosecond != 0 {
239            let s = format!("{:09}", self.nanosecond);
240            write!(f, ".{}", s.trim_end_matches('0'))?;
241        }
242        Ok(())
243    }
244}
245
246impl fmt::Display for Offset {
247    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
248        match *self {
249            Offset::Z => write!(f, "Z"),
250            Offset::Custom { hours, minutes } => write!(f, "{:+03}:{:02}", hours, minutes),
251        }
252    }
253}
254
255impl FromStr for Datetime {
256    type Err = DatetimeParseError;
257
258    fn from_str(date: &str) -> Result<Datetime, DatetimeParseError> {
259        // Accepted formats:
260        //
261        // 0000-00-00T00:00:00.00Z
262        // 0000-00-00T00:00:00.00
263        // 0000-00-00
264        // 00:00:00.00
265        if date.len() < 3 {
266            return Err(DatetimeParseError { _private: () });
267        }
268        let mut offset_allowed = true;
269        let mut chars = date.chars();
270
271        // First up, parse the full date if we can
272        let full_date = if chars.clone().nth(2) == Some(':') {
273            offset_allowed = false;
274            None
275        } else {
276            let y1 = u16::from(digit(&mut chars)?);
277            let y2 = u16::from(digit(&mut chars)?);
278            let y3 = u16::from(digit(&mut chars)?);
279            let y4 = u16::from(digit(&mut chars)?);
280
281            match chars.next() {
282                Some('-') => {}
283                _ => return Err(DatetimeParseError { _private: () }),
284            }
285
286            let m1 = digit(&mut chars)?;
287            let m2 = digit(&mut chars)?;
288
289            match chars.next() {
290                Some('-') => {}
291                _ => return Err(DatetimeParseError { _private: () }),
292            }
293
294            let d1 = digit(&mut chars)?;
295            let d2 = digit(&mut chars)?;
296
297            let date = Date {
298                year: y1 * 1000 + y2 * 100 + y3 * 10 + y4,
299                month: m1 * 10 + m2,
300                day: d1 * 10 + d2,
301            };
302
303            if date.month < 1 || date.month > 12 {
304                return Err(DatetimeParseError { _private: () });
305            }
306            if date.day < 1 || date.day > 31 {
307                return Err(DatetimeParseError { _private: () });
308            }
309
310            Some(date)
311        };
312
313        // Next parse the "partial-time" if available
314        let next = chars.clone().next();
315        let partial_time = if full_date.is_some()
316            && (next == Some('T') || next == Some('t') || next == Some(' '))
317        {
318            chars.next();
319            true
320        } else {
321            full_date.is_none()
322        };
323
324        let time = if partial_time {
325            let h1 = digit(&mut chars)?;
326            let h2 = digit(&mut chars)?;
327            match chars.next() {
328                Some(':') => {}
329                _ => return Err(DatetimeParseError { _private: () }),
330            }
331            let m1 = digit(&mut chars)?;
332            let m2 = digit(&mut chars)?;
333            match chars.next() {
334                Some(':') => {}
335                _ => return Err(DatetimeParseError { _private: () }),
336            }
337            let s1 = digit(&mut chars)?;
338            let s2 = digit(&mut chars)?;
339
340            let mut nanosecond = 0;
341            if chars.clone().next() == Some('.') {
342                chars.next();
343                let whole = chars.as_str();
344
345                let mut end = whole.len();
346                for (i, byte) in whole.bytes().enumerate() {
347                    match byte {
348                        b'0'..=b'9' => {
349                            if i < 9 {
350                                let p = 10_u32.pow(8 - i as u32);
351                                nanosecond += p * u32::from(byte - b'0');
352                            }
353                        }
354                        _ => {
355                            end = i;
356                            break;
357                        }
358                    }
359                }
360                if end == 0 {
361                    return Err(DatetimeParseError { _private: () });
362                }
363                chars = whole[end..].chars();
364            }
365
366            let time = Time {
367                hour: h1 * 10 + h2,
368                minute: m1 * 10 + m2,
369                second: s1 * 10 + s2,
370                nanosecond,
371            };
372
373            if time.hour > 24 {
374                return Err(DatetimeParseError { _private: () });
375            }
376            if time.minute > 59 {
377                return Err(DatetimeParseError { _private: () });
378            }
379            if time.second > 59 {
380                return Err(DatetimeParseError { _private: () });
381            }
382            if time.nanosecond > 999_999_999 {
383                return Err(DatetimeParseError { _private: () });
384            }
385
386            Some(time)
387        } else {
388            offset_allowed = false;
389            None
390        };
391
392        // And finally, parse the offset
393        let offset = if offset_allowed {
394            let next = chars.clone().next();
395            if next == Some('Z') || next == Some('z') {
396                chars.next();
397                Some(Offset::Z)
398            } else if next.is_none() {
399                None
400            } else {
401                let sign = match next {
402                    Some('+') => 1,
403                    Some('-') => -1,
404                    _ => return Err(DatetimeParseError { _private: () }),
405                };
406                chars.next();
407                let h1 = digit(&mut chars)? as i8;
408                let h2 = digit(&mut chars)? as i8;
409                match chars.next() {
410                    Some(':') => {}
411                    _ => return Err(DatetimeParseError { _private: () }),
412                }
413                let m1 = digit(&mut chars)?;
414                let m2 = digit(&mut chars)?;
415
416                Some(Offset::Custom {
417                    hours: sign * (h1 * 10 + h2),
418                    minutes: m1 * 10 + m2,
419                })
420            }
421        } else {
422            None
423        };
424
425        // Return an error if we didn't hit eof, otherwise return our parsed
426        // date
427        if chars.next().is_some() {
428            return Err(DatetimeParseError { _private: () });
429        }
430
431        Ok(Datetime {
432            date: full_date,
433            time,
434            offset,
435        })
436    }
437}
438
439fn digit(chars: &mut str::Chars<'_>) -> Result<u8, DatetimeParseError> {
440    match chars.next() {
441        Some(c) if ('0'..='9').contains(&c) => Ok(c as u8 - b'0'),
442        _ => Err(DatetimeParseError { _private: () }),
443    }
444}
445
446#[cfg(feature = "serde")]
447impl ser::Serialize for Datetime {
448    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
449    where
450        S: ser::Serializer,
451    {
452        use serde::ser::SerializeStruct;
453
454        let mut s = serializer.serialize_struct(NAME, 1)?;
455        s.serialize_field(FIELD, &self.to_string())?;
456        s.end()
457    }
458}
459
460#[cfg(feature = "serde")]
461impl<'de> de::Deserialize<'de> for Datetime {
462    fn deserialize<D>(deserializer: D) -> Result<Datetime, D::Error>
463    where
464        D: de::Deserializer<'de>,
465    {
466        struct DatetimeVisitor;
467
468        impl<'de> de::Visitor<'de> for DatetimeVisitor {
469            type Value = Datetime;
470
471            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
472                formatter.write_str("a TOML datetime")
473            }
474
475            fn visit_map<V>(self, mut visitor: V) -> Result<Datetime, V::Error>
476            where
477                V: de::MapAccess<'de>,
478            {
479                let value = visitor.next_key::<DatetimeKey>()?;
480                if value.is_none() {
481                    return Err(de::Error::custom("datetime key not found"));
482                }
483                let v: DatetimeFromString = visitor.next_value()?;
484                Ok(v.value)
485            }
486        }
487
488        static FIELDS: [&str; 1] = [FIELD];
489        deserializer.deserialize_struct(NAME, &FIELDS, DatetimeVisitor)
490    }
491}
492
493#[cfg(feature = "serde")]
494struct DatetimeKey;
495
496#[cfg(feature = "serde")]
497impl<'de> de::Deserialize<'de> for DatetimeKey {
498    fn deserialize<D>(deserializer: D) -> Result<DatetimeKey, D::Error>
499    where
500        D: de::Deserializer<'de>,
501    {
502        struct FieldVisitor;
503
504        impl<'de> de::Visitor<'de> for FieldVisitor {
505            type Value = ();
506
507            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
508                formatter.write_str("a valid datetime field")
509            }
510
511            fn visit_str<E>(self, s: &str) -> Result<(), E>
512            where
513                E: de::Error,
514            {
515                if s == FIELD {
516                    Ok(())
517                } else {
518                    Err(de::Error::custom("expected field with custom name"))
519                }
520            }
521        }
522
523        deserializer.deserialize_identifier(FieldVisitor)?;
524        Ok(DatetimeKey)
525    }
526}
527
528#[doc(hidden)]
529#[cfg(feature = "serde")]
530pub struct DatetimeFromString {
531    pub value: Datetime,
532}
533
534#[cfg(feature = "serde")]
535impl<'de> de::Deserialize<'de> for DatetimeFromString {
536    fn deserialize<D>(deserializer: D) -> Result<DatetimeFromString, D::Error>
537    where
538        D: de::Deserializer<'de>,
539    {
540        struct Visitor;
541
542        impl<'de> de::Visitor<'de> for Visitor {
543            type Value = DatetimeFromString;
544
545            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
546                formatter.write_str("string containing a datetime")
547            }
548
549            fn visit_str<E>(self, s: &str) -> Result<DatetimeFromString, E>
550            where
551                E: de::Error,
552            {
553                match s.parse() {
554                    Ok(date) => Ok(DatetimeFromString { value: date }),
555                    Err(e) => Err(de::Error::custom(e)),
556                }
557            }
558        }
559
560        deserializer.deserialize_str(Visitor)
561    }
562}
563
564impl fmt::Display for DatetimeParseError {
565    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
566        "failed to parse datetime".fmt(f)
567    }
568}
569
570impl error::Error for DatetimeParseError {}