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