Skip to main content

ixdtf/parsers/
time.rs

1// This file is part of ICU4X. For terms of use, please see the file
2// called LICENSE at the top level of the ICU4X source tree
3// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).
4
5//! Parsing of Time Values
6
7use core::num::NonZeroU8;
8
9use crate::{
10    assert_syntax,
11    encoding::EncodingType,
12    parsers::{
13        datetime::{parse_month_day, parse_year_month},
14        grammar::{
15            is_annotation_open, is_decimal_separator, is_time_designator, is_time_separator,
16            is_utc_designator,
17        },
18        Cursor,
19    },
20    records::{Annotation, Fraction, IxdtfParseRecord, TimeRecord},
21    ParseError, ParserResult,
22};
23
24use super::{annotations, grammar::is_ascii_sign, timezone};
25
26/// Parse annotated time record is silently fallible returning None in the case that the
27/// value does not align
28pub(crate) fn parse_annotated_time_record<'a, T: EncodingType>(
29    cursor: &mut Cursor<'a, T>,
30    handler: impl FnMut(Annotation<'a, T>) -> Option<Annotation<'a, T>>,
31) -> ParserResult<IxdtfParseRecord<'a, T>> {
32    let start = cursor.pos();
33    let designator = cursor.check_or(false, is_time_designator)?;
34    cursor.advance_if(designator);
35
36    let time = parse_time_record(cursor)?;
37
38    // If Time was successfully parsed, assume from this point that this IS a
39    // valid AnnotatedTimeRecord.
40
41    let offset = if cursor.check_or(false, |ch| is_ascii_sign(ch) || is_utc_designator(ch))? {
42        Some(timezone::parse_date_time_utc_offset(cursor)?)
43    } else {
44        None
45    };
46
47    // Check if annotations exist.
48    if !cursor.check_or(false, is_annotation_open)? {
49        cursor.close()?;
50        check_time_ambiguity(cursor, start)?;
51
52        return Ok(IxdtfParseRecord {
53            date: None,
54            time: Some(time),
55            offset,
56            tz: None,
57            calendar: None,
58        });
59    }
60
61    check_time_ambiguity(cursor, start)?;
62    let annotations = annotations::parse_annotation_set(cursor, handler)?;
63
64    cursor.close()?;
65
66    Ok(IxdtfParseRecord {
67        date: None,
68        time: Some(time),
69        offset,
70        tz: annotations.tz,
71        calendar: annotations.calendar,
72    })
73}
74
75#[inline]
76fn check_time_ambiguity<T: EncodingType>(cursor: &mut Cursor<T>, start: usize) -> ParserResult<()> {
77    let current_loc = cursor.pos();
78    // It is a Syntax Error if ParseText(Time DateTimeUTCOffset[~Z], DateSpecMonthDay) is a Parse Node.
79    cursor.set_position(start);
80    if parse_month_day(cursor).is_ok() {
81        return Err(ParseError::AmbiguousTimeMonthDay);
82    }
83    // It is a Syntax Error if ParseText(Time DateTimeUTCOffset[~Z], DateSpecYearMonth) is a Parse Node.
84    cursor.set_position(start);
85    if parse_year_month(cursor).is_ok() {
86        return Err(ParseError::AmbiguousTimeYearMonth);
87    }
88    cursor.set_position(current_loc);
89    Ok(())
90}
91
92/// Parse `TimeRecord`
93pub(crate) fn parse_time_record<T: EncodingType>(
94    cursor: &mut Cursor<T>,
95) -> ParserResult<TimeRecord> {
96    let hour = parse_hour(cursor)?;
97
98    if !cursor.check_or(false, |ch| is_time_separator(ch) || ch.is_ascii_digit())? {
99        return Ok(TimeRecord {
100            hour,
101            minute: 0,
102            second: 0,
103            fraction: None,
104        });
105    }
106
107    let separator_present = cursor.check_or(false, is_time_separator)?;
108    cursor.advance_if(separator_present);
109
110    let minute = parse_minute_second(cursor, false)?;
111
112    if !cursor.check_or(false, |ch| is_time_separator(ch) || ch.is_ascii_digit())? {
113        return Ok(TimeRecord {
114            hour,
115            minute,
116            second: 0,
117            fraction: None,
118        });
119    }
120
121    let second_separator = cursor.check_or(false, is_time_separator)?;
122    assert_syntax!(separator_present == second_separator, TimeSeparator);
123    cursor.advance_if(second_separator);
124
125    let second = parse_minute_second(cursor, true)?;
126
127    let fraction = parse_fraction(cursor)?;
128
129    Ok(TimeRecord {
130        hour,
131        minute,
132        second,
133        fraction,
134    })
135}
136
137/// Parse an hour value.
138#[inline]
139pub(crate) fn parse_hour<T: EncodingType>(cursor: &mut Cursor<T>) -> ParserResult<u8> {
140    let first = cursor.next_digit()?.ok_or(ParseError::TimeHour)?;
141    let hour_value = first * 10 + cursor.next_digit()?.ok_or(ParseError::TimeHour)?;
142    if !(0..=23).contains(&hour_value) {
143        return Err(ParseError::TimeHour);
144    }
145    Ok(hour_value)
146}
147
148/// Parses `MinuteSecond` value.
149#[inline]
150pub(crate) fn parse_minute_second<T: EncodingType>(
151    cursor: &mut Cursor<T>,
152    is_leap_second_valid: bool,
153) -> ParserResult<u8> {
154    let (valid_range, err) = if is_leap_second_valid {
155        (0..=60, ParseError::TimeSecond)
156    } else {
157        (0..=59, ParseError::TimeMinuteSecond)
158    };
159    let first = cursor.next_digit()?.ok_or(err)?;
160    let min_sec_value = first * 10 + cursor.next_digit()?.ok_or(err)?;
161    if !valid_range.contains(&min_sec_value) {
162        return Err(err);
163    }
164    Ok(min_sec_value)
165}
166
167/// Parse a `Fraction` value
168///
169/// This is primarily used in ISO8601 to add percision past
170/// a second.
171#[inline]
172pub(crate) fn parse_fraction<T: EncodingType>(
173    cursor: &mut Cursor<T>,
174) -> ParserResult<Option<Fraction>> {
175    // Assert that the first char provided is a decimal separator.
176    if !cursor.check_or(false, is_decimal_separator)? {
177        return Ok(None);
178    }
179    cursor.next_or(ParseError::FractionPart)?;
180
181    let mut value = 0;
182    let mut digits: u8 = 0;
183    while cursor.check_or(false, |ch| ch.is_ascii_digit())? {
184        let next_value = u64::from(cursor.next_digit()?.ok_or(ParseError::ImplAssert)?);
185        if digits < 18 {
186            value = value * 10 + next_value;
187        }
188        digits = digits.saturating_add(1);
189    }
190
191    let digits = NonZeroU8::new(digits).ok_or(ParseError::FractionPart)?;
192
193    Ok(Some(Fraction { digits, value }))
194}