Skip to main content

time/parsing/
component.rs

1//! Parsing implementations for all [`Component`](crate::format_description::Component)s.
2
3use core::num::NonZero;
4
5use num_conv::prelude::*;
6
7use crate::convert::*;
8use crate::format_description::{Period, modifier};
9use crate::parsing::ParsedItem;
10use crate::parsing::combinator::{
11    ExactlyNDigits, Sign, any_digit, exactly_n_digits_padded, n_to_m_digits, n_to_m_digits_padded,
12    opt, sign,
13};
14use crate::{Month, Weekday};
15
16/// Parse the "year" component of a `Date`.
17pub(crate) fn parse_year(
18    input: &[u8],
19    modifiers: modifier::Year,
20) -> Option<ParsedItem<'_, (i32, bool)>> {
21    match modifiers.repr {
22        modifier::YearRepr::Full => {
23            let ParsedItem(input, sign) = opt(sign)(input);
24
25            if let Some(sign) = sign {
26                let ParsedItem(input, year) = if cfg!(feature = "large-dates")
27                    && modifiers.range == modifier::YearRange::Extended
28                {
29                    n_to_m_digits_padded::<4, 6, u32>(modifiers.padding)(input)?
30                } else {
31                    exactly_n_digits_padded::<4, u32>(modifiers.padding)(input)?
32                };
33
34                Some(ParsedItem(
35                    input,
36                    match sign {
37                        Sign::Negative => (-year.cast_signed(), true),
38                        Sign::Positive => (year.cast_signed(), false),
39                    },
40                ))
41            } else if modifiers.sign_is_mandatory {
42                None
43            } else {
44                let ParsedItem(input, year) =
45                    exactly_n_digits_padded::<4, u32>(modifiers.padding)(input)?;
46                Some(ParsedItem(input, (year.cast_signed(), false)))
47            }
48        }
49        modifier::YearRepr::Century => {
50            let ParsedItem(input, sign) = opt(sign)(input);
51
52            if let Some(sign) = sign {
53                let ParsedItem(input, year) = if cfg!(feature = "large-dates")
54                    && modifiers.range == modifier::YearRange::Extended
55                {
56                    n_to_m_digits_padded::<2, 4, u32>(modifiers.padding)(input)?
57                } else {
58                    exactly_n_digits_padded::<2, u32>(modifiers.padding)(input)?
59                };
60
61                Some(ParsedItem(
62                    input,
63                    match sign {
64                        Sign::Negative => (-year.cast_signed(), true),
65                        Sign::Positive => (year.cast_signed(), false),
66                    },
67                ))
68            } else if modifiers.sign_is_mandatory {
69                None
70            } else {
71                let ParsedItem(input, year) =
72                    n_to_m_digits_padded::<1, 2, u32>(modifiers.padding)(input)?;
73                Some(ParsedItem(input, (year.cast_signed(), false)))
74            }
75        }
76        modifier::YearRepr::LastTwo => Some(
77            exactly_n_digits_padded::<2, u32>(modifiers.padding)(input)?
78                .map(|v| (v.cast_signed(), false)),
79        ),
80    }
81}
82
83/// Parse the "month" component of a `Date`.
84#[inline]
85pub(crate) fn parse_month(
86    input: &[u8],
87    modifiers: modifier::Month,
88) -> Option<ParsedItem<'_, Month>> {
89    use Month::*;
90    match modifiers.repr {
91        modifier::MonthRepr::Numerical => {
92            exactly_n_digits_padded::<2, _>(modifiers.padding)(input)?
93                .flat_map(|n| Month::from_number(NonZero::new(n)?).ok())
94        }
95        modifier::MonthRepr::Long | modifier::MonthRepr::Short => {
96            let [first, second, third, rest @ ..] = input else {
97                return None;
98            };
99            let byte = if modifiers.case_sensitive {
100                u32::from_ne_bytes([0, *first, *second, *third])
101            } else {
102                u32::from_ne_bytes([
103                    0,
104                    first.to_ascii_uppercase(),
105                    second.to_ascii_lowercase(),
106                    third.to_ascii_lowercase(),
107                ])
108            };
109            const WEEKDAYS: [u32; 12] = [
110                u32::from_ne_bytes([0, b'J', b'a', b'n']),
111                u32::from_ne_bytes([0, b'F', b'e', b'b']),
112                u32::from_ne_bytes([0, b'M', b'a', b'r']),
113                u32::from_ne_bytes([0, b'A', b'p', b'r']),
114                u32::from_ne_bytes([0, b'M', b'a', b'y']),
115                u32::from_ne_bytes([0, b'J', b'u', b'n']),
116                u32::from_ne_bytes([0, b'J', b'u', b'l']),
117                u32::from_ne_bytes([0, b'A', b'u', b'g']),
118                u32::from_ne_bytes([0, b'S', b'e', b'p']),
119                u32::from_ne_bytes([0, b'O', b'c', b't']),
120                u32::from_ne_bytes([0, b'N', b'o', b'v']),
121                u32::from_ne_bytes([0, b'D', b'e', b'c']),
122            ];
123
124            let bitmask = ((WEEKDAYS[0] == byte) as u32) << 1
125                | ((WEEKDAYS[1] == byte) as u32) << 2
126                | ((WEEKDAYS[2] == byte) as u32) << 3
127                | ((WEEKDAYS[3] == byte) as u32) << 4
128                | ((WEEKDAYS[4] == byte) as u32) << 5
129                | ((WEEKDAYS[5] == byte) as u32) << 6
130                | ((WEEKDAYS[6] == byte) as u32) << 7
131                | ((WEEKDAYS[7] == byte) as u32) << 8
132                | ((WEEKDAYS[8] == byte) as u32) << 9
133                | ((WEEKDAYS[9] == byte) as u32) << 10
134                | ((WEEKDAYS[10] == byte) as u32) << 11
135                | ((WEEKDAYS[11] == byte) as u32) << 12;
136            if bitmask == 0 {
137                return None;
138            }
139            let index = if cfg!(target_endian = "little") {
140                bitmask.trailing_zeros() as u8
141            } else {
142                31 - bitmask.leading_zeros() as u8
143            };
144
145            // Safety: `index` cannot be greater than 12 because there are only 12 elements in the
146            // array that is converted to a bitmask. We know at least one element matched because
147            // the bitmask is non-zero.
148            let month = unsafe { Month::from_number(NonZero::new(index)?).unwrap_unchecked() };
149
150            // For the "short" repr, we've already validated the full text expected. For the "long"
151            // repr, we need to validate the remaining characters.
152            if modifiers.repr == modifier::MonthRepr::Short {
153                return Some(ParsedItem(rest, month));
154            }
155
156            let expected_remaining = match month {
157                January => b"uary".as_slice(),
158                February => b"ruary".as_slice(),
159                March => b"ch".as_slice(),
160                April => b"il".as_slice(),
161                May => b"".as_slice(),
162                June => b"e".as_slice(),
163                July => b"y".as_slice(),
164                August => b"ust".as_slice(),
165                September => b"tember".as_slice(),
166                October => b"ober".as_slice(),
167                November | December => b"ember".as_slice(),
168            };
169
170            if modifiers.case_sensitive {
171                rest.strip_prefix(expected_remaining)
172                    .map(|remaining| ParsedItem(remaining, month))
173            } else {
174                let (head, tail) = rest.split_at_checked(expected_remaining.len())?;
175                core::iter::zip(head, expected_remaining)
176                    .all(|(a, b)| a.eq_ignore_ascii_case(b))
177                    .then_some(ParsedItem(tail, month))
178            }
179        }
180    }
181}
182
183/// Parse the "week number" component of a `Date`.
184pub(crate) fn parse_week_number(
185    input: &[u8],
186    modifiers: modifier::WeekNumber,
187) -> Option<ParsedItem<'_, u8>> {
188    exactly_n_digits_padded::<2, _>(modifiers.padding)(input)
189}
190
191/// Parse the "weekday" component of a `Date`.
192#[inline]
193pub(crate) fn parse_weekday(
194    input: &[u8],
195    modifiers: modifier::Weekday,
196) -> Option<ParsedItem<'_, Weekday>> {
197    match modifiers.repr {
198        modifier::WeekdayRepr::Long | modifier::WeekdayRepr::Short => {
199            let [first, second, third, rest @ ..] = input else {
200                return None;
201            };
202            let byte = if modifiers.case_sensitive {
203                u32::from_ne_bytes([0, *first, *second, *third])
204            } else {
205                u32::from_ne_bytes([
206                    0,
207                    first.to_ascii_uppercase(),
208                    second.to_ascii_lowercase(),
209                    third.to_ascii_lowercase(),
210                ])
211            };
212            const WEEKDAYS: [u32; 7] = [
213                u32::from_ne_bytes([0, b'M', b'o', b'n']),
214                u32::from_ne_bytes([0, b'T', b'u', b'e']),
215                u32::from_ne_bytes([0, b'W', b'e', b'd']),
216                u32::from_ne_bytes([0, b'T', b'h', b'u']),
217                u32::from_ne_bytes([0, b'F', b'r', b'i']),
218                u32::from_ne_bytes([0, b'S', b'a', b't']),
219                u32::from_ne_bytes([0, b'S', b'u', b'n']),
220            ];
221
222            let bitmask = ((WEEKDAYS[0] == byte) as u32)
223                | ((WEEKDAYS[1] == byte) as u32) << 1
224                | ((WEEKDAYS[2] == byte) as u32) << 2
225                | ((WEEKDAYS[3] == byte) as u32) << 3
226                | ((WEEKDAYS[4] == byte) as u32) << 4
227                | ((WEEKDAYS[5] == byte) as u32) << 5
228                | ((WEEKDAYS[6] == byte) as u32) << 6;
229            if bitmask == 0 {
230                return None;
231            }
232            let index = if cfg!(target_endian = "little") {
233                bitmask.trailing_zeros()
234            } else {
235                31 - bitmask.leading_zeros()
236            };
237
238            if index > 6 {
239                return None;
240            }
241            // Safety: Values zero thru six are valid variants, while values greater than six have
242            // already been excluded above. We know at least one element matched because the bitmask
243            // is non-zero.
244            let weekday = unsafe { core::mem::transmute::<u8, Weekday>(index.truncate()) };
245
246            // For the "short" repr, we've already validated the full text expected. For the "long"
247            // repr, we need to validate the remaining characters.
248            if modifiers.repr == modifier::WeekdayRepr::Short {
249                return Some(ParsedItem(rest, weekday));
250            }
251
252            let expected_remaining = match weekday {
253                Weekday::Monday | Weekday::Friday | Weekday::Sunday => b"day".as_slice(),
254                Weekday::Tuesday => b"sday".as_slice(),
255                Weekday::Wednesday => b"nesday".as_slice(),
256                Weekday::Thursday => b"rsday".as_slice(),
257                Weekday::Saturday => b"urday".as_slice(),
258            };
259
260            if modifiers.case_sensitive {
261                rest.strip_prefix(expected_remaining)
262                    .map(|remaining| ParsedItem(remaining, weekday))
263            } else {
264                let (head, tail) = rest.split_at_checked(expected_remaining.len())?;
265                core::iter::zip(head, expected_remaining)
266                    .all(|(a, b)| a.eq_ignore_ascii_case(b))
267                    .then_some(ParsedItem(tail, weekday))
268            }
269        }
270        modifier::WeekdayRepr::Sunday | modifier::WeekdayRepr::Monday => {
271            let [digit, rest @ ..] = input else {
272                return None;
273            };
274            let mut digit = digit
275                .wrapping_sub(b'0')
276                .wrapping_sub(u8::from(modifiers.one_indexed));
277            if digit > 6 {
278                return None;
279            }
280
281            if modifiers.repr == modifier::WeekdayRepr::Sunday {
282                // Remap so that Sunday comes after Saturday, not before Monday.
283                digit = (digit + 6) % 7;
284            }
285            // Safety: Values zero thru six are valid variants.
286            let weekday = unsafe { core::mem::transmute::<u8, Weekday>(digit) };
287            Some(ParsedItem(rest, weekday))
288        }
289    }
290}
291
292/// Parse the "ordinal" component of a `Date`.
293#[inline]
294pub(crate) fn parse_ordinal(
295    input: &[u8],
296    modifiers: modifier::Ordinal,
297) -> Option<ParsedItem<'_, NonZero<u16>>> {
298    exactly_n_digits_padded::<3, _>(modifiers.padding)(input)
299        .and_then(|parsed| parsed.flat_map(NonZero::new))
300}
301
302/// Parse the "day" component of a `Date`.
303#[inline]
304pub(crate) fn parse_day(
305    input: &[u8],
306    modifiers: modifier::Day,
307) -> Option<ParsedItem<'_, NonZero<u8>>> {
308    exactly_n_digits_padded::<2, _>(modifiers.padding)(input)
309        .and_then(|parsed| parsed.flat_map(NonZero::new))
310}
311
312/// Parse the "hour" component of a `Time`.
313#[inline]
314pub(crate) fn parse_hour(input: &[u8], modifiers: modifier::Hour) -> Option<ParsedItem<'_, u8>> {
315    exactly_n_digits_padded::<2, _>(modifiers.padding)(input)
316}
317
318/// Parse the "minute" component of a `Time`.
319#[inline]
320pub(crate) fn parse_minute(
321    input: &[u8],
322    modifiers: modifier::Minute,
323) -> Option<ParsedItem<'_, u8>> {
324    exactly_n_digits_padded::<2, _>(modifiers.padding)(input)
325}
326
327/// Parse the "second" component of a `Time`.
328#[inline]
329pub(crate) fn parse_second(
330    input: &[u8],
331    modifiers: modifier::Second,
332) -> Option<ParsedItem<'_, u8>> {
333    exactly_n_digits_padded::<2, _>(modifiers.padding)(input)
334}
335
336/// Parse the "period" component of a `Time`. Required if the hour is on a 12-hour clock.
337#[inline]
338pub(crate) fn parse_period(
339    input: &[u8],
340    modifiers: modifier::Period,
341) -> Option<ParsedItem<'_, Period>> {
342    let [first, second, rest @ ..] = input else {
343        return None;
344    };
345    let mut first = *first;
346    let mut second = *second;
347
348    if modifiers.is_uppercase && modifiers.case_sensitive {
349        match [first, second].as_slice() {
350            b"AM" => Some(ParsedItem(rest, Period::Am)),
351            b"PM" => Some(ParsedItem(rest, Period::Pm)),
352            _ => None,
353        }
354    } else {
355        first = first.to_ascii_lowercase();
356        second = second.to_ascii_lowercase();
357
358        match &[first, second] {
359            b"am" => Some(ParsedItem(rest, Period::Am)),
360            b"pm" => Some(ParsedItem(rest, Period::Pm)),
361            _ => None,
362        }
363    }
364}
365
366/// Parse the "subsecond" component of a `Time`.
367pub(crate) fn parse_subsecond(
368    input: &[u8],
369    modifiers: modifier::Subsecond,
370) -> Option<ParsedItem<'_, u32>> {
371    use modifier::SubsecondDigits::*;
372    Some(match modifiers.digits {
373        One => ExactlyNDigits::<1>::parse(input)?.map(|v| v.extend::<u32>() * 100_000_000),
374        Two => ExactlyNDigits::<2>::parse(input)?.map(|v| v.extend::<u32>() * 10_000_000),
375        Three => ExactlyNDigits::<3>::parse(input)?.map(|v| v.extend::<u32>() * 1_000_000),
376        Four => ExactlyNDigits::<4>::parse(input)?.map(|v| v.extend::<u32>() * 100_000),
377        Five => ExactlyNDigits::<5>::parse(input)?.map(|v| v * 10_000),
378        Six => ExactlyNDigits::<6>::parse(input)?.map(|v| v * 1_000),
379        Seven => ExactlyNDigits::<7>::parse(input)?.map(|v| v * 100),
380        Eight => ExactlyNDigits::<8>::parse(input)?.map(|v| v * 10),
381        Nine => ExactlyNDigits::<9>::parse(input)?,
382        OneOrMore => {
383            let ParsedItem(mut input, mut value) =
384                any_digit(input)?.map(|v| (v - b'0').extend::<u32>() * 100_000_000);
385
386            let mut multiplier = 10_000_000;
387            while let Some(ParsedItem(new_input, digit)) = any_digit(input) {
388                value += (digit - b'0').extend::<u32>() * multiplier;
389                input = new_input;
390                multiplier /= 10;
391            }
392
393            ParsedItem(input, value)
394        }
395    })
396}
397
398/// Parse the "hour" component of a `UtcOffset`.
399///
400/// Returns the value and whether the value is negative. This is used for when "-0" is parsed.
401#[inline]
402pub(crate) fn parse_offset_hour(
403    input: &[u8],
404    modifiers: modifier::OffsetHour,
405) -> Option<ParsedItem<'_, (i8, bool)>> {
406    let ParsedItem(input, sign) = opt(sign)(input);
407    let ParsedItem(input, hour) = exactly_n_digits_padded::<2, u8>(modifiers.padding)(input)?;
408    match sign {
409        Some(Sign::Negative) => Some(ParsedItem(input, (-hour.cast_signed(), true))),
410        None if modifiers.sign_is_mandatory => None,
411        _ => Some(ParsedItem(input, (hour.cast_signed(), false))),
412    }
413}
414
415/// Parse the "minute" component of a `UtcOffset`.
416#[inline]
417pub(crate) fn parse_offset_minute(
418    input: &[u8],
419    modifiers: modifier::OffsetMinute,
420) -> Option<ParsedItem<'_, i8>> {
421    Some(
422        exactly_n_digits_padded::<2, u8>(modifiers.padding)(input)?
423            .map(|offset_minute| offset_minute.cast_signed()),
424    )
425}
426
427/// Parse the "second" component of a `UtcOffset`.
428#[inline]
429pub(crate) fn parse_offset_second(
430    input: &[u8],
431    modifiers: modifier::OffsetSecond,
432) -> Option<ParsedItem<'_, i8>> {
433    Some(
434        exactly_n_digits_padded::<2, u8>(modifiers.padding)(input)?
435            .map(|offset_second| offset_second.cast_signed()),
436    )
437}
438
439/// Ignore the given number of bytes.
440#[inline]
441pub(crate) fn parse_ignore(
442    input: &[u8],
443    modifiers: modifier::Ignore,
444) -> Option<ParsedItem<'_, ()>> {
445    let modifier::Ignore { count } = modifiers;
446    let input = input.get((count.get().extend())..)?;
447    Some(ParsedItem(input, ()))
448}
449
450/// Parse the Unix timestamp component.
451pub(crate) fn parse_unix_timestamp(
452    input: &[u8],
453    modifiers: modifier::UnixTimestamp,
454) -> Option<ParsedItem<'_, i128>> {
455    let ParsedItem(input, sign) = opt(sign)(input);
456    let ParsedItem(input, nano_timestamp) = match modifiers.precision {
457        modifier::UnixTimestampPrecision::Second => {
458            n_to_m_digits::<1, 14, u128>(input)?.map(|val| val * Nanosecond::per_t::<u128>(Second))
459        }
460        modifier::UnixTimestampPrecision::Millisecond => n_to_m_digits::<1, 17, u128>(input)?
461            .map(|val| val * Nanosecond::per_t::<u128>(Millisecond)),
462        modifier::UnixTimestampPrecision::Microsecond => n_to_m_digits::<1, 20, u128>(input)?
463            .map(|val| val * Nanosecond::per_t::<u128>(Microsecond)),
464        modifier::UnixTimestampPrecision::Nanosecond => n_to_m_digits::<1, 23, _>(input)?,
465    };
466
467    match sign {
468        Some(Sign::Negative) => Some(ParsedItem(input, -nano_timestamp.cast_signed())),
469        None if modifiers.sign_is_mandatory => None,
470        _ => Some(ParsedItem(input, nano_timestamp.cast_signed())),
471    }
472}
473
474/// Parse the `end` component, which represents the end of input. If any input is remaining _and_
475/// trailing input is prohibited, `None` is returned. If trailing input is permitted, it is
476/// discarded.
477#[inline]
478pub(crate) fn parse_end(input: &[u8], end: modifier::End) -> Option<ParsedItem<'_, ()>> {
479    let modifier::End { trailing_input } = end;
480
481    if trailing_input == modifier::TrailingInput::Discard || input.is_empty() {
482        Some(ParsedItem(b"", ()))
483    } else {
484        None
485    }
486}