icu_time/
ixdtf.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
5use crate::{
6    zone::{iana::IanaParserBorrowed, models, InvalidOffsetError, UtcOffset},
7    DateTime, Time, TimeZoneInfo, ZonedDateTime,
8};
9use core::str::FromStr;
10use icu_calendar::{AnyCalendarKind, AsCalendar, Date, DateError, Iso, RangeError};
11use ixdtf::{
12    encoding::Utf8,
13    parsers::IxdtfParser,
14    records::{
15        DateRecord, IxdtfParseRecord, TimeRecord, TimeZoneAnnotation, TimeZoneRecord,
16        UtcOffsetRecord, UtcOffsetRecordOrZ,
17    },
18    ParseError as Rfc9557ParseError,
19};
20
21/// The error type for parsing RFC 9557 strings.
22#[derive(Debug, PartialEq, displaydoc::Display)]
23#[non_exhaustive]
24pub enum ParseError {
25    /// Syntax error.
26    #[displaydoc("Syntax error in the RFC 9557 string: {0}")]
27    Syntax(Rfc9557ParseError),
28    /// Parsed record is out of valid date range.
29    #[displaydoc("Value out of range: {0}")]
30    Range(RangeError),
31    /// Parsed date and time records were not a valid ISO date.
32    #[displaydoc("Parsed date and time records were not a valid ISO date: {0}")]
33    Date(DateError),
34    /// There were missing fields required to parse component.
35    MissingFields,
36    /// There were two offsets provided that were not consistent with each other.
37    InconsistentTimeUtcOffsets,
38    /// There was an invalid Offset.
39    InvalidOffsetError,
40    /// Parsed fractional digits had excessive precision beyond nanosecond.
41    ExcessivePrecision,
42    /// The set of time zone fields was not expected for the given type.
43    /// For example, if a named time zone was present with offset-only parsing,
44    /// or an offset was present with named-time-zone-only parsing.
45    #[displaydoc("The set of time zone fields was not expected for the given type")]
46    MismatchedTimeZoneFields,
47    /// An unknown calendar was provided.
48    UnknownCalendar,
49    /// Expected a different calendar.
50    #[displaydoc("Expected calendar {0} but found calendar {1}")]
51    MismatchedCalendar(AnyCalendarKind, AnyCalendarKind),
52    /// A timezone calculation is required to interpret this string, which is not supported.
53    ///
54    /// # Example
55    /// ```
56    /// use icu::calendar::Iso;
57    /// use icu::time::{ZonedDateTime, ParseError, zone::IanaParser};
58    ///
59    /// // This timestamp is in UTC, and requires a time zone calculation in order to display a Zurich time.
60    /// assert_eq!(
61    ///     ZonedDateTime::try_lenient_from_str("2024-08-12T12:32:00Z[Europe/Zurich]", Iso, IanaParser::new()).unwrap_err(),
62    ///     ParseError::RequiresCalculation,
63    /// );
64    ///
65    /// // These timestamps are in local time
66    /// ZonedDateTime::try_lenient_from_str("2024-08-12T14:32:00+02:00[Europe/Zurich]", Iso, IanaParser::new()).unwrap();
67    /// ZonedDateTime::try_lenient_from_str("2024-08-12T14:32:00[Europe/Zurich]", Iso, IanaParser::new()).unwrap();
68    /// ```
69    #[displaydoc(
70        "A timezone calculation is required to interpret this string, which is not supported"
71    )]
72    RequiresCalculation,
73}
74
75impl core::error::Error for ParseError {}
76
77impl From<Rfc9557ParseError> for ParseError {
78    fn from(value: Rfc9557ParseError) -> Self {
79        Self::Syntax(value)
80    }
81}
82
83impl From<RangeError> for ParseError {
84    fn from(value: RangeError) -> Self {
85        Self::Range(value)
86    }
87}
88
89impl From<DateError> for ParseError {
90    fn from(value: DateError) -> Self {
91        Self::Date(value)
92    }
93}
94
95impl From<InvalidOffsetError> for ParseError {
96    fn from(_: InvalidOffsetError) -> Self {
97        Self::InvalidOffsetError
98    }
99}
100
101impl From<icu_calendar::ParseError> for ParseError {
102    fn from(value: icu_calendar::ParseError) -> Self {
103        match value {
104            icu_calendar::ParseError::MissingFields => Self::MissingFields,
105            icu_calendar::ParseError::Range(r) => Self::Range(r),
106            icu_calendar::ParseError::Syntax(s) => Self::Syntax(s),
107            icu_calendar::ParseError::UnknownCalendar => Self::UnknownCalendar,
108            _ => unreachable!(),
109        }
110    }
111}
112
113impl UtcOffset {
114    fn try_from_utc_offset_record(record: UtcOffsetRecord) -> Result<Self, ParseError> {
115        let hour_seconds = i32::from(record.hour()) * 3600;
116        let minute_seconds = i32::from(record.minute()) * 60;
117        Self::try_from_seconds(
118            i32::from(record.sign() as i8)
119                * (hour_seconds + minute_seconds + i32::from(record.second().unwrap_or(0))),
120        )
121        .map_err(Into::into)
122    }
123}
124
125struct Intermediate<'a> {
126    offset: Option<UtcOffsetRecord>,
127    is_z: bool,
128    iana_identifier: Option<&'a [u8]>,
129    date: DateRecord,
130    time: TimeRecord,
131}
132
133impl<'a> Intermediate<'a> {
134    fn try_from_ixdtf_record(
135        ixdtf_record: &'a IxdtfParseRecord<'_, Utf8>,
136    ) -> Result<Self, ParseError> {
137        let (offset, is_z, iana_identifier) = match ixdtf_record {
138            // empty
139            IxdtfParseRecord {
140                offset: None,
141                tz: None,
142                ..
143            } => (None, false, None),
144            // -0800
145            IxdtfParseRecord {
146                offset: Some(UtcOffsetRecordOrZ::Offset(offset)),
147                tz: None,
148                ..
149            } => (Some(*offset), false, None),
150            // Z
151            IxdtfParseRecord {
152                offset: Some(UtcOffsetRecordOrZ::Z),
153                tz: None,
154                ..
155            } => (None, true, None),
156            // [-0800]
157            IxdtfParseRecord {
158                offset: None,
159                tz:
160                    Some(TimeZoneAnnotation {
161                        tz: TimeZoneRecord::Offset(offset),
162                        ..
163                    }),
164                ..
165            } => (Some(UtcOffsetRecord::MinutePrecision(*offset)), false, None),
166            // -0800[-0800]
167            IxdtfParseRecord {
168                offset: Some(UtcOffsetRecordOrZ::Offset(offset)),
169                tz:
170                    Some(TimeZoneAnnotation {
171                        tz: TimeZoneRecord::Offset(offset1),
172                        ..
173                    }),
174                ..
175            } => {
176                let annotation_offset = UtcOffsetRecord::MinutePrecision(*offset1);
177                if offset != &annotation_offset {
178                    return Err(ParseError::InconsistentTimeUtcOffsets);
179                }
180                (Some(*offset), false, None)
181            }
182            // -0800[America/Los_Angeles]
183            IxdtfParseRecord {
184                offset: Some(UtcOffsetRecordOrZ::Offset(offset)),
185                tz:
186                    Some(TimeZoneAnnotation {
187                        tz: TimeZoneRecord::Name(iana_identifier),
188                        ..
189                    }),
190                ..
191            } => (Some(*offset), false, Some(*iana_identifier)),
192            // Z[-0800]
193            IxdtfParseRecord {
194                offset: Some(UtcOffsetRecordOrZ::Z),
195                tz:
196                    Some(TimeZoneAnnotation {
197                        tz: TimeZoneRecord::Offset(offset),
198                        ..
199                    }),
200                ..
201            } => (Some(UtcOffsetRecord::MinutePrecision(*offset)), true, None),
202            // Z[America/Los_Angeles]
203            IxdtfParseRecord {
204                offset: Some(UtcOffsetRecordOrZ::Z),
205                tz:
206                    Some(TimeZoneAnnotation {
207                        tz: TimeZoneRecord::Name(iana_identifier),
208                        ..
209                    }),
210                ..
211            } => (None, true, Some(*iana_identifier)),
212            // [America/Los_Angeles]
213            IxdtfParseRecord {
214                offset: None,
215                tz:
216                    Some(TimeZoneAnnotation {
217                        tz: TimeZoneRecord::Name(iana_identifier),
218                        ..
219                    }),
220                ..
221            } => (None, false, Some(*iana_identifier)),
222            // non_exhaustive match: maybe something like [u-tz=uslax] in the future
223            IxdtfParseRecord {
224                tz: Some(TimeZoneAnnotation { tz, .. }),
225                ..
226            } => {
227                debug_assert!(false, "unexpected TimeZoneRecord: {tz:?}");
228                (None, false, None)
229            }
230        };
231        let IxdtfParseRecord {
232            date: Some(date),
233            time: Some(time),
234            ..
235        } = *ixdtf_record
236        else {
237            // Date or time was missing
238            return Err(ParseError::MismatchedTimeZoneFields);
239        };
240        Ok(Self {
241            offset,
242            is_z,
243            iana_identifier,
244            date,
245            time,
246        })
247    }
248
249    fn offset_only(self) -> Result<UtcOffset, ParseError> {
250        let None = self.iana_identifier else {
251            return Err(ParseError::MismatchedTimeZoneFields);
252        };
253        if self.is_z {
254            if let Some(offset) = self.offset {
255                if offset != UtcOffsetRecord::zero() {
256                    return Err(ParseError::RequiresCalculation);
257                }
258            }
259            return Ok(UtcOffset::zero());
260        }
261        let Some(offset) = self.offset else {
262            return Err(ParseError::MismatchedTimeZoneFields);
263        };
264        UtcOffset::try_from_utc_offset_record(offset)
265    }
266
267    fn location_only(
268        self,
269        iana_parser: IanaParserBorrowed<'_>,
270    ) -> Result<TimeZoneInfo<models::AtTime>, ParseError> {
271        let None = self.offset else {
272            return Err(ParseError::MismatchedTimeZoneFields);
273        };
274        let Some(iana_identifier) = self.iana_identifier else {
275            if self.is_z {
276                return Err(ParseError::RequiresCalculation);
277            }
278            return Err(ParseError::MismatchedTimeZoneFields);
279        };
280        let id = iana_parser.parse_from_utf8(iana_identifier);
281        let date = Date::<Iso>::try_new_iso(self.date.year, self.date.month, self.date.day)?;
282        let time = Time::try_from_time_record(&self.time)?;
283        Ok(id
284            .with_offset(None)
285            .at_date_time_iso(DateTime { date, time }))
286    }
287
288    fn lenient(
289        self,
290        iana_parser: IanaParserBorrowed<'_>,
291    ) -> Result<TimeZoneInfo<models::AtTime>, ParseError> {
292        let mut zone = match self.iana_identifier {
293            Some(iana_identifier) => {
294                if self.is_z {
295                    return Err(ParseError::RequiresCalculation);
296                }
297                iana_parser
298                    .parse_from_utf8(iana_identifier)
299                    .with_offset(None)
300            }
301            None if self.is_z => TimeZoneInfo::utc(),
302            None => TimeZoneInfo::unknown(),
303        };
304
305        if let Some(offset) = self.offset {
306            let offset = UtcOffset::try_from_utc_offset_record(offset)?;
307            if zone.offset().is_some_and(|i| i != offset) {
308                return Err(ParseError::RequiresCalculation);
309            }
310            zone = zone.id().with_offset(Some(offset));
311        }
312        let date = Date::<Iso>::try_new_iso(self.date.year, self.date.month, self.date.day)?;
313        let time = Time::try_from_time_record(&self.time)?;
314        Ok(zone.at_date_time_iso(DateTime { date, time }))
315    }
316
317    #[allow(deprecated)]
318    fn all(
319        self,
320        iana_parser: IanaParserBorrowed<'_>,
321    ) -> Result<TimeZoneInfo<models::AtTime>, ParseError> {
322        let Some(offset) = self.offset else {
323            return Err(ParseError::MismatchedTimeZoneFields);
324        };
325        let Some(iana_identifier) = self.iana_identifier else {
326            return Err(ParseError::MismatchedTimeZoneFields);
327        };
328        let time_zone_id = iana_parser.parse_from_utf8(iana_identifier);
329        let date = Date::try_new_iso(self.date.year, self.date.month, self.date.day)?;
330        let time = Time::try_from_time_record(&self.time)?;
331        let offset = UtcOffset::try_from_utc_offset_record(offset)?;
332        Ok(time_zone_id
333            .with_offset(Some(offset))
334            .at_date_time_iso(DateTime { date, time }))
335    }
336
337    #[allow(deprecated)]
338    fn full(
339        self,
340        iana_parser: IanaParserBorrowed<'_>,
341        offset_calculator: crate::zone::VariantOffsetsCalculatorBorrowed,
342    ) -> Result<TimeZoneInfo<models::Full>, ParseError> {
343        let Some(offset) = self.offset else {
344            return Err(ParseError::MismatchedTimeZoneFields);
345        };
346        let Some(iana_identifier) = self.iana_identifier else {
347            return Err(ParseError::MismatchedTimeZoneFields);
348        };
349        let time_zone_id = iana_parser.parse_from_utf8(iana_identifier);
350        let date = Date::try_new_iso(self.date.year, self.date.month, self.date.day)?;
351        let time = Time::try_from_time_record(&self.time)?;
352        let offset = UtcOffset::try_from_utc_offset_record(offset)?;
353        Ok(time_zone_id
354            .with_offset(Some(offset))
355            .at_date_time_iso(DateTime { date, time })
356            .infer_variant(offset_calculator))
357    }
358}
359
360impl<A: AsCalendar> ZonedDateTime<A, UtcOffset> {
361    /// Create a [`ZonedDateTime`] in any calendar from an RFC 9557 string.
362    ///
363    /// Returns an error if the string has a calendar annotation that does not
364    /// match the calendar argument, unless the argument is [`Iso`].
365    ///
366    /// This function is "strict": the string should have only an offset and no named time zone.
367    pub fn try_offset_only_from_str(rfc_9557_str: &str, calendar: A) -> Result<Self, ParseError> {
368        Self::try_offset_only_from_utf8(rfc_9557_str.as_bytes(), calendar)
369    }
370
371    /// Create a [`ZonedDateTime`] in any calendar from RFC 9557 syntax UTF-8 bytes.
372    ///
373    /// See [`Self:try_offset_only_from_str`](Self::try_offset_only_from_str).
374    pub fn try_offset_only_from_utf8(rfc_9557_str: &[u8], calendar: A) -> Result<Self, ParseError> {
375        let ixdtf_record = IxdtfParser::from_utf8(rfc_9557_str).parse()?;
376        let date = Date::try_from_ixdtf_record(&ixdtf_record, calendar)?;
377        let time = Time::try_from_ixdtf_record(&ixdtf_record)?;
378        let zone = Intermediate::try_from_ixdtf_record(&ixdtf_record)?.offset_only()?;
379        Ok(ZonedDateTime { date, time, zone })
380    }
381}
382
383impl<A: AsCalendar> ZonedDateTime<A, TimeZoneInfo<models::AtTime>> {
384    /// Create a [`ZonedDateTime`] in any calendar from an RFC 9557 string.
385    ///
386    /// Returns an error if the string has a calendar annotation that does not
387    /// match the calendar argument, unless the argument is [`Iso`].
388    ///
389    /// This function is "strict": the string should have only a named time zone and no offset.
390    pub fn try_location_only_from_str(
391        rfc_9557_str: &str,
392        calendar: A,
393        iana_parser: IanaParserBorrowed,
394    ) -> Result<Self, ParseError> {
395        Self::try_location_only_from_utf8(rfc_9557_str.as_bytes(), calendar, iana_parser)
396    }
397
398    /// Create a [`ZonedDateTime`] in any calendar from RFC 9557 UTF-8 bytes.
399    ///
400    /// See [`Self::try_location_only_from_str`].
401    pub fn try_location_only_from_utf8(
402        rfc_9557_str: &[u8],
403        calendar: A,
404        iana_parser: IanaParserBorrowed,
405    ) -> Result<Self, ParseError> {
406        let ixdtf_record = IxdtfParser::from_utf8(rfc_9557_str).parse()?;
407        let date = Date::try_from_ixdtf_record(&ixdtf_record, calendar)?;
408        let time = Time::try_from_ixdtf_record(&ixdtf_record)?;
409        let zone =
410            Intermediate::try_from_ixdtf_record(&ixdtf_record)?.location_only(iana_parser)?;
411        Ok(ZonedDateTime { date, time, zone })
412    }
413
414    /// Create a [`ZonedDateTime`] in any calendar from an RFC 9557 string.
415    ///
416    /// Returns an error if the string has a calendar annotation that does not
417    /// match the calendar argument, unless the argument is [`Iso`].
418    ///
419    /// This function is "lenient": the string can have an offset, and named time zone, both, or
420    /// neither. If the named time zone is missing, it is returned as Etc/Unknown.
421    pub fn try_lenient_from_str(
422        rfc_9557_str: &str,
423        calendar: A,
424        iana_parser: IanaParserBorrowed,
425    ) -> Result<Self, ParseError> {
426        Self::try_lenient_from_utf8(rfc_9557_str.as_bytes(), calendar, iana_parser)
427    }
428
429    /// Create a [`ZonedDateTime`] in any calendar from RFC 9557 UTF-8 bytes.
430    ///
431    /// See [`Self::try_lenient_from_str`].
432    pub fn try_lenient_from_utf8(
433        rfc_9557_str: &[u8],
434        calendar: A,
435        iana_parser: IanaParserBorrowed,
436    ) -> Result<Self, ParseError> {
437        let ixdtf_record = IxdtfParser::from_utf8(rfc_9557_str).parse()?;
438        let date = Date::try_from_ixdtf_record(&ixdtf_record, calendar)?;
439        let time = Time::try_from_ixdtf_record(&ixdtf_record)?;
440        let zone = Intermediate::try_from_ixdtf_record(&ixdtf_record)?.lenient(iana_parser)?;
441        Ok(ZonedDateTime { date, time, zone })
442    }
443
444    /// Create a [`ZonedDateTime`] in any calendar from an RFC 9557 string.
445    ///
446    /// Returns an error if the string has a calendar annotation that does not
447    /// match the calendar argument, unless the argument is [`Iso`].
448    ///
449    /// The string should have both an offset and a named time zone.
450    ///
451    /// For more information on RFC 9557, see the [`ixdtf`] crate.
452    ///
453    /// # Examples
454    ///
455    /// Basic usage:
456    ///
457    /// ```
458    /// use icu::calendar::cal::Hebrew;
459    /// use icu::locale::subtags::subtag;
460    /// use icu::time::{
461    ///     zone::{IanaParser, TimeZoneVariant, UtcOffset},
462    ///     TimeZone, TimeZoneInfo, ZonedDateTime,
463    /// };
464    ///
465    /// let zoneddatetime = ZonedDateTime::try_strict_from_str(
466    ///     "2024-08-08T12:08:19-05:00[America/Chicago][u-ca=hebrew]",
467    ///     Hebrew,
468    ///     IanaParser::new(),
469    /// )
470    /// .unwrap();
471    ///
472    /// assert_eq!(zoneddatetime.date.extended_year(), 5784);
473    /// assert_eq!(
474    ///     zoneddatetime.date.month().standard_code,
475    ///     icu::calendar::types::MonthCode(tinystr::tinystr!(4, "M11"))
476    /// );
477    /// assert_eq!(zoneddatetime.date.day_of_month().0, 4);
478    ///
479    /// assert_eq!(zoneddatetime.time.hour.number(), 12);
480    /// assert_eq!(zoneddatetime.time.minute.number(), 8);
481    /// assert_eq!(zoneddatetime.time.second.number(), 19);
482    /// assert_eq!(zoneddatetime.time.subsecond.number(), 0);
483    /// assert_eq!(zoneddatetime.zone.id(), TimeZone(subtag!("uschi")));
484    /// assert_eq!(
485    ///     zoneddatetime.zone.offset(),
486    ///     Some(UtcOffset::try_from_seconds(-18000).unwrap())
487    /// );
488    /// let _ = zoneddatetime.zone.zone_name_timestamp();
489    /// ```
490    ///
491    /// An RFC 9557 string can provide a time zone in two parts: the DateTime UTC Offset or the Time Zone
492    /// Annotation. A DateTime UTC Offset is the time offset as laid out by RFC 3339; meanwhile, the Time
493    /// Zone Annotation is the annotation laid out by RFC 9557 and is defined as a UTC offset or IANA Time
494    /// Zone identifier.
495    ///
496    /// ## DateTime UTC Offsets
497    ///
498    /// Below is an example of a time zone from a DateTime UTC Offset. The syntax here is familiar to a RFC 3339
499    /// DateTime string.
500    ///
501    /// ```
502    /// use icu::calendar::Iso;
503    /// use icu::time::{zone::UtcOffset, TimeZoneInfo, ZonedDateTime};
504    ///
505    /// let tz_from_offset = ZonedDateTime::try_offset_only_from_str(
506    ///     "2024-08-08T12:08:19-05:00",
507    ///     Iso,
508    /// )
509    /// .unwrap();
510    ///
511    /// assert_eq!(
512    ///     tz_from_offset.zone,
513    ///     UtcOffset::try_from_seconds(-18000).unwrap()
514    /// );
515    /// ```
516    ///
517    /// ## Time Zone Annotations
518    ///
519    /// Below is an example of a time zone being provided by a time zone annotation.
520    ///
521    /// ```
522    /// use icu::calendar::Iso;
523    /// use icu::locale::subtags::subtag;
524    /// use icu::time::{
525    ///     zone::{IanaParser, TimeZoneVariant, UtcOffset},
526    ///     TimeZone, TimeZoneInfo, ZonedDateTime,
527    /// };
528    ///
529    /// let tz_from_offset_annotation = ZonedDateTime::try_offset_only_from_str(
530    ///     "2024-08-08T12:08:19[-05:00]",
531    ///     Iso,
532    /// )
533    /// .unwrap();
534    /// let tz_from_iana_annotation = ZonedDateTime::try_location_only_from_str(
535    ///     "2024-08-08T12:08:19[America/Chicago]",
536    ///     Iso,
537    ///     IanaParser::new(),
538    /// )
539    /// .unwrap();
540    ///
541    /// assert_eq!(
542    ///     tz_from_offset_annotation.zone,
543    ///     UtcOffset::try_from_seconds(-18000).unwrap()
544    /// );
545    ///
546    /// assert_eq!(
547    ///     tz_from_iana_annotation.zone.id(),
548    ///     TimeZone(subtag!("uschi"))
549    /// );
550    /// assert_eq!(tz_from_iana_annotation.zone.offset(), None);
551    /// ```
552    ///
553    /// ## UTC Offset and time zone annotations.
554    ///
555    /// An RFC 9557 string may contain both a UTC Offset and time zone annotation. This is fine as long as
556    /// the time zone parts can be deemed as inconsistent or unknown consistency.
557    ///
558    /// ### DateTime UTC offset with UTC Offset annotation.
559    ///
560    /// These annotations must always be consistent as they should be either the same value or are inconsistent.
561    ///
562    /// ```
563    /// use icu::calendar::Iso;
564    /// use icu::time::{
565    ///     zone::UtcOffset, ParseError, TimeZone, TimeZoneInfo, ZonedDateTime,
566    /// };
567    /// use tinystr::tinystr;
568    ///
569    /// let consistent_tz_from_both = ZonedDateTime::try_offset_only_from_str(
570    ///     "2024-08-08T12:08:19-05:00[-05:00]",
571    ///     Iso,
572    /// )
573    /// .unwrap();
574    ///
575    /// assert_eq!(
576    ///     consistent_tz_from_both.zone,
577    ///     UtcOffset::try_from_seconds(-18000).unwrap()
578    /// );
579    ///
580    /// let inconsistent_tz_from_both = ZonedDateTime::try_offset_only_from_str(
581    ///     "2024-08-08T12:08:19-05:00[+05:00]",
582    ///     Iso,
583    /// );
584    ///
585    /// assert!(matches!(
586    ///     inconsistent_tz_from_both,
587    ///     Err(ParseError::InconsistentTimeUtcOffsets)
588    /// ));
589    /// ```
590    pub fn try_strict_from_str(
591        rfc_9557_str: &str,
592        calendar: A,
593        iana_parser: IanaParserBorrowed,
594    ) -> Result<Self, ParseError> {
595        Self::try_strict_from_utf8(rfc_9557_str.as_bytes(), calendar, iana_parser)
596    }
597
598    /// Create a [`ZonedDateTime`] in any calendar from RFC 9557 UTF-8 bytes.
599    ///
600    /// See [`Self::try_strict_from_str`].
601    pub fn try_strict_from_utf8(
602        rfc_9557_str: &[u8],
603        calendar: A,
604        iana_parser: IanaParserBorrowed,
605    ) -> Result<Self, ParseError> {
606        let ixdtf_record = IxdtfParser::from_utf8(rfc_9557_str).parse()?;
607        let date = Date::try_from_ixdtf_record(&ixdtf_record, calendar)?;
608        let time = Time::try_from_ixdtf_record(&ixdtf_record)?;
609        let zone = Intermediate::try_from_ixdtf_record(&ixdtf_record)?.all(iana_parser)?;
610
611        Ok(ZonedDateTime { date, time, zone })
612    }
613}
614
615#[allow(deprecated)]
616impl<A: AsCalendar> ZonedDateTime<A, TimeZoneInfo<models::Full>> {
617    /// Create a [`ZonedDateTime`] in any calendar from an RFC 9557 string.
618    #[deprecated(since = "2.1.0", note = "use `try_strict_from_str`")]
619    pub fn try_full_from_str(
620        rfc_9557_str: &str,
621        calendar: A,
622        iana_parser: IanaParserBorrowed,
623        offset_calculator: crate::zone::VariantOffsetsCalculatorBorrowed,
624    ) -> Result<Self, ParseError> {
625        Self::try_full_from_utf8(
626            rfc_9557_str.as_bytes(),
627            calendar,
628            iana_parser,
629            offset_calculator,
630        )
631    }
632
633    /// Create a [`ZonedDateTime`] in any calendar from RFC 9557 UTF-8 bytes.
634    ///
635    /// See [`Self::try_full_from_str`].
636    #[deprecated(since = "2.1.0", note = "use `try_strict_from_utf8`")]
637    pub fn try_full_from_utf8(
638        rfc_9557_str: &[u8],
639        calendar: A,
640        iana_parser: IanaParserBorrowed,
641        offset_calculator: crate::zone::VariantOffsetsCalculatorBorrowed,
642    ) -> Result<Self, ParseError> {
643        let ixdtf_record = IxdtfParser::from_utf8(rfc_9557_str).parse()?;
644        let date = Date::try_from_ixdtf_record(&ixdtf_record, calendar)?;
645        let time = Time::try_from_ixdtf_record(&ixdtf_record)?;
646        let zone = Intermediate::try_from_ixdtf_record(&ixdtf_record)?
647            .full(iana_parser, offset_calculator)?;
648
649        Ok(ZonedDateTime { date, time, zone })
650    }
651}
652
653impl FromStr for DateTime<Iso> {
654    type Err = ParseError;
655    fn from_str(rfc_9557_str: &str) -> Result<Self, Self::Err> {
656        Self::try_from_str(rfc_9557_str, Iso)
657    }
658}
659
660impl<A: AsCalendar> DateTime<A> {
661    /// Creates a [`DateTime`] in any calendar from an RFC 9557 string.
662    ///
663    /// Returns an error if the string has a calendar annotation that does not
664    /// match the calendar argument, unless the argument is [`Iso`].
665    ///
666    /// ✨ *Enabled with the `ixdtf` Cargo feature.*
667    ///
668    /// # Examples
669    ///
670    /// ```
671    /// use icu::calendar::cal::Hebrew;
672    /// use icu::time::DateTime;
673    ///
674    /// let datetime =
675    ///     DateTime::try_from_str("2024-07-17T16:01:17.045[u-ca=hebrew]", Hebrew)
676    ///         .unwrap();
677    ///
678    /// assert_eq!(datetime.date.era_year().year, 5784);
679    /// assert_eq!(
680    ///     datetime.date.month().standard_code,
681    ///     icu::calendar::types::MonthCode(tinystr::tinystr!(4, "M10"))
682    /// );
683    /// assert_eq!(datetime.date.day_of_month().0, 11);
684    ///
685    /// assert_eq!(datetime.time.hour.number(), 16);
686    /// assert_eq!(datetime.time.minute.number(), 1);
687    /// assert_eq!(datetime.time.second.number(), 17);
688    /// assert_eq!(datetime.time.subsecond.number(), 45000000);
689    /// ```
690    pub fn try_from_str(rfc_9557_str: &str, calendar: A) -> Result<Self, ParseError> {
691        Self::try_from_utf8(rfc_9557_str.as_bytes(), calendar)
692    }
693
694    /// Creates a [`DateTime`] in any calendar from an RFC 9557 string.
695    ///
696    /// See [`Self::try_from_str()`].
697    ///
698    /// ✨ *Enabled with the `ixdtf` Cargo feature.*
699    pub fn try_from_utf8(rfc_9557_str: &[u8], calendar: A) -> Result<Self, ParseError> {
700        let ixdtf_record = IxdtfParser::from_utf8(rfc_9557_str).parse()?;
701        let date = Date::try_from_ixdtf_record(&ixdtf_record, calendar)?;
702        let time = Time::try_from_ixdtf_record(&ixdtf_record)?;
703        Ok(Self { date, time })
704    }
705}
706
707impl Time {
708    /// Creates a [`Time`] from an RFC 9557 string of a time.
709    ///
710    /// Does not support parsing an RFC 9557 string with a date and time; for that, use [`DateTime`].
711    ///
712    /// ✨ *Enabled with the `ixdtf` Cargo feature.*
713    ///
714    /// # Examples
715    ///
716    /// ```
717    /// use icu::time::Time;
718    ///
719    /// let time = Time::try_from_str("16:01:17.045").unwrap();
720    ///
721    /// assert_eq!(time.hour.number(), 16);
722    /// assert_eq!(time.minute.number(), 1);
723    /// assert_eq!(time.second.number(), 17);
724    /// assert_eq!(time.subsecond.number(), 45000000);
725    /// ```
726    pub fn try_from_str(rfc_9557_str: &str) -> Result<Self, ParseError> {
727        Self::try_from_utf8(rfc_9557_str.as_bytes())
728    }
729
730    /// Creates a [`Time`] in the ISO-8601 calendar from an RFC 9557 string.
731    ///
732    /// ✨ *Enabled with the `ixdtf` Cargo feature.*
733    ///
734    /// See [`Self::try_from_str()`].
735    pub fn try_from_utf8(rfc_9557_str: &[u8]) -> Result<Self, ParseError> {
736        let ixdtf_record = IxdtfParser::from_utf8(rfc_9557_str).parse_time()?;
737        Self::try_from_ixdtf_record(&ixdtf_record)
738    }
739
740    fn try_from_ixdtf_record(
741        ixdtf_record: &IxdtfParseRecord<'_, Utf8>,
742    ) -> Result<Self, ParseError> {
743        let time_record = ixdtf_record.time.ok_or(ParseError::MissingFields)?;
744        Self::try_from_time_record(&time_record)
745    }
746
747    fn try_from_time_record(time_record: &TimeRecord) -> Result<Self, ParseError> {
748        let nanosecond = time_record
749            .fraction
750            .map(|fraction| {
751                fraction
752                    .to_nanoseconds()
753                    .ok_or(ParseError::ExcessivePrecision)
754            })
755            .transpose()?
756            .unwrap_or_default();
757
758        Ok(Self::try_new(
759            time_record.hour,
760            time_record.minute,
761            time_record.second,
762            nanosecond,
763        )?)
764    }
765}
766
767impl FromStr for Time {
768    type Err = ParseError;
769    fn from_str(rfc_9557_str: &str) -> Result<Self, Self::Err> {
770        Self::try_from_str(rfc_9557_str)
771    }
772}
773
774#[cfg(test)]
775mod test {
776    use super::*;
777    use crate::TimeZone;
778
779    #[test]
780    fn max_possible_rfc_9557_utc_offset() {
781        assert_eq!(
782            ZonedDateTime::try_offset_only_from_str("2024-08-08T12:08:19+23:59:59.999999999", Iso)
783                .unwrap_err(),
784            ParseError::InvalidOffsetError
785        );
786    }
787
788    #[test]
789    fn zone_calculations() {
790        ZonedDateTime::try_offset_only_from_str("2024-08-08T12:08:19Z", Iso).unwrap();
791        assert_eq!(
792            ZonedDateTime::try_offset_only_from_str("2024-08-08T12:08:19Z[+08:00]", Iso)
793                .unwrap_err(),
794            ParseError::RequiresCalculation
795        );
796        assert_eq!(
797            ZonedDateTime::try_offset_only_from_str("2024-08-08T12:08:19Z[Europe/Zurich]", Iso)
798                .unwrap_err(),
799            ParseError::MismatchedTimeZoneFields
800        );
801    }
802
803    #[test]
804    fn future_zone() {
805        let result = ZonedDateTime::try_location_only_from_str(
806            "2024-08-08T12:08:19[Future/Zone]",
807            Iso,
808            IanaParserBorrowed::new(),
809        )
810        .unwrap();
811        assert_eq!(result.zone.id(), TimeZone::UNKNOWN);
812        assert_eq!(result.zone.offset(), None);
813    }
814
815    #[test]
816    fn lax() {
817        ZonedDateTime::try_location_only_from_str(
818            "2024-10-18T15:44[America/Los_Angeles]",
819            icu_calendar::cal::Gregorian,
820            IanaParserBorrowed::new(),
821        )
822        .unwrap();
823    }
824}