Skip to main content

ixdtf/parsers/
datetime.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 `Date`, `DateTime`, and `MonthDay`.
6
7use crate::{
8    assert_syntax,
9    encoding::EncodingType,
10    parsers::{
11        annotations,
12        grammar::{is_annotation_open, is_date_time_separator, is_hyphen, is_utc_designator},
13        time::parse_time_record,
14        timezone, Cursor, IxdtfParseRecord,
15    },
16    records::{Annotation, DateRecord, TimeRecord, UtcOffsetRecordOrZ},
17    ParseError, ParserResult,
18};
19
20use super::grammar::is_ascii_sign;
21
22#[derive(Debug, Default, Clone)]
23/// A `DateTime` Parse Node that contains the date, time, and offset info.
24pub(crate) struct DateTimeRecord {
25    /// Date
26    pub(crate) date: Option<DateRecord>,
27    /// Time
28    pub(crate) time: Option<TimeRecord>,
29    /// Tz Offset
30    pub(crate) time_zone: Option<UtcOffsetRecordOrZ>,
31}
32
33/// This function handles parsing for [`AnnotatedDateTime`][datetime],
34/// [`AnnotatedDateTimeTimeRequred`][time], and
35/// [`TemporalInstantString.`][instant] according to the requirements
36/// provided via Spec.
37///
38/// [datetime]: https://tc39.es/proposal-temporal/#prod-AnnotatedDateTime
39/// [time]: https://tc39.es/proposal-temporal/#prod-AnnotatedDateTimeTimeRequired
40/// [instant]: https://tc39.es/proposal-temporal/#prod-TemporalInstantString
41pub(crate) fn parse_annotated_date_time<'a, T: EncodingType>(
42    cursor: &mut Cursor<'a, T>,
43    handler: impl FnMut(Annotation<'a, T>) -> Option<Annotation<'a, T>>,
44) -> ParserResult<IxdtfParseRecord<'a, T>> {
45    let date_time = parse_date_time(cursor)?;
46
47    // Peek Annotation presence
48    // Throw error if annotation does not exist and zoned is true, else return.
49    if !cursor.check_or(false, is_annotation_open)? {
50        cursor.close()?;
51
52        return Ok(IxdtfParseRecord {
53            date: date_time.date,
54            time: date_time.time,
55            offset: date_time.time_zone,
56            tz: None,
57            calendar: None,
58        });
59    }
60
61    let annotation_set = annotations::parse_annotation_set(cursor, handler)?;
62
63    cursor.close()?;
64
65    Ok(IxdtfParseRecord {
66        date: date_time.date,
67        time: date_time.time,
68        offset: date_time.time_zone,
69        tz: annotation_set.tz,
70        calendar: annotation_set.calendar,
71    })
72}
73
74/// Parses a `DateTime` record.
75fn parse_date_time<T: EncodingType>(cursor: &mut Cursor<T>) -> ParserResult<DateTimeRecord> {
76    let date = parse_date(cursor)?;
77
78    // If there is no `DateTimeSeparator`, return date early.
79    if !cursor.check_or(false, is_date_time_separator)? {
80        return Ok(DateTimeRecord {
81            date: Some(date),
82            time: None,
83            time_zone: None,
84        });
85    }
86
87    cursor.advance();
88
89    let time = parse_time_record(cursor)?;
90
91    let time_zone = if cursor.check_or(false, |ch| is_ascii_sign(ch) || is_utc_designator(ch))? {
92        Some(timezone::parse_date_time_utc_offset(cursor)?)
93    } else {
94        None
95    };
96
97    Ok(DateTimeRecord {
98        date: Some(date),
99        time: Some(time),
100        time_zone,
101    })
102}
103
104/// Parses `Date` record.
105fn parse_date<T: EncodingType>(cursor: &mut Cursor<T>) -> ParserResult<DateRecord> {
106    let year = parse_date_year(cursor)?;
107    let hyphenated = cursor
108        .check(is_hyphen)?
109        .ok_or(ParseError::AbruptEnd { location: "Date" })?;
110
111    cursor.advance_if(hyphenated);
112
113    let month = parse_date_month(cursor)?;
114
115    let second_hyphen = cursor.check_or(false, is_hyphen)?;
116    assert_syntax!(hyphenated == second_hyphen, DateSeparator);
117    cursor.advance_if(second_hyphen);
118
119    let day = parse_date_day(cursor)?;
120
121    check_date_validity(year, month, day)?;
122
123    Ok(DateRecord { year, month, day })
124}
125
126// ==== `YearMonth` parsing functions ====
127
128/// Parse an annotated `YearMonth`
129pub(crate) fn parse_annotated_year_month<'a, T: EncodingType>(
130    cursor: &mut Cursor<'a, T>,
131    handler: impl FnMut(Annotation<'a, T>) -> Option<Annotation<'a, T>>,
132) -> ParserResult<IxdtfParseRecord<'a, T>> {
133    let year_month = parse_year_month(cursor)?;
134    if !cursor.check_or(false, is_annotation_open)? {
135        cursor.close()?;
136
137        return Ok(IxdtfParseRecord {
138            date: Some(year_month),
139            time: None,
140            offset: None,
141            tz: None,
142            calendar: None,
143        });
144    }
145
146    let annotation_set = annotations::parse_annotation_set(cursor, handler)?;
147    cursor.close()?;
148
149    Ok(IxdtfParseRecord {
150        date: Some(year_month),
151        time: None,
152        offset: None,
153        tz: annotation_set.tz,
154        calendar: annotation_set.calendar,
155    })
156}
157
158pub(crate) fn parse_year_month<T: EncodingType>(
159    cursor: &mut Cursor<T>,
160) -> ParserResult<DateRecord> {
161    let year = parse_date_year(cursor)?;
162    cursor.advance_if(cursor.check_or(false, is_hyphen)?);
163    let month = parse_date_month(cursor)?;
164
165    Ok(DateRecord {
166        year,
167        month,
168        day: 1,
169    })
170}
171
172// ==== `MonthDay` parsing functions ====
173
174/// Parses an `AnnotatedMonthDay`.
175pub(crate) fn parse_annotated_month_day<'a, T: EncodingType>(
176    cursor: &mut Cursor<'a, T>,
177    handler: impl FnMut(Annotation<'a, T>) -> Option<Annotation<'a, T>>,
178) -> ParserResult<IxdtfParseRecord<'a, T>> {
179    let date = parse_month_day(cursor)?;
180
181    if !cursor.check_or(false, is_annotation_open)? {
182        cursor.close()?;
183
184        return Ok(IxdtfParseRecord {
185            date: Some(date),
186            time: None,
187            offset: None,
188            tz: None,
189            calendar: None,
190        });
191    }
192
193    let annotation_set = annotations::parse_annotation_set(cursor, handler)?;
194    cursor.close()?;
195
196    Ok(IxdtfParseRecord {
197        date: Some(date),
198        time: None,
199        offset: None,
200        tz: annotation_set.tz,
201        calendar: annotation_set.calendar,
202    })
203}
204
205/// Parses a `DateSpecMonthDay`
206pub(crate) fn parse_month_day<T: EncodingType>(cursor: &mut Cursor<T>) -> ParserResult<DateRecord> {
207    let hyphenated = cursor.check(is_hyphen)?.ok_or(ParseError::AbruptEnd {
208        location: "MonthDay",
209    })?;
210    cursor.advance_if(hyphenated);
211    let balanced_hyphens = hyphenated
212        && cursor.check(is_hyphen)?.ok_or(ParseError::AbruptEnd {
213            location: "MonthDay",
214        })?;
215    cursor.advance_if(balanced_hyphens);
216
217    if hyphenated && !balanced_hyphens {
218        return Err(ParseError::MonthDayHyphen);
219    }
220
221    let month = parse_date_month(cursor)?;
222
223    cursor.advance_if(cursor.check_or(false, is_hyphen)?);
224
225    let day = parse_date_day(cursor)?;
226
227    if !is_valid_month_day(month, day) {
228        return Err(ParseError::InvalidMonthDay);
229    }
230
231    Ok(DateRecord {
232        year: 0,
233        month,
234        day,
235    })
236}
237
238fn is_valid_month_day(month: u8, day: u8) -> bool {
239    match month {
240        2 | 4 | 6 | 9 | 11 if day >= 31 => false,
241        2 if day == 30 => false,
242        _ => day <= 31,
243    }
244}
245
246// ==== Unit Parsers ====
247
248#[inline]
249fn parse_date_year<T: EncodingType>(cursor: &mut Cursor<T>) -> ParserResult<i32> {
250    if cursor.check_or(false, is_ascii_sign)? {
251        let sign = if cursor.next_or(ParseError::ImplAssert)? == b'+' {
252            1
253        } else {
254            -1
255        };
256
257        let first = cursor.next_digit()?.ok_or(ParseError::DateExtendedYear)? as i32 * 100_000;
258        let second = cursor.next_digit()?.ok_or(ParseError::DateExtendedYear)? as i32 * 10_000;
259        let third = cursor.next_digit()?.ok_or(ParseError::DateExtendedYear)? as i32 * 1000;
260        let fourth = cursor.next_digit()?.ok_or(ParseError::DateExtendedYear)? as i32 * 100;
261        let fifth = cursor.next_digit()?.ok_or(ParseError::DateExtendedYear)? as i32 * 10;
262
263        let year_value = first
264            + second
265            + third
266            + fourth
267            + fifth
268            + cursor.next_digit()?.ok_or(ParseError::DateExtendedYear)? as i32;
269
270        // 13.30.1 Static Semantics: Early Errors
271        //
272        // It is a Syntax Error if DateYear is "-000000" or "−000000" (U+2212 MINUS SIGN followed by 000000).
273        if sign == -1 && year_value == 0 {
274            return Err(ParseError::DateExtendedYear);
275        }
276
277        let year = sign * year_value;
278
279        return Ok(year);
280    }
281
282    let first = cursor.next_digit()?.ok_or(ParseError::DateYear)? as i32 * 1000;
283    let second = cursor.next_digit()?.ok_or(ParseError::DateYear)? as i32 * 100;
284    let third = cursor.next_digit()?.ok_or(ParseError::DateYear)? as i32 * 10;
285    let year_value =
286        first + second + third + cursor.next_digit()?.ok_or(ParseError::DateYear)? as i32;
287
288    Ok(year_value)
289}
290
291#[inline]
292fn parse_date_month<T: EncodingType>(cursor: &mut Cursor<T>) -> ParserResult<u8> {
293    let first = cursor.next_digit()?.ok_or(ParseError::DateMonth)?;
294    let month_value = first * 10 + cursor.next_digit()?.ok_or(ParseError::DateMonth)?;
295    if !(1..=12).contains(&month_value) {
296        return Err(ParseError::InvalidMonthRange);
297    }
298    Ok(month_value)
299}
300
301#[inline]
302fn parse_date_day<T: EncodingType>(cursor: &mut Cursor<T>) -> ParserResult<u8> {
303    let first = cursor.next_digit()?.ok_or(ParseError::DateDay)?;
304    let day_value = first * 10 + cursor.next_digit()?.ok_or(ParseError::DateDay)?;
305    Ok(day_value)
306}
307
308#[inline]
309fn check_date_validity(year: i32, month: u8, day: u8) -> ParserResult<()> {
310    let Some(days_in_month) = days_in_month(year, month) else {
311        // NOTE: This should never through due to check in `parse_date_month`
312        return Err(ParseError::InvalidMonthRange);
313    };
314    if !(1..=days_in_month).contains(&day) {
315        return Err(ParseError::InvalidDayRange);
316    }
317    Ok(())
318}
319
320/// Utilty to return the days in month, returns None if month is invalid
321#[inline]
322fn days_in_month(year: i32, month: u8) -> Option<u8> {
323    match month {
324        1 | 3 | 5 | 7 | 8 | 10 | 12 => Some(31),
325        4 | 6 | 9 | 11 => Some(30),
326        2 => Some(28 + u8::from(in_leap_year(year))),
327        _ => None,
328    }
329}
330
331/// Utility that returns whether a year is a leap year.
332#[inline]
333fn in_leap_year(year: i32) -> bool {
334    if year % 4 != 0 {
335        false
336    } else if year % 4 == 0 && year % 100 != 0 {
337        true
338    } else if year % 100 == 0 && year % 400 != 0 {
339        false
340    } else {
341        assert_eq!(year % 400, 0);
342        true
343    }
344}