Skip to main content

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