1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
//! Rules defined in [ISO 8601].
//!
//! [ISO 8601]: https://www.iso.org/iso-8601-date-and-time-format.html

use core::num::{NonZeroU16, NonZeroU8};

use num_conv::prelude::*;

use crate::parsing::combinator::{any_digit, ascii_char, exactly_n_digits, first_match, sign};
use crate::parsing::ParsedItem;
use crate::{Month, Weekday};

/// What kind of format is being parsed. This is used to ensure each part of the format (date, time,
/// offset) is the same kind.
#[derive(Debug, Clone, Copy)]
pub(crate) enum ExtendedKind {
    /// The basic format.
    Basic,
    /// The extended format.
    Extended,
    /// ¯\_(ツ)_/¯
    Unknown,
}

impl ExtendedKind {
    /// Is it possible that the format is extended?
    pub(crate) const fn maybe_extended(self) -> bool {
        matches!(self, Self::Extended | Self::Unknown)
    }

    /// Is the format known for certain to be extended?
    pub(crate) const fn is_extended(self) -> bool {
        matches!(self, Self::Extended)
    }

    /// If the kind is `Unknown`, make it `Basic`. Otherwise, do nothing. Returns `Some` if and only
    /// if the kind is now `Basic`.
    pub(crate) fn coerce_basic(&mut self) -> Option<()> {
        match self {
            Self::Basic => Some(()),
            Self::Extended => None,
            Self::Unknown => {
                *self = Self::Basic;
                Some(())
            }
        }
    }

    /// If the kind is `Unknown`, make it `Extended`. Otherwise, do nothing. Returns `Some` if and
    /// only if the kind is now `Extended`.
    pub(crate) fn coerce_extended(&mut self) -> Option<()> {
        match self {
            Self::Basic => None,
            Self::Extended => Some(()),
            Self::Unknown => {
                *self = Self::Extended;
                Some(())
            }
        }
    }
}

/// Parse a possibly expanded year.
pub(crate) fn year(input: &[u8]) -> Option<ParsedItem<'_, i32>> {
    Some(match sign(input) {
        Some(ParsedItem(input, sign)) => exactly_n_digits::<6, u32>(input)?.map(|val| {
            let val = val.cast_signed();
            if sign == b'-' { -val } else { val }
        }),
        None => exactly_n_digits::<4, u32>(input)?.map(|val| val.cast_signed()),
    })
}

/// Parse a month.
pub(crate) fn month(input: &[u8]) -> Option<ParsedItem<'_, Month>> {
    first_match(
        [
            (b"01".as_slice(), Month::January),
            (b"02".as_slice(), Month::February),
            (b"03".as_slice(), Month::March),
            (b"04".as_slice(), Month::April),
            (b"05".as_slice(), Month::May),
            (b"06".as_slice(), Month::June),
            (b"07".as_slice(), Month::July),
            (b"08".as_slice(), Month::August),
            (b"09".as_slice(), Month::September),
            (b"10".as_slice(), Month::October),
            (b"11".as_slice(), Month::November),
            (b"12".as_slice(), Month::December),
        ],
        true,
    )(input)
}

/// Parse a week number.
pub(crate) fn week(input: &[u8]) -> Option<ParsedItem<'_, NonZeroU8>> {
    exactly_n_digits::<2, _>(input)
}

/// Parse a day of the month.
pub(crate) fn day(input: &[u8]) -> Option<ParsedItem<'_, NonZeroU8>> {
    exactly_n_digits::<2, _>(input)
}

/// Parse a day of the week.
pub(crate) fn dayk(input: &[u8]) -> Option<ParsedItem<'_, Weekday>> {
    first_match(
        [
            (b"1".as_slice(), Weekday::Monday),
            (b"2".as_slice(), Weekday::Tuesday),
            (b"3".as_slice(), Weekday::Wednesday),
            (b"4".as_slice(), Weekday::Thursday),
            (b"5".as_slice(), Weekday::Friday),
            (b"6".as_slice(), Weekday::Saturday),
            (b"7".as_slice(), Weekday::Sunday),
        ],
        true,
    )(input)
}

/// Parse a day of the year.
pub(crate) fn dayo(input: &[u8]) -> Option<ParsedItem<'_, NonZeroU16>> {
    exactly_n_digits::<3, _>(input)
}

/// Parse the hour.
pub(crate) fn hour(input: &[u8]) -> Option<ParsedItem<'_, u8>> {
    exactly_n_digits::<2, _>(input)
}

/// Parse the minute.
pub(crate) fn min(input: &[u8]) -> Option<ParsedItem<'_, u8>> {
    exactly_n_digits::<2, _>(input)
}

/// Parse a floating point number as its integer and optional fractional parts.
///
/// The number must have two digits before the decimal point. If a decimal point is present, at
/// least one digit must follow.
///
/// The return type is a tuple of the integer part and optional fraction part.
pub(crate) fn float(input: &[u8]) -> Option<ParsedItem<'_, (u8, Option<f64>)>> {
    // Two digits before the decimal.
    let ParsedItem(input, integer_part) = match input {
        [
            first_digit @ b'0'..=b'9',
            second_digit @ b'0'..=b'9',
            input @ ..,
        ] => ParsedItem(input, (first_digit - b'0') * 10 + (second_digit - b'0')),
        _ => return None,
    };

    if let Some(ParsedItem(input, ())) = decimal_sign(input) {
        // Mandatory post-decimal digit.
        let ParsedItem(mut input, mut fractional_part) =
            any_digit(input)?.map(|digit| ((digit - b'0') as f64) / 10.);

        let mut divisor = 10.;
        // Any number of subsequent digits.
        while let Some(ParsedItem(new_input, digit)) = any_digit(input) {
            input = new_input;
            divisor *= 10.;
            fractional_part += (digit - b'0') as f64 / divisor;
        }

        Some(ParsedItem(input, (integer_part, Some(fractional_part))))
    } else {
        Some(ParsedItem(input, (integer_part, None)))
    }
}

/// Parse a "decimal sign", which is either a comma or a period.
fn decimal_sign(input: &[u8]) -> Option<ParsedItem<'_, ()>> {
    ascii_char::<b'.'>(input).or_else(|| ascii_char::<b','>(input))
}