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