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: &Date) = self.date {
241 write!(f, "{}", date)?;
242 }
243 if let Some(ref time: &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: &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: String = 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: i16 } => {
278 let mut sign: char = '+';
279 if minutes < 0 {
280 minutes *= -1;
281 sign = '-';
282 }
283 let hours: i16 = minutes / 60;
284 let minutes: i16 = 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: char) 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: ::SerializeStruct = serializer.serialize_struct(NAME, len: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: Date),
576 time: None,
577 offset: None,
578 } => Ok(date),
579 datetime: Datetime => Err(de::Error::invalid_type(
580 unexp: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: Time),
597 offset: None,
598 } => Ok(time),
599 datetime: Datetime => Err(de::Error::invalid_type(
600 unexp: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 {}
685