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}