time/parsing/combinator/rfc/
iso8601.rs

1//! Rules defined in [ISO 8601].
2//!
3//! [ISO 8601]: https://www.iso.org/iso-8601-date-and-time-format.html
4
5use core::num::NonZero;
6
7use num_conv::prelude::*;
8
9use crate::parsing::ParsedItem;
10use crate::parsing::combinator::{ExactlyNDigits, Sign, any_digit, sign};
11use crate::{Month, Weekday};
12
13/// What kind of format is being parsed. This is used to ensure each part of the format (date, time,
14/// offset) is the same kind.
15#[derive(Debug, Clone, Copy)]
16pub(crate) enum ExtendedKind {
17    /// The basic format.
18    Basic,
19    /// The extended format.
20    Extended,
21    /// ¯\_(ツ)_/¯
22    Unknown,
23}
24
25impl ExtendedKind {
26    /// Is it possible that the format is extended?
27    #[inline]
28    pub(crate) const fn maybe_extended(self) -> bool {
29        matches!(self, Self::Extended | Self::Unknown)
30    }
31
32    /// Is the format known for certain to be extended?
33    #[inline]
34    pub(crate) const fn is_extended(self) -> bool {
35        matches!(self, Self::Extended)
36    }
37
38    /// If the kind is `Unknown`, make it `Basic`. Otherwise, do nothing. Returns `Some` if and only
39    /// if the kind is now `Basic`.
40    #[inline]
41    pub(crate) const fn coerce_basic(&mut self) -> Option<()> {
42        match self {
43            Self::Basic => Some(()),
44            Self::Extended => None,
45            Self::Unknown => {
46                *self = Self::Basic;
47                Some(())
48            }
49        }
50    }
51
52    /// If the kind is `Unknown`, make it `Extended`. Otherwise, do nothing. Returns `Some` if and
53    /// only if the kind is now `Extended`.
54    #[inline]
55    pub(crate) const fn coerce_extended(&mut self) -> Option<()> {
56        match self {
57            Self::Basic => None,
58            Self::Extended => Some(()),
59            Self::Unknown => {
60                *self = Self::Extended;
61                Some(())
62            }
63        }
64    }
65}
66
67/// Parse a possibly expanded year.
68#[inline]
69pub(crate) fn year(input: &[u8]) -> Option<ParsedItem<'_, i32>> {
70    Some(match sign(input) {
71        Some(ParsedItem(input, sign)) => ExactlyNDigits::<6>::parse(input)?.map(|val| {
72            let val = val.cast_signed();
73            match sign {
74                Sign::Negative => -val,
75                Sign::Positive => val,
76            }
77        }),
78        None => ExactlyNDigits::<4>::parse(input)?.map(|val| val.cast_signed().extend()),
79    })
80}
81
82/// Parse a month.
83#[inline]
84pub(crate) fn month(input: &[u8]) -> Option<ParsedItem<'_, Month>> {
85    match input {
86        [b'0', b'1', remaining @ ..] => Some(ParsedItem(remaining, Month::January)),
87        [b'0', b'2', remaining @ ..] => Some(ParsedItem(remaining, Month::February)),
88        [b'0', b'3', remaining @ ..] => Some(ParsedItem(remaining, Month::March)),
89        [b'0', b'4', remaining @ ..] => Some(ParsedItem(remaining, Month::April)),
90        [b'0', b'5', remaining @ ..] => Some(ParsedItem(remaining, Month::May)),
91        [b'0', b'6', remaining @ ..] => Some(ParsedItem(remaining, Month::June)),
92        [b'0', b'7', remaining @ ..] => Some(ParsedItem(remaining, Month::July)),
93        [b'0', b'8', remaining @ ..] => Some(ParsedItem(remaining, Month::August)),
94        [b'0', b'9', remaining @ ..] => Some(ParsedItem(remaining, Month::September)),
95        [b'1', b'0', remaining @ ..] => Some(ParsedItem(remaining, Month::October)),
96        [b'1', b'1', remaining @ ..] => Some(ParsedItem(remaining, Month::November)),
97        [b'1', b'2', remaining @ ..] => Some(ParsedItem(remaining, Month::December)),
98        _ => None,
99    }
100}
101
102/// Parse a week number.
103#[inline]
104pub(crate) fn week(input: &[u8]) -> Option<ParsedItem<'_, NonZero<u8>>> {
105    ExactlyNDigits::<2>::parse(input).and_then(|parsed| parsed.flat_map(NonZero::new))
106}
107
108/// Parse a day of the month.
109#[inline]
110pub(crate) fn day(input: &[u8]) -> Option<ParsedItem<'_, NonZero<u8>>> {
111    ExactlyNDigits::<2>::parse(input).and_then(|parsed| parsed.flat_map(NonZero::new))
112}
113
114/// Parse a day of the week.
115#[inline]
116pub(crate) fn dayk(input: &[u8]) -> Option<ParsedItem<'_, Weekday>> {
117    match input {
118        [b'1', remaining @ ..] => Some(ParsedItem(remaining, Weekday::Monday)),
119        [b'2', remaining @ ..] => Some(ParsedItem(remaining, Weekday::Tuesday)),
120        [b'3', remaining @ ..] => Some(ParsedItem(remaining, Weekday::Wednesday)),
121        [b'4', remaining @ ..] => Some(ParsedItem(remaining, Weekday::Thursday)),
122        [b'5', remaining @ ..] => Some(ParsedItem(remaining, Weekday::Friday)),
123        [b'6', remaining @ ..] => Some(ParsedItem(remaining, Weekday::Saturday)),
124        [b'7', remaining @ ..] => Some(ParsedItem(remaining, Weekday::Sunday)),
125        _ => None,
126    }
127}
128
129/// Parse a day of the year.
130#[inline]
131pub(crate) fn dayo(input: &[u8]) -> Option<ParsedItem<'_, NonZero<u16>>> {
132    ExactlyNDigits::<3>::parse(input).and_then(|parsed| parsed.flat_map(NonZero::new))
133}
134
135/// Parse the hour.
136#[inline]
137pub(crate) const fn hour(input: &[u8]) -> Option<ParsedItem<'_, u8>> {
138    ExactlyNDigits::<2>::parse(input)
139}
140
141/// Parse the minute.
142#[inline]
143pub(crate) const fn min(input: &[u8]) -> Option<ParsedItem<'_, u8>> {
144    ExactlyNDigits::<2>::parse(input)
145}
146
147/// Parse a floating point number as its integer and optional fractional parts.
148///
149/// The number must have two digits before the decimal point. If a decimal point is present, at
150/// least one digit must follow.
151///
152/// The return type is a tuple of the integer part and optional fraction part.
153#[inline]
154pub(crate) fn float(input: &[u8]) -> Option<ParsedItem<'_, (u8, Option<f64>)>> {
155    // Two digits before the decimal.
156    let ParsedItem(input, integer_part) = match input {
157        [
158            first_digit @ b'0'..=b'9',
159            second_digit @ b'0'..=b'9',
160            input @ ..,
161        ] => ParsedItem(input, (first_digit - b'0') * 10 + (second_digit - b'0')),
162        _ => return None,
163    };
164
165    if let Some(ParsedItem(input, ())) = decimal_sign(input) {
166        // Mandatory post-decimal digit.
167        let ParsedItem(mut input, mut fractional_part) =
168            any_digit(input)?.map(|digit| ((digit - b'0') as f64) / 10.);
169
170        let mut divisor = 10.;
171        // Any number of subsequent digits.
172        while let Some(ParsedItem(new_input, digit)) = any_digit(input) {
173            input = new_input;
174            divisor *= 10.;
175            fractional_part += (digit - b'0') as f64 / divisor;
176        }
177
178        Some(ParsedItem(input, (integer_part, Some(fractional_part))))
179    } else {
180        Some(ParsedItem(input, (integer_part, None)))
181    }
182}
183
184/// Parse a "decimal sign", which is either a comma or a period.
185#[inline]
186fn decimal_sign(input: &[u8]) -> Option<ParsedItem<'_, ()>> {
187    match input {
188        [b'.' | b',', remaining @ ..] => Some(ParsedItem(remaining, ())),
189        _ => None,
190    }
191}