1use core::str::FromStr;
6
7use crate::cal::abstract_gregorian::AbstractGregorianYear;
8use crate::cal::iso::IsoDateInner;
9use crate::calendar_arithmetic::{ArithmeticDate, VALID_RD_RANGE};
10use crate::types::RataDie;
11use crate::{AsCalendar, Calendar, Date, Iso, RangeError};
12use calendrical_calculations::gregorian::fixed_from_gregorian;
13use icu_locale_core::preferences::extensions::unicode::keywords::CalendarAlgorithm;
14use ixdtf::ParseError as Rfc9557Error;
15use ixdtf::encoding::Utf8;
16use ixdtf::parsers::IxdtfParser;
17use ixdtf::records::IxdtfParseRecord;
18
19#[derive(Debug, displaydoc::Display)]
21#[non_exhaustive]
22pub enum ParseError {
23 #[displaydoc("Syntax error in the RFC 9557 string: {0}")]
25 Syntax(Rfc9557Error),
26 #[displaydoc("Value out of range: {0}")]
28 Range(RangeError),
29 MissingFields,
31 UnknownCalendar,
33 #[displaydoc("Expected calendar {0:?} but found calendar {1:?}")]
35 MismatchedCalendar(CalendarAlgorithm, CalendarAlgorithm),
36}
37
38impl From<RangeError> for ParseError {
39 fn from(value: RangeError) -> Self {
40 Self::Range(value)
41 }
42}
43
44impl From<Rfc9557Error> for ParseError {
45 fn from(value: Rfc9557Error) -> Self {
46 Self::Syntax(value)
47 }
48}
49
50impl FromStr for Date<Iso> {
51 type Err = ParseError;
52 fn from_str(rfc_9557_str: &str) -> Result<Self, Self::Err> {
53 Self::try_from_str(rfc_9557_str, Iso)
54 }
55}
56
57impl<A: AsCalendar> Date<A> {
58 pub fn try_from_str(rfc_9557_str: &str, calendar: A) -> Result<Self, ParseError> {
81 Self::try_from_utf8(rfc_9557_str.as_bytes(), calendar)
82 }
83
84 pub fn try_from_utf8(rfc_9557_str: &[u8], calendar: A) -> Result<Self, ParseError> {
93 let ixdtf_record = IxdtfParser::from_utf8(rfc_9557_str).parse()?;
94 Self::try_from_ixdtf_record(&ixdtf_record, calendar)
95 }
96
97 #[doc(hidden)]
98 pub fn try_from_ixdtf_record(
99 ixdtf_record: &IxdtfParseRecord<'_, Utf8>,
100 calendar: A,
101 ) -> Result<Self, ParseError> {
102 let inner = Self::try_inner_from_ixdtf_record(
103 ixdtf_record,
104 calendar.as_calendar().calendar_algorithm(),
105 )?;
106
107 Ok(Date::from_raw(inner, Iso).to_calendar(calendar))
109 }
110
111 #[doc(hidden)]
114 pub fn try_from_ixdtf_record_with_rd(
115 ixdtf_record: &IxdtfParseRecord<'_, Utf8>,
116 calendar: A,
117 ) -> Result<(Self, RataDie), ParseError> {
118 let inner = Self::try_inner_from_ixdtf_record(
119 ixdtf_record,
120 calendar.as_calendar().calendar_algorithm(),
121 )?;
122
123 let rd = Iso.to_rata_die(&inner);
124
125 let c = calendar.as_calendar();
126 let inner = if c.has_cheap_iso_conversion() {
127 c.from_iso(inner)
129 } else {
130 c.from_rata_die(rd)
132 };
133
134 Ok((Date::from_raw(inner, calendar), rd))
135 }
136
137 fn try_inner_from_ixdtf_record(
140 ixdtf_record: &IxdtfParseRecord<'_, Utf8>,
141 calendar: Option<CalendarAlgorithm>,
142 ) -> Result<IsoDateInner, ParseError> {
143 let date_record = ixdtf_record.date.ok_or(ParseError::MissingFields)?;
144
145 if let Some(ixdtf_calendar) = ixdtf_record.calendar
146 && let Some(expected_calendar) = calendar
147 && ixdtf_calendar != expected_calendar.as_str().as_bytes()
148 {
149 return Err(ParseError::MismatchedCalendar(
150 expected_calendar,
151 icu_locale_core::extensions::unicode::Value::try_from_utf8(ixdtf_calendar)
152 .ok()
153 .and_then(|v| CalendarAlgorithm::try_from(&v).ok())
154 .ok_or(ParseError::UnknownCalendar)?,
155 ));
156 }
157
158 const _: () = assert!(
160 VALID_RD_RANGE.start().to_i64_date()
161 <= fixed_from_gregorian(-999_999, 1, 1).to_i64_date()
162 && fixed_from_gregorian(999_999, 12, 31).to_i64_date()
163 <= VALID_RD_RANGE.end().to_i64_date()
164 );
165
166 Ok(IsoDateInner(ArithmeticDate::new_unchecked(
167 AbstractGregorianYear::from_iso_year(date_record.year),
168 date_record.month,
169 date_record.day,
170 )))
171 }
172}
173
174#[test]
175fn invalid_calendar() {
176 Date::try_from_str("2025-01-01T00:00:00[u-ca=foo]", crate::Gregorian).unwrap_err();
177}