Skip to main content

ixdtf/parsers/
mod.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//! The parser module contains the implementation details for `IxdtfParser` and `IsoDurationParser`
6
7use crate::core::Cursor;
8use crate::encoding::{EncodingType, Utf16, Utf8};
9use crate::ParserResult;
10
11#[cfg(feature = "duration")]
12use crate::records::DurationParseRecord;
13use crate::records::{IxdtfParseRecord, TimeZoneRecord, UtcOffsetRecord};
14
15use crate::records::Annotation;
16
17mod annotations;
18pub(crate) mod datetime;
19#[cfg(feature = "duration")]
20pub(crate) mod duration;
21mod grammar;
22mod time;
23pub(crate) mod timezone;
24
25#[cfg(test)]
26mod tests;
27
28/// `assert_syntax!` is a parser specific utility macro for asserting a syntax test, and returning the
29/// the provided provided error if the assertion fails.
30#[macro_export]
31macro_rules! assert_syntax {
32    ($cond:expr, $err:ident $(,)?) => {
33        if !$cond {
34            return Err(ParseError::$err);
35        }
36    };
37}
38
39/// `IxdtfParser` is the primary parser implementation of `ixdtf`.
40///
41/// This parser provides various options for parsing date/time strings with the extended notation
42/// laid out in [RFC9557][rfc9557] along with other variations laid out in the [`Temporal`][temporal-proposal].
43///
44/// ```rust
45/// use ixdtf::{
46///     parsers::IxdtfParser,
47///     records::{Sign, TimeZoneRecord, UtcOffsetRecord},
48/// };
49///
50/// let ixdtf_str = "2024-03-02T08:48:00-05:00[America/New_York]";
51///
52/// let result = IxdtfParser::from_str(ixdtf_str).parse().unwrap();
53///
54/// let date = result.date.unwrap();
55/// let time = result.time.unwrap();
56/// let offset = result.offset.unwrap().resolve_rfc_9557();
57/// let tz_annotation = result.tz.unwrap();
58///
59/// assert_eq!(date.year, 2024);
60/// assert_eq!(date.month, 3);
61/// assert_eq!(date.day, 2);
62/// assert_eq!(time.hour, 8);
63/// assert_eq!(time.minute, 48);
64/// assert_eq!(offset.sign(), Sign::Negative);
65/// assert_eq!(offset.hour(), 5);
66/// assert_eq!(offset.minute(), 0);
67/// assert_eq!(offset.second(), None);
68/// assert_eq!(offset.fraction(), None);
69/// assert!(!tz_annotation.critical);
70/// assert_eq!(
71///     tz_annotation.tz,
72///     TimeZoneRecord::Name("America/New_York".as_bytes())
73/// );
74/// ```
75///
76/// [rfc9557]: https://datatracker.ietf.org/doc/rfc9557/
77/// [temporal-proposal]: https://tc39.es/proposal-temporal/
78#[derive(Debug)]
79pub struct IxdtfParser<'a, T: EncodingType> {
80    cursor: Cursor<'a, T>,
81}
82
83impl<'a> IxdtfParser<'a, Utf8> {
84    /// Creates a new `IxdtfParser` from a source `&str`.
85    #[inline]
86    #[must_use]
87    #[expect(clippy::should_implement_trait)]
88    pub fn from_str(source: &'a str) -> Self {
89        Self::from_utf8(source.as_bytes())
90    }
91
92    /// Creates a new `IxdtfParser` from a slice of utf-8 bytes.
93    #[inline]
94    #[must_use]
95    pub fn from_utf8(source: &'a [u8]) -> Self {
96        Self::new(source)
97    }
98}
99
100impl<'a> IxdtfParser<'a, Utf16> {
101    /// Creates a new `IxdtfParser` from a slice of utf-16 bytes.
102    pub fn from_utf16(source: &'a [u16]) -> Self {
103        Self::new(source)
104    }
105}
106
107impl<'a, T: EncodingType> IxdtfParser<'a, T> {
108    /// Create a new `IxdtfParser` for the specified encoding.
109    #[inline]
110    #[must_use]
111    pub fn new(source: &'a [T::CodeUnit]) -> Self {
112        Self {
113            cursor: Cursor::new(source),
114        }
115    }
116
117    /// Parses the source as an [extended Date/Time string][rfc9557].
118    ///
119    /// This is the baseline parse method for `ixdtf`. For this method, the
120    /// [`TimeRecord`](crate::records::TimeRecord), [`UtcOffsetRecord`],
121    /// and all annotations are optional.
122    ///
123    /// # Example
124    ///
125    /// [rfc9557]: https://datatracker.ietf.org/doc/rfc9557/
126    pub fn parse(&mut self) -> ParserResult<IxdtfParseRecord<'a, T>> {
127        self.parse_with_annotation_handler(Some)
128    }
129
130    /// Parses the source as an extended Date/Time string with an Annotation handler.
131    ///
132    /// For more, see [Implementing Annotation Handlers](crate#implementing-annotation-handlers)
133    pub fn parse_with_annotation_handler(
134        &mut self,
135        handler: impl FnMut(Annotation<'a, T>) -> Option<Annotation<'a, T>>,
136    ) -> ParserResult<IxdtfParseRecord<'a, T>> {
137        datetime::parse_annotated_date_time(&mut self.cursor, handler)
138    }
139
140    /// Parses the source as an extended [YearMonth string][temporal-ym].
141    ///
142    /// # Example
143    ///
144    /// ```rust
145    /// # use ixdtf::parsers::IxdtfParser;
146    ///
147    /// let extended_year_month = "2020-11[u-ca=iso8601]";
148    ///
149    /// let result = IxdtfParser::from_str(extended_year_month)
150    ///     .parse_year_month()
151    ///     .unwrap();
152    ///
153    /// let date = result.date.unwrap();
154    ///
155    /// assert_eq!(date.year, 2020);
156    /// assert_eq!(date.month, 11);
157    /// ```
158    ///
159    /// [temporal-ym]: https://tc39.es/proposal-temporal/#prod-TemporalYearMonthString
160    pub fn parse_year_month(&mut self) -> ParserResult<IxdtfParseRecord<'a, T>> {
161        self.parse_year_month_with_annotation_handler(Some)
162    }
163
164    /// Parses the source as an extended `YearMonth` string with an Annotation handler.
165    ///
166    /// For more, see [Implementing Annotation Handlers](crate#implementing-annotation-handlers)
167    pub fn parse_year_month_with_annotation_handler(
168        &mut self,
169        handler: impl FnMut(Annotation<'a, T>) -> Option<Annotation<'a, T>>,
170    ) -> ParserResult<IxdtfParseRecord<'a, T>> {
171        datetime::parse_annotated_year_month(&mut self.cursor, handler)
172    }
173
174    /// Parses the source as an extended [MonthDay string][temporal-md].
175    ///
176    /// # Example
177    ///
178    /// ```rust
179    /// # use ixdtf::parsers::IxdtfParser;
180    /// let extended_month_day = "1107[+04:00]";
181    ///
182    /// let result = IxdtfParser::from_str(extended_month_day)
183    ///     .parse_month_day()
184    ///     .unwrap();
185    ///
186    /// let date = result.date.unwrap();
187    ///
188    /// assert_eq!(date.month, 11);
189    /// assert_eq!(date.day, 7);
190    /// ```
191    ///
192    /// [temporal-md]: https://tc39.es/proposal-temporal/#prod-TemporalMonthDayString
193    pub fn parse_month_day(&mut self) -> ParserResult<IxdtfParseRecord<'a, T>> {
194        self.parse_month_day_with_annotation_handler(Some)
195    }
196
197    /// Parses the source as an extended `MonthDay` string with an Annotation handler.
198    ///
199    /// For more, see [Implementing Annotation Handlers](crate#implementing-annotation-handlers)
200    pub fn parse_month_day_with_annotation_handler(
201        &mut self,
202        handler: impl FnMut(Annotation<'a, T>) -> Option<Annotation<'a, T>>,
203    ) -> ParserResult<IxdtfParseRecord<'a, T>> {
204        datetime::parse_annotated_month_day(&mut self.cursor, handler)
205    }
206
207    /// Parses the source as an extended [Time string][temporal-time].
208    ///
209    /// # Example
210    ///
211    /// ```rust
212    /// # use ixdtf::{parsers::IxdtfParser, records::{Sign, TimeZoneRecord}};
213    /// let extended_time = "12:01:04-05:00[America/New_York][u-ca=iso8601]";
214    ///
215    /// let result = IxdtfParser::from_str(extended_time).parse_time().unwrap();
216    ///
217    /// let time = result.time.unwrap();
218    /// let offset = result.offset.unwrap().resolve_rfc_9557();
219    /// let tz_annotation = result.tz.unwrap();
220    ///
221    /// assert_eq!(time.hour, 12);
222    /// assert_eq!(time.minute, 1);
223    /// assert_eq!(time.second, 4);
224    /// assert_eq!(offset.sign(), Sign::Negative);
225    /// assert_eq!(offset.hour(), 5);
226    /// assert_eq!(offset.minute(), 0);
227    /// assert!(!tz_annotation.critical);
228    /// assert_eq!(
229    ///     tz_annotation.tz,
230    ///     TimeZoneRecord::Name("America/New_York".as_bytes())
231    /// );
232    /// ```
233    ///
234    /// [temporal-time]: https://tc39.es/proposal-temporal/#prod-TemporalTimeString
235    pub fn parse_time(&mut self) -> ParserResult<IxdtfParseRecord<'a, T>> {
236        self.parse_time_with_annotation_handler(Some)
237    }
238
239    /// Parses the source as an extended Time string with an Annotation handler.
240    ///
241    /// For more, see [Implementing Annotation Handlers](crate#implementing-annotation-handlers)
242    pub fn parse_time_with_annotation_handler(
243        &mut self,
244        handler: impl FnMut(Annotation<'a, T>) -> Option<Annotation<'a, T>>,
245    ) -> ParserResult<IxdtfParseRecord<'a, T>> {
246        time::parse_annotated_time_record(&mut self.cursor, handler)
247    }
248}
249
250/// A parser for time zone offset and IANA identifier strings.
251///
252/// ✨ *Enabled with the `timezone` Cargo feature.*
253#[derive(Debug)]
254pub struct TimeZoneParser<'a, T: EncodingType> {
255    cursor: Cursor<'a, T>,
256}
257
258impl<'a> TimeZoneParser<'a, Utf8> {
259    /// Creates a new `TimeZoneParser` from a source `&str`.
260    #[inline]
261    #[must_use]
262    #[expect(clippy::should_implement_trait)]
263    pub fn from_str(source: &'a str) -> Self {
264        Self::from_utf8(source.as_bytes())
265    }
266
267    /// Creates a new `TimeZoneParser` from a slice of utf-8 bytes.
268    #[inline]
269    #[must_use]
270    pub fn from_utf8(source: &'a [u8]) -> Self {
271        Self::new(source)
272    }
273}
274
275impl<'a> TimeZoneParser<'a, Utf16> {
276    /// Creates a new `TimeZoneParser` from a slice of utf-16 bytes.
277    pub fn from_utf16(source: &'a [u16]) -> Self {
278        Self::new(source)
279    }
280}
281
282impl<'a, T: EncodingType> TimeZoneParser<'a, T> {
283    /// Creates a new `TimeZoneParser` for the provided encoding.
284    #[inline]
285    #[must_use]
286    pub fn new(source: &'a [T::CodeUnit]) -> Self {
287        Self {
288            cursor: Cursor::new(source),
289        }
290    }
291
292    /// Parse a time zone identifier that can be either an
293    /// IANA identifer name or minute precision offset.
294    ///
295    /// ## IANA identifier example
296    ///
297    /// ```rust
298    /// use ixdtf::{parsers::TimeZoneParser, records::TimeZoneRecord};
299    ///
300    /// let identifier = "Europe/London";
301    /// let record = TimeZoneParser::from_str(identifier)
302    ///     .parse_identifier()
303    ///     .unwrap();
304    /// assert_eq!(record, TimeZoneRecord::Name(identifier.as_bytes()))
305    /// ```
306    ///
307    /// ## Minute precision offset example
308    ///
309    /// ```rust
310    /// use ixdtf::{
311    ///     parsers::TimeZoneParser,
312    ///     records::{MinutePrecisionOffset, Sign, TimeZoneRecord},
313    /// };
314    ///
315    /// let identifier = "+00:00";
316    /// let offset = match TimeZoneParser::from_str(identifier).parse_identifier() {
317    ///     Ok(TimeZoneRecord::Offset(o)) => o,
318    ///     _ => unreachable!(),
319    /// };
320    ///
321    /// assert_eq!(offset.sign, Sign::Positive);
322    /// assert_eq!(offset.hour, 0);
323    /// assert_eq!(offset.minute, 0);
324    /// ```
325    ///
326    /// ## Errors
327    ///
328    /// It is an error to provide a full precision offset as a
329    /// time zone identifier.
330    ///
331    /// **NOTE**: To parse either a full or minute precision,
332    /// use [`Self::parse_offset`].
333    ///
334    /// ```rust
335    /// use ixdtf::{parsers::TimeZoneParser, ParseError};
336    ///
337    /// let identifier = "+00:00:00";
338    /// let err = TimeZoneParser::from_str(identifier)
339    ///     .parse_identifier()
340    ///     .unwrap_err();
341    /// assert_eq!(err, ParseError::InvalidMinutePrecisionOffset);
342    ///
343    /// let identifier = "+00:00.1";
344    /// let err = TimeZoneParser::from_str(identifier)
345    ///     .parse_identifier()
346    ///     .unwrap_err();
347    /// assert_eq!(err, ParseError::InvalidEnd);
348    /// ```
349    pub fn parse_identifier(&mut self) -> ParserResult<TimeZoneRecord<'a, T>> {
350        let result = timezone::parse_time_zone(&mut self.cursor)?;
351        self.cursor.close()?;
352        Ok(result)
353    }
354
355    /// Parse a UTC offset from the provided source.
356    ///
357    /// This method can parse both a minute precision and full
358    /// precision offset.
359    ///
360    /// ## Minute precision offset example
361    ///
362    /// ```rust
363    /// use ixdtf::{parsers::TimeZoneParser, records::Sign};
364    ///
365    /// let offset_src = "-05:00";
366    /// let parse_result =
367    ///     TimeZoneParser::from_str(offset_src).parse_offset().unwrap();
368    /// assert_eq!(parse_result.sign(), Sign::Negative);
369    /// assert_eq!(parse_result.hour(), 5);
370    /// assert_eq!(parse_result.minute(), 0);
371    /// assert_eq!(parse_result.second(), None);
372    /// assert_eq!(parse_result.fraction(), None);
373    /// ```
374    ///
375    /// ## Full precision offset example
376    ///
377    /// ```rust
378    /// use ixdtf::{parsers::TimeZoneParser, records::Sign};
379    ///
380    /// let offset_src = "-05:00:30.123456789";
381    /// let parse_result =
382    ///     TimeZoneParser::from_str(offset_src).parse_offset().unwrap();
383    /// assert_eq!(parse_result.sign(), Sign::Negative);
384    /// assert_eq!(parse_result.hour(), 5);
385    /// assert_eq!(parse_result.minute(), 0);
386    /// assert_eq!(parse_result.second(), Some(30));
387    /// let fraction = parse_result.fraction().unwrap();
388    /// assert_eq!(fraction.to_nanoseconds(), Some(123456789));
389    /// ```
390    #[inline]
391    pub fn parse_offset(&mut self) -> ParserResult<UtcOffsetRecord> {
392        let result = timezone::parse_utc_offset(&mut self.cursor)?;
393        self.cursor.close()?;
394        Ok(result)
395    }
396
397    /// Parse an IANA identifier name.
398    ///
399    ///
400    /// ```rust
401    /// use ixdtf::{parsers::TimeZoneParser, records::Sign};
402    ///
403    /// let iana_identifier = "America/Chicago";
404    /// let parse_result = TimeZoneParser::from_str(iana_identifier)
405    ///     .parse_iana_identifier()
406    ///     .unwrap();
407    /// assert_eq!(parse_result, iana_identifier.as_bytes());
408    ///
409    /// let iana_identifier = "Europe/Berlin";
410    /// let parse_result = TimeZoneParser::from_str(iana_identifier)
411    ///     .parse_iana_identifier()
412    ///     .unwrap();
413    /// assert_eq!(parse_result, iana_identifier.as_bytes());
414    /// ```
415    #[inline]
416    pub fn parse_iana_identifier(&mut self) -> ParserResult<&'a [T::CodeUnit]> {
417        let result = timezone::parse_tz_iana_name(&mut self.cursor)?;
418        self.cursor.close()?;
419        Ok(result)
420    }
421}
422
423/// A parser for ISO8601 Duration strings.
424///
425/// ✨ *Enabled with the `duration` Cargo feature.*
426///
427/// # Example
428///
429/// ```rust
430/// use ixdtf::{parsers::IsoDurationParser, records::{Sign, DurationParseRecord, TimeDurationRecord}};
431///
432/// let duration_str = "P1Y2M1DT2H10M30S";
433///
434/// let result = IsoDurationParser::from_str(duration_str).parse().unwrap();
435///
436/// let date_duration = result.date.unwrap();
437///
438/// let (hours, minutes, seconds, fraction) = match result.time {
439///     // Hours variant is defined as { hours: u32, fraction: Option<Fraction> }
440///     Some(TimeDurationRecord::Hours{ hours, fraction }) => (hours, 0, 0, fraction),
441///     // Minutes variant is defined as { hours: u32, minutes: u32, fraction: Option<Fraction> }
442///     Some(TimeDurationRecord::Minutes{ hours, minutes, fraction }) => (hours, minutes, 0, fraction),
443///     // Seconds variant is defined as { hours: u32, minutes: u32, seconds: u32, fraction: Option<Fraction> }
444///     Some(TimeDurationRecord::Seconds{ hours, minutes, seconds, fraction }) => (hours, minutes, seconds, fraction),
445///     None => (0,0,0, None),
446/// };
447///
448/// assert_eq!(result.sign, Sign::Positive);
449/// assert_eq!(date_duration.years, 1);
450/// assert_eq!(date_duration.months, 2);
451/// assert_eq!(date_duration.weeks, 0);
452/// assert_eq!(date_duration.days, 1);//
453/// assert_eq!(hours, 2);
454/// assert_eq!(minutes, 10);
455/// assert_eq!(seconds, 30);
456/// assert_eq!(fraction, None);
457/// ```
458#[cfg(feature = "duration")]
459#[derive(Debug)]
460pub struct IsoDurationParser<'a, T: EncodingType> {
461    cursor: Cursor<'a, T>,
462}
463
464#[cfg(feature = "duration")]
465impl<'a> IsoDurationParser<'a, Utf8> {
466    /// Creates a new `IsoDurationParser` from a source `&str`.
467    #[inline]
468    #[must_use]
469    #[expect(clippy::should_implement_trait)]
470    pub fn from_str(source: &'a str) -> Self {
471        Self::from_utf8(source.as_bytes())
472    }
473
474    /// Creates a new `IsoDurationParser` from a slice of utf-8 bytes.
475    #[inline]
476    #[must_use]
477    pub fn from_utf8(source: &'a [u8]) -> Self {
478        Self::new(source)
479    }
480}
481
482#[cfg(feature = "duration")]
483impl<'a> IsoDurationParser<'a, Utf16> {
484    /// Creates a new `IsoDurationParser` from a slice of utf-16 bytes.
485    #[inline]
486    #[must_use]
487    pub fn from_utf8(source: &'a [u16]) -> Self {
488        Self::new(source)
489    }
490}
491
492#[cfg(feature = "duration")]
493impl<'a, T: EncodingType> IsoDurationParser<'a, T> {
494    /// Creates a new `IsoDurationParser` for the provided encoding.
495    #[inline]
496    #[must_use]
497    pub fn new(source: &'a [T::CodeUnit]) -> Self {
498        Self {
499            cursor: Cursor::new(source),
500        }
501    }
502
503    /// Parse the contents of this `IsoDurationParser` into a `DurationParseRecord`.
504    ///
505    /// # Examples
506    ///
507    /// ## Parsing a date duration
508    ///
509    /// ```
510    /// # use ixdtf::{parsers::IsoDurationParser, records::DurationParseRecord };
511    /// let date_duration = "P1Y2M3W1D";
512    ///
513    /// let result = IsoDurationParser::from_str(date_duration).parse().unwrap();
514    ///
515    /// let date_duration = result.date.unwrap();
516    ///
517    /// assert!(result.time.is_none());
518    /// assert_eq!(date_duration.years, 1);
519    /// assert_eq!(date_duration.months, 2);
520    /// assert_eq!(date_duration.weeks, 3);
521    /// assert_eq!(date_duration.days, 1);
522    /// ```
523    ///
524    /// ## Parsing a time duration
525    ///
526    /// ```rust
527    /// # use ixdtf::{parsers::IsoDurationParser, records::{DurationParseRecord, TimeDurationRecord }};
528    /// let time_duration = "PT2H10M30S";
529    ///
530    /// let result = IsoDurationParser::from_str(time_duration).parse().unwrap();
531    ///
532    /// let (hours, minutes, seconds, fraction) = match result.time {
533    ///     // Hours variant is defined as { hours: u32, fraction: Option<Fraction> }
534    ///     Some(TimeDurationRecord::Hours{ hours, fraction }) => (hours, 0, 0, fraction),
535    ///     // Minutes variant is defined as { hours: u32, minutes: u32, fraction: Option<Fraction> }
536    ///     Some(TimeDurationRecord::Minutes{ hours, minutes, fraction }) => (hours, minutes, 0, fraction),
537    ///     // Seconds variant is defined as { hours: u32, minutes: u32, seconds: u32, fraction: Option<Fraction> }
538    ///     Some(TimeDurationRecord::Seconds{ hours, minutes, seconds, fraction }) => (hours, minutes, seconds, fraction),
539    ///     None => (0,0,0, None),
540    /// };
541    /// assert!(result.date.is_none());
542    /// assert_eq!(hours, 2);
543    /// assert_eq!(minutes, 10);
544    /// assert_eq!(seconds, 30);
545    /// assert_eq!(fraction, None);
546    /// ```
547    pub fn parse(&mut self) -> ParserResult<DurationParseRecord> {
548        duration::parse_duration(&mut self.cursor)
549    }
550}