icu_calendar/
error.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//! Error types for functions in the calendar crate
6
7use crate::{options::Overflow, types::MonthCode};
8use displaydoc::Display;
9
10#[derive(Debug, Copy, Clone, PartialEq, Display)]
11/// Error type for date creation via [`Date::try_new_from_codes`].
12///
13/// [`Date::try_new_from_codes`]: crate::Date::try_new_from_codes
14#[non_exhaustive]
15pub enum DateError {
16    /// A field is out of range for its domain.
17    #[displaydoc("The {field} = {value} argument is out of range {min}..={max}")]
18    Range {
19        /// The field that is out of range, such as "year"
20        field: &'static str,
21        /// The actual value
22        value: i32,
23        /// The minimum value (inclusive). This might not be tight.
24        min: i32,
25        /// The maximum value (inclusive). This might not be tight.
26        max: i32,
27    },
28    /// The era code is invalid for the calendar.
29    #[displaydoc("Unknown era")]
30    UnknownEra,
31    /// The month code is invalid for the calendar or year.
32    ///
33    /// # Examples
34    ///
35    /// ```
36    /// use icu::calendar::cal::Hebrew;
37    /// use icu::calendar::types::MonthCode;
38    /// use icu::calendar::Date;
39    /// use icu::calendar::DateError;
40    /// use tinystr::tinystr;
41    ///
42    /// Date::try_new_from_codes(
43    ///     None,
44    ///     5784,
45    ///     MonthCode::new_leap(5).unwrap(),
46    ///     1,
47    ///     Hebrew,
48    /// )
49    /// .expect("5784 is a leap year");
50    ///
51    /// let err = Date::try_new_from_codes(
52    ///     None,
53    ///     5785,
54    ///     MonthCode::new_leap(5).unwrap(),
55    ///     1,
56    ///     Hebrew,
57    /// )
58    /// .expect_err("5785 is a common year");
59    ///
60    /// assert!(matches!(err, DateError::UnknownMonthCode(_)));
61    /// ```
62    #[displaydoc("Unknown month code {0:?}")]
63    UnknownMonthCode(MonthCode),
64}
65
66impl core::error::Error for DateError {}
67#[cfg(feature = "unstable")]
68pub use unstable::DateFromFieldsError;
69#[cfg(not(feature = "unstable"))]
70pub(crate) use unstable::DateFromFieldsError;
71
72mod unstable {
73    pub use super::*;
74    /// Error type for date creation via [`Date::try_from_fields`].
75    ///
76    /// [`Date::try_from_fields`]: crate::Date::try_from_fields
77    ///
78    /// <div class="stab unstable">
79    /// 🚧 This code is considered unstable; it may change at any time, in breaking or non-breaking ways,
80    /// including in SemVer minor releases. Do not use this type unless you are prepared for things to occasionally break.
81    ///
82    /// Graduation tracking issue: [issue #7161](https://github.com/unicode-org/icu4x/issues/7161).
83    /// </div>
84    ///
85    /// ✨ *Enabled with the `unstable` Cargo feature.*
86    #[derive(Debug, Copy, Clone, PartialEq, Display)]
87    #[non_exhaustive]
88    pub enum DateFromFieldsError {
89        /// A field is out of range for its domain.
90        ///
91        /// # Examples
92        ///
93        /// ```
94        /// use icu::calendar::error::DateFromFieldsError;
95        /// use icu::calendar::error::RangeError;
96        /// use icu::calendar::types::DateFields;
97        /// use icu::calendar::Date;
98        /// use icu::calendar::Iso;
99        ///
100        /// let mut fields = DateFields::default();
101        /// fields.extended_year = Some(2000);
102        /// fields.ordinal_month = Some(13);
103        /// fields.day = Some(1);
104        ///
105        /// let err = Date::try_from_fields(fields, Default::default(), Iso)
106        ///     .expect_err("no month 13 in the ISO calendar");
107        ///
108        /// assert!(matches!(
109        ///     err,
110        ///     DateFromFieldsError::Range(RangeError { field: "month", .. })
111        /// ));
112        /// ```
113        #[displaydoc("{0}")]
114        Range(RangeError),
115        /// The era code is invalid for the calendar.
116        #[displaydoc("Unknown era or invalid syntax")]
117        UnknownEra,
118        /// The month code syntax is invalid.
119        ///
120        /// # Examples
121        ///
122        /// ```
123        /// use icu::calendar::error::DateFromFieldsError;
124        /// use icu::calendar::types::DateFields;
125        /// use icu::calendar::Date;
126        /// use icu::calendar::Iso;
127        ///
128        /// let mut fields = DateFields::default();
129        /// fields.extended_year = Some(2000);
130        /// fields.month_code = Some(b"????");
131        /// fields.day = Some(1);
132        ///
133        /// let err = Date::try_from_fields(fields, Default::default(), Iso)
134        ///     .expect_err("month code is invalid");
135        ///
136        /// assert_eq!(err, DateFromFieldsError::MonthCodeInvalidSyntax);
137        /// ```
138        #[displaydoc("Invalid month code syntax")]
139        MonthCodeInvalidSyntax,
140        /// The specified month code does not exist in this calendar.
141        ///
142        /// # Examples
143        ///
144        /// ```
145        /// use icu::calendar::cal::Hebrew;
146        /// use icu::calendar::error::DateFromFieldsError;
147        /// use icu::calendar::types::DateFields;
148        /// use icu::calendar::Date;
149        ///
150        /// let mut fields = DateFields::default();
151        /// fields.extended_year = Some(5783);
152        /// fields.month_code = Some(b"M13");
153        /// fields.day = Some(1);
154        ///
155        /// let err = Date::try_from_fields(fields, Default::default(), Hebrew)
156        ///     .expect_err("no month M13 in Hebrew");
157        /// assert_eq!(err, DateFromFieldsError::MonthCodeNotInCalendar);
158        /// ```
159        #[displaydoc("The specified month code does not exist in this calendar")]
160        MonthCodeNotInCalendar,
161        /// The specified month code exists in this calendar, but not in the specified year.
162        ///
163        /// # Examples
164        ///
165        /// ```
166        /// use icu::calendar::cal::Hebrew;
167        /// use icu::calendar::error::DateFromFieldsError;
168        /// use icu::calendar::types::DateFields;
169        /// use icu::calendar::Date;
170        ///
171        /// let mut fields = DateFields::default();
172        /// fields.extended_year = Some(5783);
173        /// fields.month_code = Some(b"M05L");
174        /// fields.day = Some(1);
175        ///
176        /// let err = Date::try_from_fields(fields, Default::default(), Hebrew)
177        ///     .expect_err("no month M05L in Hebrew year 5783");
178        /// assert_eq!(err, DateFromFieldsError::MonthCodeNotInYear);
179        /// ```
180        #[displaydoc("The specified month code exists in calendar, but not for this year")]
181        MonthCodeNotInYear,
182        /// The year was specified in multiple inconsistent ways.
183        ///
184        /// # Examples
185        ///
186        /// ```
187        /// use icu::calendar::cal::Japanese;
188        /// use icu::calendar::error::DateFromFieldsError;
189        /// use icu::calendar::types::DateFields;
190        /// use icu::calendar::Date;
191        ///
192        /// let mut fields = DateFields::default();
193        /// fields.era = Some(b"reiwa");
194        /// fields.era_year = Some(6);
195        /// fields.ordinal_month = Some(1);
196        /// fields.day = Some(1);
197        ///
198        /// Date::try_from_fields(fields, Default::default(), Japanese::new())
199        ///     .expect("a well-defined Japanese date");
200        ///
201        /// fields.extended_year = Some(1900);
202        ///
203        /// let err =
204        ///     Date::try_from_fields(fields, Default::default(), Japanese::new())
205        ///         .expect_err("year 1900 is not the same as 6 Reiwa");
206        ///
207        /// assert_eq!(err, DateFromFieldsError::InconsistentYear);
208        /// ```
209        #[displaydoc("Inconsistent year")]
210        InconsistentYear,
211        /// The month was specified in multiple inconsistent ways.
212        ///
213        /// # Examples
214        ///
215        /// ```
216        /// use icu::calendar::cal::Hebrew;
217        /// use icu::calendar::error::DateFromFieldsError;
218        /// use icu::calendar::types::DateFields;
219        /// use icu::calendar::Date;
220        /// use tinystr::tinystr;
221        ///
222        /// let mut fields = DateFields::default();
223        /// fields.extended_year = Some(5783);
224        /// fields.month_code = Some(b"M06");
225        /// fields.ordinal_month = Some(6);
226        /// fields.day = Some(1);
227        ///
228        /// Date::try_from_fields(fields, Default::default(), Hebrew)
229        ///     .expect("a well-defined Hebrew date in a common year");
230        ///
231        /// fields.extended_year = Some(5784);
232        ///
233        /// let err = Date::try_from_fields(fields, Default::default(), Hebrew)
234        ///     .expect_err("month M06 is not the 6th month in leap year 5784");
235        ///
236        /// assert_eq!(err, DateFromFieldsError::InconsistentMonth);
237        /// ```
238        #[displaydoc("Inconsistent month")]
239        InconsistentMonth,
240        /// Not enough fields were specified to form a well-defined date.
241        ///
242        /// # Examples
243        ///
244        /// ```
245        /// use icu::calendar::cal::Hebrew;
246        /// use icu::calendar::error::DateFromFieldsError;
247        /// use icu::calendar::types::DateFields;
248        /// use icu::calendar::Date;
249        /// use tinystr::tinystr;
250        ///
251        /// let mut fields = DateFields::default();
252        ///
253        /// fields.ordinal_month = Some(3);
254        ///
255        /// let err = Date::try_from_fields(fields, Default::default(), Hebrew)
256        ///     .expect_err("need more than just an ordinal month");
257        /// assert_eq!(err, DateFromFieldsError::NotEnoughFields);
258        ///
259        /// fields.era_year = Some(5783);
260        ///
261        /// let err = Date::try_from_fields(fields, Default::default(), Hebrew)
262        ///     .expect_err("need more than an ordinal month and an era year");
263        /// assert_eq!(err, DateFromFieldsError::NotEnoughFields);
264        ///
265        /// fields.extended_year = Some(5783);
266        ///
267        /// let err = Date::try_from_fields(fields, Default::default(), Hebrew)
268        ///     .expect_err("era year still needs an era");
269        /// assert_eq!(err, DateFromFieldsError::NotEnoughFields);
270        ///
271        /// fields.era = Some(b"am");
272        ///
273        /// let date = Date::try_from_fields(fields, Default::default(), Hebrew)
274        ///     .expect_err("still missing the day");
275        /// assert_eq!(err, DateFromFieldsError::NotEnoughFields);
276        ///
277        /// fields.day = Some(1);
278        /// let date = Date::try_from_fields(fields, Default::default(), Hebrew)
279        ///     .expect("we have enough fields!");
280        /// ```
281        #[displaydoc("Not enough fields")]
282        NotEnoughFields,
283    }
284
285    impl core::error::Error for DateFromFieldsError {}
286
287    impl From<RangeError> for DateFromFieldsError {
288        #[inline]
289        fn from(value: RangeError) -> Self {
290            DateFromFieldsError::Range(value)
291        }
292    }
293}
294
295/// Internal narrow error type for functions that only fail on unknown eras
296pub(crate) struct UnknownEraError;
297
298impl From<UnknownEraError> for DateError {
299    #[inline]
300    fn from(_value: UnknownEraError) -> Self {
301        DateError::UnknownEra
302    }
303}
304
305impl From<UnknownEraError> for DateFromFieldsError {
306    #[inline]
307    fn from(_value: UnknownEraError) -> Self {
308        DateFromFieldsError::UnknownEra
309    }
310}
311
312/// Internal narrow error type for functions that only fail on parsing month codes
313#[derive(Debug)]
314pub(crate) enum MonthCodeParseError {
315    InvalidSyntax,
316}
317
318impl From<MonthCodeParseError> for DateFromFieldsError {
319    #[inline]
320    fn from(value: MonthCodeParseError) -> Self {
321        match value {
322            MonthCodeParseError::InvalidSyntax => DateFromFieldsError::MonthCodeInvalidSyntax,
323        }
324    }
325}
326
327/// Internal narrow error type for functions that only fail on month code operations
328#[derive(Debug, PartialEq)]
329pub(crate) enum MonthCodeError {
330    NotInCalendar,
331    NotInYear,
332}
333
334impl From<MonthCodeError> for DateFromFieldsError {
335    #[inline]
336    fn from(value: MonthCodeError) -> Self {
337        match value {
338            MonthCodeError::NotInCalendar => DateFromFieldsError::MonthCodeNotInCalendar,
339            MonthCodeError::NotInYear => DateFromFieldsError::MonthCodeNotInYear,
340        }
341    }
342}
343
344mod inner {
345    /// Internal narrow error type for calculating the ECMA reference year
346    ///
347    /// Public but unstable because it is used on hijri::Rules
348    #[derive(Debug, Copy, Clone, PartialEq, Eq)]
349    #[allow(missing_docs)] // TODO: fix when graduating
350    #[non_exhaustive]
351    pub enum EcmaReferenceYearError {
352        Unimplemented,
353        MonthCodeNotInCalendar,
354    }
355}
356
357#[cfg(feature = "unstable")]
358pub use inner::EcmaReferenceYearError;
359#[cfg(not(feature = "unstable"))]
360pub(crate) use inner::EcmaReferenceYearError;
361
362impl From<EcmaReferenceYearError> for DateFromFieldsError {
363    #[inline]
364    fn from(value: EcmaReferenceYearError) -> Self {
365        match value {
366            EcmaReferenceYearError::Unimplemented => DateFromFieldsError::NotEnoughFields,
367            EcmaReferenceYearError::MonthCodeNotInCalendar => {
368                DateFromFieldsError::MonthCodeNotInCalendar
369            }
370        }
371    }
372}
373
374#[derive(Debug, Copy, Clone, PartialEq, Display)]
375/// An argument is out of range for its domain.
376#[displaydoc("The {field} = {value} argument is out of range {min}..={max}")]
377#[allow(clippy::exhaustive_structs)]
378pub struct RangeError {
379    /// The argument that is out of range, such as "year"
380    pub field: &'static str,
381    /// The actual value
382    pub value: i32,
383    /// The minimum value (inclusive). This might not be tight.
384    pub min: i32,
385    /// The maximum value (inclusive). This might not be tight.
386    pub max: i32,
387}
388
389impl core::error::Error for RangeError {}
390
391impl From<RangeError> for DateError {
392    #[inline]
393    fn from(value: RangeError) -> Self {
394        let RangeError {
395            field,
396            value,
397            min,
398            max,
399        } = value;
400        DateError::Range {
401            field,
402            value,
403            min,
404            max,
405        }
406    }
407}
408
409pub(crate) fn range_check_with_overflow<T: Ord + Into<i32> + Copy>(
410    value: T,
411    field: &'static str,
412    bounds: core::ops::RangeInclusive<T>,
413    overflow: Overflow,
414) -> Result<T, RangeError> {
415    if matches!(overflow, Overflow::Constrain) {
416        Ok(value.clamp(*bounds.start(), *bounds.end()))
417    } else {
418        range_check(value, field, bounds)
419    }
420}
421
422pub(crate) fn range_check<T: Ord + Into<i32> + Copy>(
423    value: T,
424    field: &'static str,
425    bounds: core::ops::RangeInclusive<T>,
426) -> Result<T, RangeError> {
427    if !bounds.contains(&value) {
428        return Err(RangeError {
429            field,
430            value: value.into(),
431            min: (*bounds.start()).into(),
432            max: (*bounds.end()).into(),
433        });
434    }
435    Ok(value)
436}