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    let tz_name_start = cursor.pos();
134    assert_syntax!(cursor.next_if(is_tz_leading_char)?.is_some(), TzLeadingChar,);
135    while let Some(potential_value_char) = cursor.next_if(|ch| !is_annotation_close(ch))? {
136        if is_tz_name_separator(potential_value_char) {
137            assert_syntax!(cursor.next_if(is_tz_char)?.is_some(), IanaCharPostSeparator,);
138            continue;
139        }
140
141        assert_syntax!(is_tz_char(potential_value_char), IanaChar);
142    }
143
144    cursor
145        .slice(tz_name_start, cursor.pos())
146        .ok_or(ParseError::ImplAssert)
147}
148
149// ==== Utc Offset Parsing ====
150
151/// Parses a potentially full precision UTC offset or Z
152pub(crate) fn parse_date_time_utc_offset<T: EncodingType>(
153    cursor: &mut Cursor<T>,
154) -> ParserResult<UtcOffsetRecordOrZ> {
155    if cursor.check_or(false, is_utc_designator)? {
156        cursor.advance();
157        return Ok(UtcOffsetRecordOrZ::Z);
158    }
159
160    let utc_offset = parse_utc_offset(cursor)?;
161    Ok(UtcOffsetRecordOrZ::Offset(utc_offset))
162}
163
164/// Parse a potentially full precision `UtcOffset`
165pub(crate) fn parse_utc_offset<T: EncodingType>(
166    cursor: &mut Cursor<T>,
167) -> ParserResult<UtcOffsetRecord> {
168    let (minute_precision_offset, separated) = parse_utc_offset_minute_precision(cursor)?;
169
170    // If `UtcOffsetWithSubMinuteComponents`, continue parsing.
171    if !cursor.check_or(false, |ch| ch.is_ascii_digit() || is_time_separator(ch))? {
172        return Ok(UtcOffsetRecord::MinutePrecision(minute_precision_offset));
173    }
174
175    if Some(separated) != cursor.check(is_time_separator)? {
176        return Err(ParseError::UtcTimeSeparator);
177    }
178    cursor.advance_if(cursor.check_or(false, is_time_separator)?);
179
180    let second = parse_minute_second(cursor, false)?;
181
182    let fraction = parse_fraction(cursor)?;
183
184    Ok(UtcOffsetRecord::FullPrecisionOffset(FullPrecisionOffset {
185        minute_precision_offset,
186        second,
187        fraction,
188    }))
189}
190
191pub(crate) fn parse_utc_offset_minute_precision_strict<T: EncodingType>(
192    cursor: &mut Cursor<T>,
193) -> ParserResult<MinutePrecisionOffset> {
194    let (offset, _) = parse_utc_offset_minute_precision(cursor)?;
195    if cursor.check_or(false, |ch| is_time_separator(ch) || ch.is_ascii_digit())? {
196        return Err(ParseError::InvalidMinutePrecisionOffset);
197    }
198    Ok(offset)
199}
200
201/// Parse an `UtcOffsetMinutePrecision` node
202///
203/// Returns the offset and whether the utc parsing includes a minute separator.
204pub(crate) fn parse_utc_offset_minute_precision<T: EncodingType>(
205    cursor: &mut Cursor<T>,
206) -> ParserResult<(MinutePrecisionOffset, bool)> {
207    // https://www.rfc-editor.org/rfc/rfc3339#section-5.6
208    let sign = cursor.next_or(ParseError::AbruptEnd {
209        location: "time-numoffset",
210    })?;
211    if !is_ascii_sign(sign) {
212        return Err(ParseError::OffsetNeedsSign);
213    }
214    let sign = Sign::from(sign == b'+');
215
216    let hour = parse_hour(cursor)?;
217
218    // If at the end of the utc, then return.
219    if !cursor.check_or(false, |ch| ch.is_ascii_digit() || is_time_separator(ch))? {
220        let offset = MinutePrecisionOffset {
221            sign,
222            hour,
223            minute: 0,
224        };
225        return Ok((offset, false));
226    }
227    // Advance cursor beyond any TimeSeparator
228    let separated = cursor.check_or(false, is_time_separator)?;
229    cursor.advance_if(separated);
230
231    let minute = parse_minute_second(cursor, false)?;
232
233    Ok((MinutePrecisionOffset { sign, hour, minute }, separated))
234}