icu_calendar/
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 core::str::FromStr;
6
7use crate::{AsCalendar, Calendar, Date, Iso, RangeError};
8use icu_locale_core::preferences::extensions::unicode::keywords::CalendarAlgorithm;
9use ixdtf::parsers::records::IxdtfParseRecord;
10use ixdtf::parsers::IxdtfParser;
11use ixdtf::ParseError as Rfc9557Error;
12
13/// An error returned from parsing an RFC 9557 string to an `icu_calendar` type.
14#[derive(Debug, displaydoc::Display)]
15#[non_exhaustive]
16pub enum ParseError {
17    /// Syntax error.
18    #[displaydoc("Syntax error in the RFC 9557 string: {0}")]
19    Syntax(Rfc9557Error),
20    /// Value is out of range.
21    #[displaydoc("Value out of range: {0}")]
22    Range(RangeError),
23    /// The RFC 9557 string is missing fields required for parsing into the chosen type.
24    MissingFields,
25    /// The RFC 9557 string specifies an unknown calendar.
26    UnknownCalendar,
27    /// Expected a different calendar.
28    #[displaydoc("Expected calendar {0:?} but found calendar {1:?}")]
29    MismatchedCalendar(CalendarAlgorithm, CalendarAlgorithm),
30}
31
32impl From<RangeError> for ParseError {
33    fn from(value: RangeError) -> Self {
34        Self::Range(value)
35    }
36}
37
38impl From<Rfc9557Error> for ParseError {
39    fn from(value: Rfc9557Error) -> Self {
40        Self::Syntax(value)
41    }
42}
43
44impl FromStr for Date<Iso> {
45    type Err = ParseError;
46    fn from_str(rfc_9557_str: &str) -> Result<Self, Self::Err> {
47        Self::try_from_str(rfc_9557_str, Iso)
48    }
49}
50
51impl<A: AsCalendar> Date<A> {
52    /// Creates a [`Date`] in the given calendar from an RFC 9557 string.
53    ///
54    /// Returns an error if the string has a calendar annotation that does not
55    /// match the calendar argument, unless the argument is [`Iso`].
56    ///
57    /// ✨ *Enabled with the `ixdtf` Cargo feature.*
58    ///
59    /// # Examples
60    ///
61    /// ```
62    /// use icu::calendar::{Date, Gregorian};
63    ///
64    /// let date = Date::try_from_str("2024-07-17", Gregorian).unwrap();
65    /// let date =
66    ///     Date::try_from_str("2024-07-17[u-ca=gregory]", Gregorian).unwrap();
67    /// let _ =
68    ///     Date::try_from_str("2024-07-17[u-ca=hebrew]", Gregorian).unwrap_err();
69    ///
70    /// assert_eq!(date.era_year().year, 2024);
71    /// assert_eq!(
72    ///     date.month().standard_code,
73    ///     icu::calendar::types::MonthCode(tinystr::tinystr!(4, "M07"))
74    /// );
75    /// assert_eq!(date.day_of_month().0, 17);
76    /// ```
77    pub fn try_from_str(rfc_9557_str: &str, calendar: A) -> Result<Self, ParseError> {
78        Self::try_from_utf8(rfc_9557_str.as_bytes(), calendar)
79    }
80
81    /// Creates a [`Date`] in the given calendar from an RFC 9557 string.
82    ///
83    /// Returns an error if the string has a calendar annotation that does not
84    /// match the calendar argument.
85    ///
86    /// See [`Self::try_from_str()`].
87    ///
88    /// ✨ *Enabled with the `ixdtf` Cargo feature.*
89    pub fn try_from_utf8(rfc_9557_str: &[u8], calendar: A) -> Result<Self, ParseError> {
90        let ixdtf_record = IxdtfParser::from_utf8(rfc_9557_str).parse()?;
91        Self::try_from_ixdtf_record(&ixdtf_record, calendar)
92    }
93
94    #[doc(hidden)]
95    pub fn try_from_ixdtf_record(
96        ixdtf_record: &IxdtfParseRecord,
97        calendar: A,
98    ) -> Result<Self, ParseError> {
99        let date_record = ixdtf_record.date.ok_or(ParseError::MissingFields)?;
100        let iso = Date::try_new_iso(date_record.year, date_record.month, date_record.day)?;
101
102        if let Some(ixdtf_calendar) = ixdtf_record.calendar {
103            if let Some(expected_calendar) = calendar.as_calendar().calendar_algorithm() {
104                if let Some(parsed_calendar) =
105                    icu_locale_core::extensions::unicode::Value::try_from_utf8(ixdtf_calendar)
106                        .ok()
107                        .and_then(|v| CalendarAlgorithm::try_from(&v).ok())
108                {
109                    if parsed_calendar != expected_calendar {
110                        return Err(ParseError::MismatchedCalendar(
111                            expected_calendar,
112                            parsed_calendar,
113                        ));
114                    }
115                }
116            }
117        }
118        Ok(iso.to_calendar(calendar))
119    }
120}