ixdtf/
lib.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//! Parsers for extended date time string and Duration parsing.
6//!
7//! The [Internet Extended Date/Time Fmt (IXDTF)][rfc9557] is laid out by RFC 9557. RFC 9557
8//! builds on RFC 3339's time stamp specification and ISO 8601 to provide an optional extension
9//! syntax for date/time strings.
10//!
11//! RFC 9557 also updates the interpretation of `Z` from RFC 3339.
12//!
13//! # Date Time Extended Examples
14//!
15//! - `2024-03-02T08:48:00-05:00[America/New_York]`
16//! - `2024-03-02T08:48:00-05:00[-05:00]`
17//! - `2024-03-02T08:48:00-05:00[u-ca=iso8601]`
18//!
19//! ## Example Usage
20//!
21//! ```
22//! use ixdtf::parsers::{
23//!     records::{Sign, TimeZoneRecord},
24//!     IxdtfParser,
25//! };
26//!
27//! let ixdtf_str = "2024-03-02T08:48:00-05:00[America/New_York]";
28//!
29//! let result = IxdtfParser::from_str(ixdtf_str).parse().unwrap();
30//!
31//! let date = result.date.unwrap();
32//! let time = result.time.unwrap();
33//! let offset = result.offset.unwrap().resolve_rfc_9557();
34//! let tz_annotation = result.tz.unwrap();
35//!
36//! assert_eq!(date.year, 2024);
37//! assert_eq!(date.month, 3);
38//! assert_eq!(date.day, 2);
39//! assert_eq!(time.hour, 8);
40//! assert_eq!(time.minute, 48);
41//! assert_eq!(offset.sign(), Sign::Negative);
42//! assert_eq!(offset.hour(), 5);
43//! assert_eq!(offset.minute(), 0);
44//! assert_eq!(offset.second(), None);
45//! assert_eq!(offset.fraction(), None);
46//! assert!(!tz_annotation.critical);
47//! assert_eq!(
48//!     tz_annotation.tz,
49//!     TimeZoneRecord::Name("America/New_York".as_bytes())
50//! );
51//! ```
52//!
53//! ## Date/Time Strings
54//!
55//! The extended suffixes laid out by RFC 9557 are optional, so the `IxdtfParser`
56//! will also still parse any valid date time strings described by RFC3339.
57//!
58//! Example Valid Date Time Strings:
59//!
60//! - `2024-03-02`
61//! - `+002024-03-02`
62//! - `20240302`
63//! - `+0020240302`
64//! - `2024-03-02T08:48:00`
65//! - `2024-03-02T08:48:00`
66//!
67//! ## Updates to Zulu interpretation from RFC 3339
68//!
69//! RFC 3339 interpreted both `+00:00` and `Z` "UTC is the preferred reference point for the
70//! specified time"; meanwhile, `-00:00` expressed "the time in UTC is known, but the local
71//! time is unknown".
72//!
73//! RFC 9557 updates the interpretation of `Z` to align with `-00:00`.
74//!
75//! ```rust
76//! use ixdtf::parsers::{
77//!     records::{Sign, TimeZoneRecord},
78//!     IxdtfParser,
79//! };
80//!
81//! let ixdtf_str = "2024-03-02T08:48:00Z[America/New_York]";
82//!
83//! let result = IxdtfParser::from_str(ixdtf_str).parse().unwrap();
84//!
85//! let date = result.date.unwrap();
86//! let time = result.time.unwrap();
87//! let offset = result.offset.unwrap().resolve_rfc_9557();
88//! let tz_annotation = result.tz.unwrap();
89//!
90//! assert_eq!(date.year, 2024);
91//! assert_eq!(date.month, 3);
92//! assert_eq!(date.day, 2);
93//! assert_eq!(time.hour, 8);
94//! assert_eq!(time.minute, 48);
95//! assert_eq!(offset.sign(), Sign::Negative);
96//! assert_eq!(offset.hour(), 0);
97//! assert_eq!(offset.minute(), 0);
98//! assert_eq!(offset.second(), None);
99//! assert_eq!(offset.fraction(), None);
100//! assert!(!tz_annotation.critical);
101//! assert_eq!(
102//!     tz_annotation.tz,
103//!     TimeZoneRecord::Name("America/New_York".as_bytes())
104//! );
105//! ```
106//!
107//! For more information on the update to RFC 3339, please see RFC 9557, Section 2.
108//!
109//! For more information on `Z` along with time zone annotations, please see the Annotations
110//! with Application Defined Behavior section below.
111//!
112//! ## IXDTF Extensions: A Deeper Look
113//!
114//! The suffix extensions come in two primary kinds: a time zone annotation and a key-value
115//! annotation. The suffixes may also be flagged as critical with a `!` as a leading flag
116//! character.
117//!
118//! ### Time Zone Annotations
119//!
120//! Time zone annotations can be either a valid IANA time zone name or numeric
121//! offset.
122//!
123//! #### Valid Time Zone Annotations
124//!
125//! - `2024-03-02T08:48:00-5:00[America/New_York]`
126//! - `2024-03-02T08:48:00-5:00[-05:00]`
127//! - `2024-03-02T08:48:00Z[America/New_York]`
128//!
129//! ##### Time Zone Consistency
130//!
131//! With the update to RFC 3339, when `Z` is provided as a datetime offset along side a time zone
132//! annotation, the IXDTF string is not considered inconsistent as `Z` does not assert any local
133//! time. Instead, an application may decide to calculate the time with the rules of the time
134//! zone annotation if it is provided.
135//!
136//! ```rust
137//! use ixdtf::parsers::{
138//!     records::{Sign, TimeZoneRecord},
139//!     IxdtfParser,
140//! };
141//!
142//! let zulu_offset = "2024-03-02T08:48:00Z[!America/New_York]";
143//!
144//! let result = IxdtfParser::from_str(zulu_offset).parse().unwrap();
145//!
146//! let tz_annotation = result.tz.unwrap();
147//! let offset = result.offset.unwrap().resolve_rfc_9557();
148//!
149//! // The offset is `Z`/`-00:00`, so the application can use the rules of
150//! // "America/New_York" to calculate the time for IXDTF string.
151//! assert_eq!(offset.sign(), Sign::Negative);
152//! assert_eq!(offset.hour(), 0);
153//! assert_eq!(offset.minute(), 0);
154//! assert_eq!(offset.second(), None);
155//! assert_eq!(offset.fraction(), None);
156//! assert!(tz_annotation.critical);
157//! assert_eq!(
158//!     tz_annotation.tz,
159//!     TimeZoneRecord::Name("America/New_York".as_bytes())
160//! );
161//! ```
162//!
163//! ### Key-Value Annotations
164//!
165//! Key-value pair annotations are any key and value string separated by a '=' character.
166//! Key-value pairs are can include any information. Keys may be permanently registered,
167//! provisionally registered, or unknown; however, only permanent keys are acted on by
168//! `IxdtfParser`.
169//!
170//! If duplicate registered keys are provided the first key will be returned, unless one
171//! of the duplicate annotations is marked as critical, in which case an error may be
172//! thrown by the `ixdtf` (See [Invalid Annotations](#invalid-annotations) for more
173//! information).
174//!
175//! #### Permanent Registered Keys
176//!
177//! - `u-ca`
178//!
179//! #### Valid Annotations
180//!
181//! - (1) `2024-03-02T08:48:00-05:00[America/New_York][u-ca=iso8601]`
182//! - (2) `2024-03-02T08:48:00-05:00[u-ca=iso8601][u-ca=japanese]`
183//! - (3) `2024-03-02T08:48:00-05:00[u-ca=iso8601][!u-ca=iso8601]`
184//! - (4) `2024-03-02T08:48:00-05:00[u-ca=iso8601][answer-to-universe=fortytwo]`
185//!
186//! ##### Example 1
187//!
188//! This is a basic annotation string that has a Time Zone and calendar annotation.
189//!
190//! ##### Example 2
191//!
192//! This example is duplicate and different calendar annotations, but neither calendar
193//! is flagged as critical so the first calendar is returned while the second calendar
194//! is ignored.
195//!
196//! ##### Example 3
197//!
198//! This example is a duplicate and identical calendar annotations with one annotation flagged
199//! as critical. As the annotations are identical values, there is no ambiguity with the use of
200//! the critical flag that may cause an error. Thus, the first annotation is returned, and the
201//! second is ignored (See [Annotations with Application Defined
202//! Behavior](#annotations-with-application-defined-behavior)).
203//!
204//! ##### Example 4
205//!
206//! This example contains an unknown annotation. The annotation is not marked as critical
207//! so the value is ignored (See [Implementing Annotation Handlers](#implementing-annotation-handlers)).
208//!
209//! #### Invalid Annotations
210//!
211//! The below `ixdtf` strings have invalid annotations that will cause an error
212//! to be thrown (NOTE: these are not to be confused with potentially invalid
213//! annotations with application defined behavior).
214//!
215//! - (1) `2024-03-02T08:48:00-05:00[u-ca=iso8601][America/New_York]`
216//! - (2) `2024-03-02T08:48:00-05:00[u-ca=iso8601][!u-ca=japanese]`
217//! - (3) `2024-03-02T08:48:00-05:00[u-ca=iso8601][!answer-to-universe=fortytwo]`
218//!
219//! ##### Example 1
220//!
221//! This example shows a Time Zone annotation that is not currently in the correct
222//! order with the key value. When parsing this invalid annotation, `ixdtf`
223//! will attempt to parse the Time Zone annotation as a key-value annotation.
224//!
225//! ```rust
226//! use ixdtf::{parsers::IxdtfParser, ParseError};
227//!
228//! let example_one =
229//!     "2024-03-02T08:48:00-05:00[u-ca=iso8601][America/New_York]";
230//!
231//! let result = IxdtfParser::from_str(example_one).parse();
232//!
233//! assert_eq!(result, Err(ParseError::AnnotationKeyLeadingChar));
234//! ```
235//!
236//! ##### Example 2
237//!
238//! This example shows a duplicate registered key; however, in this case, one
239//! of the registered keys is flagged as critical, which throws an error as
240//! the ixdtf string must be treated as erroneous
241//!
242//! ```rust
243//! use ixdtf::{parsers::IxdtfParser, ParseError};
244//!
245//! let example_two = "2024-03-02T08:48:00-05:00[u-ca=iso8601][!u-ca=japanese]";
246//!
247//! let result = IxdtfParser::from_str(example_two).parse();
248//!
249//! assert_eq!(result, Err(ParseError::CriticalDuplicateCalendar));
250//! ```
251//!
252//! ##### Example 3
253//!
254//! This example shows an unknown key flagged as critical. `ixdtf` will return an
255//! error on an unknown flag being flagged as critical.
256//!
257//! ```rust
258//! use ixdtf::{parsers::IxdtfParser, ParseError};
259//!
260//! let example_three =
261//!     "2024-03-02T08:48:00-05:00[u-ca=iso8601][!answer-to-universe=fortytwo]";
262//!
263//! let result = IxdtfParser::from_str(example_three).parse();
264//!
265//! assert_eq!(result, Err(ParseError::UnrecognizedCritical));
266//! ```
267//!
268//! #### Annotations with Application Defined Behavior
269//!
270//! The below options may be viewed as valid or invalid depending on application defined
271//! behavior. Where user defined behavior might be required, the `ixdtf` crate applies
272//! the logic in the least restrictive interpretation and provides optional callbacks
273//! for the user to define stricter behavior.
274//!
275//! - (1) `2024-03-02T08:48:00-05:00[u-ca=japanese][!u-ca=japanese]`
276//! - (2) `2024-03-02T08:48:00+01:00[America/New_York]`
277//!
278//! ##### Example 1
279//!
280//! This example shows a critical duplicate calendar where the annotation value is identical. RFC 9557 is
281//! ambiguous on whether this should be rejected for inconsistency. `ixdtf` treats these values
282//! as consistent, and, therefore, okay. However, an application may wish to handle this duplicate
283//! critical calendar value as inconsistent (See [Implementing Annotation Handlers](#implementing-annotation-handlers)).
284//!
285//! ##### Example 2
286//!
287//! This example shows an ambiguous Time Zone caused by a misalignment
288//! of the offset and the Time Zone annotation. It is up to the user to handle this ambiguity
289//! between the offset and annotation.
290//!
291//! ```rust
292//! use ixdtf::parsers::{IxdtfParser, records::TimeZoneRecord};
293//!
294//! let example_two = "2024-03-02T08:48:00+01:00[!America/New_York]";
295//!
296//! let result = IxdtfParser::from_str(example_two).parse().unwrap();
297//!
298//! let tz_annotation = result.tz.unwrap();
299//! let offset = result.offset.unwrap().resolve_rfc_9557();
300//!
301//! // The time zone annotation and offset conflict with each other, and must therefore be
302//! // resolved by the user.
303//! assert!(tz_annotation.critical);
304//! assert_eq!(tz_annotation.tz, TimeZoneRecord::Name("America/New_York".as_bytes()));
305//! assert_eq!(offset.hour(), 1);
306//! ```
307//!
308//! #### Implementing Annotation Handlers
309//!
310//! As mentioned in the prior section, there may be times where an application may
311//! need to implement application defined behavior for user defined functionality.
312//! In this instance, `ixdtf` provides a `*_with_annotation_handler` method that
313//! allows to the user to provide a callback.
314//!
315//! A handler is defined as `handler: impl FnMut(Annotation<'a>) -> Option<Annotation<'a>>`
316//! where `ixdtf` provides visibility to an annotation to the user. The call to this callback
317//! occurs prior to the `ixdtf`'s processing of the annotation, and will only occur if the
318//! annotation is provided back to `ixdtf`.
319//!
320//! If the user wishes to ignore any `ixdtf`'s errors, then they may return `None`, which
321//! results in a no-op for that annotation.
322//!
323//! Unless the user’s application has a specific reason to bypass action on an annotation,
324//! such as, custom unknown key handling or superceding a calendar based on it’s critical
325//! flag, it is recommended to return the annotation value.
326//!
327//! ##### Handler Example
328//!
329//! A user may wish to implement a custom key in an annotation set. This can be completed
330//! with custom handler.
331//!
332//! ```rust
333//! use ixdtf::parsers::IxdtfParser;
334//!
335//! let example_with_custom_key = "2024-03-02T08:48:00-05:00[u-ca=iso8601][!answer-to-universe=fortytwo]";
336//!
337//! let mut answer = None;
338//!
339//! let _ = IxdtfParser::from_str(example_with_custom_key).parse_with_annotation_handler(|annotation| {
340//!     if annotation.key == "answer-to-universe".as_bytes() {
341//!         answer.get_or_insert(annotation);
342//!         // Found our value! We don't need `ixdtf` to handle this annotation.
343//!         return None
344//!     }
345//!     // The annotation is not our custom annotation, so we return
346//!     // the value back for regular logic.
347//!     Some(annotation)
348//! }).unwrap();
349//!
350//! let answer = answer.unwrap();
351//!
352//! assert!(answer.critical);
353//! assert_eq!(answer.value, "fortytwo".as_bytes());
354//! ```
355//!
356//! It is worth noting that in the above example the annotation above found is a critically flagged
357//! unknown key. RFC 9557 and `ixdtf` considers unknown critical keys as invalid. However, handlers
358//! allow the user to define any known keys of their own and therefore also handle the logic around
359//! criticality.
360//!
361//! ## Additional grammar resources
362//!
363//! Additional resources for Date and Time string grammar can be found in [RFC3339][rfc3339]
364//! and the [Temporal proposal][temporal-grammar].
365//!
366//! ## Additional Feature
367//!
368//! The `ixdtf` crate also implements an ISO8601 Duration parser (`IsoDurationParser`) that is available under
369//! the `duration` feature flag. The API for `IsoDurationParser` is the same as `IxdtfParser`, but
370//! parses duration strings over date/time strings.
371//!
372//! [rfc9557]: https://datatracker.ietf.org/doc/rfc9557/
373//! [rfc3339]: https://datatracker.ietf.org/doc/html/rfc3339
374//! [temporal-grammar]: https://tc39.es/proposal-temporal/#sec-temporal-iso8601grammar
375
376#![no_std]
377#![cfg_attr(
378    not(test),
379    deny(
380        clippy::indexing_slicing,
381        clippy::unwrap_used,
382        clippy::expect_used,
383        clippy::panic,
384        clippy::exhaustive_structs,
385        clippy::exhaustive_enums,
386        clippy::trivially_copy_pass_by_ref,
387        missing_debug_implementations,
388    )
389)]
390
391mod error;
392pub mod parsers;
393
394extern crate alloc;
395
396pub use error::ParseError;
397
398/// The `ixdtf` crate's Result type.
399pub type ParserResult<T> = Result<T, ParseError>;