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::{NonZeroU16, NonZeroU8};
6
7use num_conv::prelude::*;
8
9use crate::parsing::combinator::{any_digit, ascii_char, exactly_n_digits, first_match, sign};
10use crate::parsing::ParsedItem;
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    pub(crate) const fn maybe_extended(self) -> bool {
28        matches!(self, Self::Extended | Self::Unknown)
29    }
30
31    /// Is the format known for certain to be extended?
32    pub(crate) const fn is_extended(self) -> bool {
33        matches!(self, Self::Extended)
34    }
35
36    /// If the kind is `Unknown`, make it `Basic`. Otherwise, do nothing. Returns `Some` if and only
37    /// if the kind is now `Basic`.
38    pub(crate) fn coerce_basic(&mut self) -> Option<()> {
39        match self {
40            Self::Basic => Some(()),
41            Self::Extended => None,
42            Self::Unknown => {
43                *self = Self::Basic;
44                Some(())
45            }
46        }
47    }
48
49    /// If the kind is `Unknown`, make it `Extended`. Otherwise, do nothing. Returns `Some` if and
50    /// only if the kind is now `Extended`.
51    pub(crate) fn coerce_extended(&mut self) -> Option<()> {
52        match self {
53            Self::Basic => None,
54            Self::Extended => Some(()),
55            Self::Unknown => {
56                *self = Self::Extended;
57                Some(())
58            }
59        }
60    }
61}
62
63/// Parse a possibly expanded year.
64pub(crate) fn year(input: &[u8]) -> Option<ParsedItem<'_, i32>> {
65    Some(match sign(input) {
66        Some(ParsedItem(input, sign)) => exactly_n_digits::<6, u32>(input)?.map(|val| {
67            let val = val.cast_signed();
68            if sign == b'-' {
69                -val
70            } else {
71                val
72            }
73        }),
74        None => exactly_n_digits::<4, u32>(input)?.map(|val| val.cast_signed()),
75    })
76}
77
78/// Parse a month.
79pub(crate) fn month(input: &[u8]) -> Option<ParsedItem<'_, Month>> {
80    first_match(
81        [
82            (b"01".as_slice(), Month::January),
83            (b"02".as_slice(), Month::February),
84            (b"03".as_slice(), Month::March),
85            (b"04".as_slice(), Month::April),
86            (b"05".as_slice(), Month::May),
87            (b"06".as_slice(), Month::June),
88            (b"07".as_slice(), Month::July),
89            (b"08".as_slice(), Month::August),
90            (b"09".as_slice(), Month::September),
91            (b"10".as_slice(), Month::October),
92            (b"11".as_slice(), Month::November),
93            (b"12".as_slice(), Month::December),
94        ],
95        true,
96    )(input)
97}
98
99/// Parse a week number.
100pub(crate) fn week(input: &[u8]) -> Option<ParsedItem<'_, NonZeroU8>> {
101    exactly_n_digits::<2, _>(input)
102}
103
104/// Parse a day of the month.
105pub(crate) fn day(input: &[u8]) -> Option<ParsedItem<'_, NonZeroU8>> {
106    exactly_n_digits::<2, _>(input)
107}
108
109/// Parse a day of the week.
110pub(crate) fn dayk(input: &[u8]) -> Option<ParsedItem<'_, Weekday>> {
111    first_match(
112        [
113            (b"1".as_slice(), Weekday::Monday),
114            (b"2".as_slice(), Weekday::Tuesday),
115            (b"3".as_slice(), Weekday::Wednesday),
116            (b"4".as_slice(), Weekday::Thursday),
117            (b"5".as_slice(), Weekday::Friday),
118            (b"6".as_slice(), Weekday::Saturday),
119            (b"7".as_slice(), Weekday::Sunday),
120        ],
121        true,
122    )(input)
123}
124
125/// Parse a day of the year.
126pub(crate) fn dayo(input: &[u8]) -> Option<ParsedItem<'_, NonZeroU16>> {
127    exactly_n_digits::<3, _>(input)
128}
129
130/// Parse the hour.
131pub(crate) fn hour(input: &[u8]) -> Option<ParsedItem<'_, u8>> {
132    exactly_n_digits::<2, _>(input)
133}
134
135/// Parse the minute.
136pub(crate) fn min(input: &[u8]) -> Option<ParsedItem<'_, u8>> {
137    exactly_n_digits::<2, _>(input)
138}
139
140/// Parse a floating point number as its integer and optional fractional parts.
141///
142/// The number must have two digits before the decimal point. If a decimal point is present, at
143/// least one digit must follow.
144///
145/// The return type is a tuple of the integer part and optional fraction part.
146pub(crate) fn float(input: &[u8]) -> Option<ParsedItem<'_, (u8, Option<f64>)>> {
147    // Two digits before the decimal.
148    let ParsedItem(input, integer_part) = match input {
149        [first_digit @ b'0'..=b'9', second_digit @ b'0'..=b'9', input @ ..] => {
150            ParsedItem(input, (first_digit - b'0') * 10 + (second_digit - b'0'))
151        }
152        _ => return None,
153    };
154
155    if let Some(ParsedItem(input, ())) = decimal_sign(input) {
156        // Mandatory post-decimal digit.
157        let ParsedItem(mut input, mut fractional_part) =
158            any_digit(input)?.map(|digit| ((digit - b'0') as f64) / 10.);
159
160        let mut divisor = 10.;
161        // Any number of subsequent digits.
162        while let Some(ParsedItem(new_input, digit)) = any_digit(input) {
163            input = new_input;
164            divisor *= 10.;
165            fractional_part += (digit - b'0') as f64 / divisor;
166        }
167
168        Some(ParsedItem(input, (integer_part, Some(fractional_part))))
169    } else {
170        Some(ParsedItem(input, (integer_part, None)))
171    }
172}
173
174/// Parse a "decimal sign", which is either a comma or a period.
175fn decimal_sign(input: &[u8]) -> Option<ParsedItem<'_, ()>> {
176    ascii_char::<b'.'>(input).or_else(|| ascii_char::<b','>(input))
177}