Skip to main content

ixdtf/parsers/
timezone.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 for Time Zone and Offset data.
6
7use super::{
8    grammar::{
9        is_a_key_leading_char, is_annotation_close, is_annotation_key_value_separator,
10        is_annotation_open, is_ascii_sign, is_critical_flag, is_time_separator, is_tz_char,
11        is_tz_leading_char, is_tz_name_separator, is_utc_designator,
12    },
13    time::{parse_fraction, parse_hour, parse_minute_second},
14    Cursor,
15};
16use crate::{
17    assert_syntax,
18    encoding::EncodingType,
19    records::{
20        FullPrecisionOffset, MinutePrecisionOffset, Sign, TimeZoneAnnotation, TimeZoneRecord,
21        UtcOffsetRecord, UtcOffsetRecordOrZ,
22    },
23    ParseError, ParserResult,
24};
25
26// NOTE: critical field on time zones is captured but not handled.
27
28// ==== Time Zone Annotation Parsing ====
29
30/// We support two kinds of annotations here: annotations (e.g. `[u-ca=foo]`)
31/// and "time zone annotations" (`[UTC]` or `[+05:30]`)
32///
33/// When parsing bracketed contents, we need to figure out which one we're dealing with.
34///
35/// This function returns a time zone annotation if we are dealing with a time zone,
36/// otherwise it returns None (and the caller must handle non-tz annotations).
37pub(crate) fn parse_ambiguous_tz_annotation<'a, T: EncodingType>(
38    cursor: &mut Cursor<'a, T>,
39) -> ParserResult<Option<TimeZoneAnnotation<'a, T>>> {
40    // Peek position + 1 to check for critical flag.
41    let mut current_peek = 1;
42    let critical =
43        cursor
44            .peek_n(current_peek)?
45            .map(is_critical_flag)
46            .ok_or(ParseError::AbruptEnd {
47                location: "AmbiguousAnnotation",
48            })?;
49
50    // Advance cursor if critical flag present.
51    if critical {
52        current_peek += 1;
53    }
54
55    let leading_char = cursor.peek_n(current_peek)?.ok_or(ParseError::AbruptEnd {
56        location: "AmbiguousAnnotation",
57    })?;
58
59    // Ambigious start values when lowercase alpha that is shared between `TzLeadingChar` and `KeyLeadingChar`.
60    if is_a_key_leading_char(leading_char) {
61        let mut peek_pos = current_peek + 1;
62        // Go through looking for `=`
63        while let Some(ch) = cursor.peek_n(peek_pos)? {
64            if is_annotation_key_value_separator(ch) {
65                // We have an `=` sign, this is a non-tz annotation
66                return Ok(None);
67            } else if is_annotation_close(ch) {
68                // We found a `]` without an `=`, this is a time zone
69                let tz = parse_tz_annotation(cursor)?;
70                return Ok(Some(tz));
71            }
72
73            peek_pos += 1;
74        }
75        Err(ParseError::AbruptEnd {
76            location: "AmbiguousAnnotation",
77        })
78    } else {
79        // Unambiguously not a non-tz annotation, try parsing a tz annotation
80        let tz = parse_tz_annotation(cursor)?;
81        Ok(Some(tz))
82    }
83}
84
85fn parse_tz_annotation<'a, T: EncodingType>(
86    cursor: &mut Cursor<'a, T>,
87) -> ParserResult<TimeZoneAnnotation<'a, T>> {
88    assert_syntax!(
89        is_annotation_open(cursor.next_or(ParseError::AnnotationOpen)?),
90        AnnotationOpen
91    );
92
93    let critical = cursor.check_or(false, is_critical_flag)?;
94    cursor.advance_if(critical);
95
96    let tz = parse_time_zone(cursor)?;
97
98    assert_syntax!(
99        is_annotation_close(cursor.next_or(ParseError::AnnotationClose)?),
100        AnnotationClose
101    );
102
103    Ok(TimeZoneAnnotation { critical, tz })
104}
105
106/// Parses the [`TimeZoneIdentifier`][tz] node.
107///
108/// [tz]: https://tc39.es/proposal-temporal/#prod-TimeZoneIdentifier
109pub(crate) fn parse_time_zone<'a, T: EncodingType>(
110    cursor: &mut Cursor<'a, T>,
111) -> ParserResult<TimeZoneRecord<'a, T>> {
112    let is_iana = cursor
113        .check(is_tz_leading_char)?
114        .ok_or(ParseError::AbruptEnd {
115            location: "TimeZoneAnnotation",
116        })?;
117    let is_offset = cursor.check_or(false, is_ascii_sign)?;
118
119    if is_iana {
120        return Ok(TimeZoneRecord::Name(parse_tz_iana_name(cursor)?));
121    } else if is_offset {
122        let offset = parse_utc_offset_minute_precision_strict(cursor)?;
123        return Ok(TimeZoneRecord::Offset(offset));
124    }
125
126    Err(ParseError::TzLeadingChar)
127}
128
129/// Parse a `TimeZoneIANAName` Parse Node
130pub(crate) fn parse_tz_iana_name<'a, T: EncodingType>(
131    cursor: &mut Cursor<'a, T>,
132) -> ParserResult<&'a [T::CodeUnit]> {
133    assert_syntax!(cursor.check_or(false, is_tz_leading_char)?, TzLeadingChar);
134    let tz_name_start = cursor.pos();
135    while let Some(potential_value_char) = cursor.next()? {
136        if cursor.check_or(true, is_annotation_close)? {
137            // Return the valid TimeZoneIANAName
138            break;
139        }
140
141        if is_tz_name_separator(potential_value_char) {
142            assert_syntax!(cursor.check_or(false, is_tz_char)?, IanaCharPostSeparator,);
143            continue;
144        }
145
146        assert_syntax!(is_tz_char(potential_value_char), IanaChar,);
147    }
148
149    cursor
150        .slice(tz_name_start, cursor.pos())
151        .ok_or(ParseError::ImplAssert)
152}
153
154// ==== Utc Offset Parsing ====
155
156/// Parses a potentially full precision UTC offset or Z
157pub(crate) fn parse_date_time_utc_offset<T: EncodingType>(
158    cursor: &mut Cursor<T>,
159) -> ParserResult<UtcOffsetRecordOrZ> {
160    if cursor.check_or(false, is_utc_designator)? {
161        cursor.advance();
162        return Ok(UtcOffsetRecordOrZ::Z);
163    }
164
165    let utc_offset = parse_utc_offset(cursor)?;
166    Ok(UtcOffsetRecordOrZ::Offset(utc_offset))
167}
168
169/// Parse a potentially full precision `UtcOffset`
170pub(crate) fn parse_utc_offset<T: EncodingType>(
171    cursor: &mut Cursor<T>,
172) -> ParserResult<UtcOffsetRecord> {
173    let (minute_precision_offset, separated) = parse_utc_offset_minute_precision(cursor)?;
174
175    // If `UtcOffsetWithSubMinuteComponents`, continue parsing.
176    if !cursor.check_or(false, |ch| ch.is_ascii_digit() || is_time_separator(ch))? {
177        return Ok(UtcOffsetRecord::MinutePrecision(minute_precision_offset));
178    }
179
180    if Some(separated) != cursor.check(is_time_separator)? {
181        return Err(ParseError::UtcTimeSeparator);
182    }
183    cursor.advance_if(cursor.check_or(false, is_time_separator)?);
184
185    let second = parse_minute_second(cursor, false)?;
186
187    let fraction = parse_fraction(cursor)?;
188
189    Ok(UtcOffsetRecord::FullPrecisionOffset(FullPrecisionOffset {
190        minute_precision_offset,
191        second,
192        fraction,
193    }))
194}
195
196pub(crate) fn parse_utc_offset_minute_precision_strict<T: EncodingType>(
197    cursor: &mut Cursor<T>,
198) -> ParserResult<MinutePrecisionOffset> {
199    let (offset, _) = parse_utc_offset_minute_precision(cursor)?;
200    if cursor.check_or(false, |ch| is_time_separator(ch) || ch.is_ascii_digit())? {
201        return Err(ParseError::InvalidMinutePrecisionOffset);
202    }
203    Ok(offset)
204}
205
206/// Parse an `UtcOffsetMinutePrecision` node
207///
208/// Returns the offset and whether the utc parsing includes a minute separator.
209pub(crate) fn parse_utc_offset_minute_precision<T: EncodingType>(
210    cursor: &mut Cursor<T>,
211) -> ParserResult<(MinutePrecisionOffset, bool)> {
212    // https://www.rfc-editor.org/rfc/rfc3339#section-5.6
213    let sign = cursor.next_or(ParseError::AbruptEnd {
214        location: "time-numoffset",
215    })?;
216    if !is_ascii_sign(sign) {
217        return Err(ParseError::OffsetNeedsSign);
218    }
219    let sign = Sign::from(sign == b'+');
220
221    let hour = parse_hour(cursor)?;
222
223    // If at the end of the utc, then return.
224    if !cursor.check_or(false, |ch| ch.is_ascii_digit() || is_time_separator(ch))? {
225        let offset = MinutePrecisionOffset {
226            sign,
227            hour,
228            minute: 0,
229        };
230        return Ok((offset, false));
231    }
232    // Advance cursor beyond any TimeSeparator
233    let separated = cursor.check_or(false, is_time_separator)?;
234    cursor.advance_if(separated);
235
236    let minute = parse_minute_second(cursor, false)?;
237
238    Ok((MinutePrecisionOffset { sign, hour, minute }, separated))
239}