icu_calendar/
options.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//! Options used by types in this crate
6
7#[cfg(feature = "unstable")]
8pub use unstable::{
9    DateAddOptions, DateDifferenceOptions, DateFromFieldsOptions, MissingFieldsStrategy, Overflow,
10};
11#[cfg(not(feature = "unstable"))]
12pub(crate) use unstable::{
13    DateAddOptions, DateDifferenceOptions, DateFromFieldsOptions, MissingFieldsStrategy, Overflow,
14};
15
16mod unstable {
17    /// Options bag for [`Date::try_from_fields`](crate::Date::try_from_fields).
18    ///
19    /// <div class="stab unstable">
20    /// 🚧 This code is considered unstable; it may change at any time, in breaking or non-breaking ways,
21    /// including in SemVer minor releases. Do not use this type unless you are prepared for things to occasionally break.
22    ///
23    /// Graduation tracking issue: [issue #7161](https://github.com/unicode-org/icu4x/issues/7161).
24    /// </div>
25    ///
26    /// ✨ *Enabled with the `unstable` Cargo feature.*
27    #[derive(Copy, Clone, Debug, PartialEq, Default)]
28    #[non_exhaustive]
29    pub struct DateFromFieldsOptions {
30        /// How to behave with out-of-bounds fields.
31        ///
32        /// Defaults to [`Overflow::Reject`].
33        ///
34        /// # Examples
35        ///
36        /// ```
37        /// use icu::calendar::options::DateFromFieldsOptions;
38        /// use icu::calendar::options::Overflow;
39        /// use icu::calendar::types::DateFields;
40        /// use icu::calendar::Date;
41        /// use icu::calendar::Iso;
42        ///
43        /// // There is no day 31 in September.
44        /// let mut fields = DateFields::default();
45        /// fields.extended_year = Some(2025);
46        /// fields.ordinal_month = Some(9);
47        /// fields.day = Some(31);
48        ///
49        /// let options_default = DateFromFieldsOptions::default();
50        /// assert!(Date::try_from_fields(fields, options_default, Iso).is_err());
51        ///
52        /// let mut options_reject = options_default;
53        /// options_reject.overflow = Some(Overflow::Reject);
54        /// assert!(Date::try_from_fields(fields, options_reject, Iso).is_err());
55        ///
56        /// let mut options_constrain = options_default;
57        /// options_constrain.overflow = Some(Overflow::Constrain);
58        /// assert_eq!(
59        ///     Date::try_from_fields(fields, options_constrain, Iso).unwrap(),
60        ///     Date::try_new_iso(2025, 9, 30).unwrap()
61        /// );
62        /// ```
63        pub overflow: Option<Overflow>,
64        /// How to behave when the fields that are present do not fully constitute a Date.
65        ///
66        /// This option can be used to fill in a missing year given a month and a day according to
67        /// the ECMAScript Temporal specification.
68        ///
69        /// # Examples
70        ///
71        /// ```
72        /// use icu::calendar::options::DateFromFieldsOptions;
73        /// use icu::calendar::options::MissingFieldsStrategy;
74        /// use icu::calendar::types::DateFields;
75        /// use icu::calendar::Date;
76        /// use icu::calendar::Iso;
77        ///
78        /// // These options are missing a year.
79        /// let mut fields = DateFields::default();
80        /// fields.month_code = Some(b"M02");
81        /// fields.day = Some(1);
82        ///
83        /// let options_default = DateFromFieldsOptions::default();
84        /// assert!(Date::try_from_fields(fields, options_default, Iso).is_err());
85        ///
86        /// let mut options_reject = options_default;
87        /// options_reject.missing_fields_strategy =
88        ///     Some(MissingFieldsStrategy::Reject);
89        /// assert!(Date::try_from_fields(fields, options_reject, Iso).is_err());
90        ///
91        /// let mut options_ecma = options_default;
92        /// options_ecma.missing_fields_strategy = Some(MissingFieldsStrategy::Ecma);
93        /// assert_eq!(
94        ///     Date::try_from_fields(fields, options_ecma, Iso).unwrap(),
95        ///     Date::try_new_iso(1972, 2, 1).unwrap()
96        /// );
97        /// ```
98        pub missing_fields_strategy: Option<MissingFieldsStrategy>,
99    }
100
101    impl DateFromFieldsOptions {
102        pub(crate) fn from_add_options(options: DateAddOptions) -> Self {
103            Self {
104                overflow: options.overflow,
105                missing_fields_strategy: Default::default(),
106            }
107        }
108    }
109
110    /// Options for adding a duration to a date.
111    ///
112    /// <div class="stab unstable">
113    /// 🚧 This code is considered unstable; it may change at any time, in breaking or non-breaking ways,
114    /// including in SemVer minor releases. Do not use this type unless you are prepared for things to occasionally break.
115    ///
116    /// Graduation tracking issue: [issue #3964](https://github.com/unicode-org/icu4x/issues/3964).
117    /// </div>
118    ///
119    /// ✨ *Enabled with the `unstable` Cargo feature.*
120    #[derive(Copy, Clone, PartialEq, Debug, Default)]
121    #[non_exhaustive]
122    pub struct DateAddOptions {
123        /// How to behave with out-of-bounds fields during arithmetic.
124        ///
125        /// Defaults to [`Overflow::Constrain`].
126        ///
127        /// # Examples
128        ///
129        /// ```
130        /// use icu::calendar::options::DateAddOptions;
131        /// use icu::calendar::options::Overflow;
132        /// use icu::calendar::types::DateDuration;
133        /// use icu::calendar::Date;
134        ///
135        /// // There is a day 31 in October but not in November.
136        /// let d1 = Date::try_new_iso(2025, 10, 31).unwrap();
137        /// let duration = DateDuration::for_months(1);
138        ///
139        /// let options_default = DateAddOptions::default();
140        /// assert_eq!(
141        ///     d1.try_added_with_options(duration, options_default)
142        ///         .unwrap(),
143        ///     Date::try_new_iso(2025, 11, 30).unwrap()
144        /// );
145        ///
146        /// let mut options_reject = options_default;
147        /// options_reject.overflow = Some(Overflow::Reject);
148        /// assert!(d1.try_added_with_options(duration, options_reject).is_err());
149        ///
150        /// let mut options_constrain = options_default;
151        /// options_constrain.overflow = Some(Overflow::Constrain);
152        /// assert_eq!(
153        ///     d1.try_added_with_options(duration, options_constrain)
154        ///         .unwrap(),
155        ///     Date::try_new_iso(2025, 11, 30).unwrap()
156        /// );
157        /// ```
158        pub overflow: Option<Overflow>,
159    }
160
161    /// Options for taking the difference between two dates.
162    ///
163    /// <div class="stab unstable">
164    /// 🚧 This code is considered unstable; it may change at any time, in breaking or non-breaking ways,
165    /// including in SemVer minor releases. Do not use this type unless you are prepared for things to occasionally break.
166    ///
167    /// Graduation tracking issue: [issue #3964](https://github.com/unicode-org/icu4x/issues/3964).
168    /// </div>
169    ///
170    /// ✨ *Enabled with the `unstable` Cargo feature.*
171    #[derive(Copy, Clone, PartialEq, Debug, Default)]
172    #[non_exhaustive]
173    pub struct DateDifferenceOptions {
174        /// Which date field to allow as the largest in a duration when taking the difference.
175        ///
176        /// When choosing [`Months`] or [`Years`], the resulting [`DateDuration`] might not be
177        /// associative or commutative in subsequent arithmetic operations, and it might require
178        /// [`Overflow::Constrain`] in addition.
179        ///
180        /// # Examples
181        ///
182        /// ```
183        /// use icu::calendar::options::DateDifferenceOptions;
184        /// use icu::calendar::types::DateDuration;
185        /// use icu::calendar::types::DateDurationUnit;
186        /// use icu::calendar::Date;
187        ///
188        /// let d1 = Date::try_new_iso(2025, 3, 31).unwrap();
189        /// let d2 = Date::try_new_iso(2026, 5, 15).unwrap();
190        ///
191        /// let options_default = DateDifferenceOptions::default();
192        /// assert_eq!(
193        ///     d1.try_until_with_options(&d2, options_default).unwrap(),
194        ///     DateDuration::for_days(410)
195        /// );
196        ///
197        /// let mut options_days = options_default;
198        /// options_days.largest_unit = Some(DateDurationUnit::Days);
199        /// assert_eq!(
200        ///     d1.try_until_with_options(&d2, options_default).unwrap(),
201        ///     DateDuration::for_days(410)
202        /// );
203        ///
204        /// let mut options_weeks = options_default;
205        /// options_weeks.largest_unit = Some(DateDurationUnit::Weeks);
206        /// assert_eq!(
207        ///     d1.try_until_with_options(&d2, options_weeks).unwrap(),
208        ///     DateDuration {
209        ///         weeks: 58,
210        ///         days: 4,
211        ///         ..Default::default()
212        ///     }
213        /// );
214        ///
215        /// let mut options_months = options_default;
216        /// options_months.largest_unit = Some(DateDurationUnit::Months);
217        /// assert_eq!(
218        ///     d1.try_until_with_options(&d2, options_months).unwrap(),
219        ///     DateDuration {
220        ///         months: 13,
221        ///         days: 15,
222        ///         ..Default::default()
223        ///     }
224        /// );
225        ///
226        /// let mut options_years = options_default;
227        /// options_years.largest_unit = Some(DateDurationUnit::Years);
228        /// assert_eq!(
229        ///     d1.try_until_with_options(&d2, options_years).unwrap(),
230        ///     DateDuration {
231        ///         years: 1,
232        ///         months: 1,
233        ///         days: 15,
234        ///         ..Default::default()
235        ///     }
236        /// );
237        /// ```
238        ///
239        /// [`Months`]: crate::types::DateDurationUnit::Months
240        /// [`Years`]: crate::types::DateDurationUnit::Years
241        /// [`DateDuration`]: crate::types::DateDuration
242        pub largest_unit: Option<crate::duration::DateDurationUnit>,
243    }
244
245    /// Whether to constrain or reject out-of-bounds values when constructing a Date.
246    ///
247    /// The behavior conforms to the ECMAScript Temporal specification.
248    ///
249    /// <div class="stab unstable">
250    /// 🚧 This code is considered unstable; it may change at any time, in breaking or non-breaking ways,
251    /// including in SemVer minor releases. Do not use this type unless you are prepared for things to occasionally break.
252    ///
253    /// Graduation tracking issue: [issue #7161](https://github.com/unicode-org/icu4x/issues/7161).
254    /// </div>
255    ///
256    /// ✨ *Enabled with the `unstable` Cargo feature.*
257    #[derive(Copy, Clone, Debug, PartialEq, Default)]
258    #[non_exhaustive]
259    pub enum Overflow {
260        /// Constrain out-of-bounds fields to the nearest in-bounds value.
261        ///
262        /// Only the out-of-bounds field is constrained. If the other fields are not themselves
263        /// out of bounds, they are not changed.
264        ///
265        /// This is the [default option](
266        /// https://tc39.es/proposal-temporal/#sec-temporal-gettemporaloverflowoption),
267        /// following the ECMAScript Temporal specification. See also the [docs on MDN](
268        /// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDate#invalid_date_clamping).
269        ///
270        /// # Examples
271        ///
272        /// ```
273        /// use icu::calendar::cal::Hebrew;
274        /// use icu::calendar::options::DateFromFieldsOptions;
275        /// use icu::calendar::options::Overflow;
276        /// use icu::calendar::types::DateFields;
277        /// use icu::calendar::Date;
278        /// use icu::calendar::DateError;
279        ///
280        /// let mut options = DateFromFieldsOptions::default();
281        /// options.overflow = Some(Overflow::Constrain);
282        ///
283        /// // 5784, a leap year, contains M05L, but the day is too big.
284        /// let mut fields = DateFields::default();
285        /// fields.extended_year = Some(5784);
286        /// fields.month_code = Some(b"M05L");
287        /// fields.day = Some(50);
288        ///
289        /// let date = Date::try_from_fields(fields, options, Hebrew).unwrap();
290        ///
291        /// // Constrained to the 30th day of M05L of year 5784
292        /// assert_eq!(date.year().extended_year(), 5784);
293        /// assert_eq!(date.month().standard_code.0, "M05L");
294        /// assert_eq!(date.day_of_month().0, 30);
295        ///
296        /// // 5785, a common year, does not contain M05L.
297        /// fields.extended_year = Some(5785);
298        ///
299        /// let date = Date::try_from_fields(fields, options, Hebrew).unwrap();
300        ///
301        /// // Constrained to the 29th day of M06 of year 5785
302        /// assert_eq!(date.year().extended_year(), 5785);
303        /// assert_eq!(date.month().standard_code.0, "M06");
304        /// assert_eq!(date.day_of_month().0, 29);
305        /// ```
306        Constrain,
307        /// Return an error if any fields are out of bounds.
308        ///
309        /// # Examples
310        ///
311        /// ```
312        /// use icu::calendar::cal::Hebrew;
313        /// use icu::calendar::error::DateFromFieldsError;
314        /// use icu::calendar::options::DateFromFieldsOptions;
315        /// use icu::calendar::options::Overflow;
316        /// use icu::calendar::types::DateFields;
317        /// use icu::calendar::Date;
318        /// use tinystr::tinystr;
319        ///
320        /// let mut options = DateFromFieldsOptions::default();
321        /// options.overflow = Some(Overflow::Reject);
322        ///
323        /// // 5784, a leap year, contains M05L, but the day is too big.
324        /// let mut fields = DateFields::default();
325        /// fields.extended_year = Some(5784);
326        /// fields.month_code = Some(b"M05L");
327        /// fields.day = Some(50);
328        ///
329        /// let err = Date::try_from_fields(fields, options, Hebrew)
330        ///     .expect_err("Day is out of bounds");
331        /// assert!(matches!(err, DateFromFieldsError::Range { .. }));
332        ///
333        /// // Set the day to one that exists
334        /// fields.day = Some(1);
335        /// Date::try_from_fields(fields, options, Hebrew)
336        ///     .expect("A valid Hebrew date");
337        ///
338        /// // 5785, a common year, does not contain M05L.
339        /// fields.extended_year = Some(5785);
340        ///
341        /// let err = Date::try_from_fields(fields, options, Hebrew)
342        ///     .expect_err("Month is out of bounds");
343        /// assert!(matches!(err, DateFromFieldsError::MonthCodeNotInYear));
344        /// ```
345        #[default]
346        Reject,
347    }
348
349    /// How to infer missing fields when the fields that are present do not fully constitute a Date.
350    ///
351    /// In order for the fields to fully constitute a Date, they must identify a year, a month,
352    /// and a day. The fields `era`, `era_year`, and `extended_year` identify the year:
353    ///
354    /// | Era? | Era Year? | Extended Year? | Outcome |
355    /// |------|-----------|----------------|---------|
356    /// | -    | -         | -              | Error |
357    /// | Some | -         | -              | Error |
358    /// | -    | Some      | -              | Error |
359    /// | -    | -         | Some           | OK |
360    /// | Some | Some      | -              | OK |
361    /// | Some | -         | Some           | Error (era requires era year) |
362    /// | -    | Some      | Some           | Error (era year requires era) |
363    /// | Some | Some      | Some           | OK (but error if inconsistent) |
364    ///
365    /// The fields `month_code` and `ordinal_month` identify the month:
366    ///
367    /// | Month Code? | Ordinal Month? | Outcome |
368    /// |-------------|----------------|---------|
369    /// | -           | -              | Error |
370    /// | Some        | -              | OK |
371    /// | -           | Some           | OK |
372    /// | Some        | Some           | OK (but error if inconsistent) |
373    ///
374    /// The field `day` identifies the day.
375    ///
376    /// If the fields identify a year, a month, and a day, then there are no missing fields, so
377    /// the strategy chosen here has no effect (fields that are out-of-bounds or inconsistent
378    /// are handled by other errors).
379    ///
380    /// <div class="stab unstable">
381    /// 🚧 This code is considered unstable; it may change at any time, in breaking or non-breaking ways,
382    /// including in SemVer minor releases. Do not use this type unless you are prepared for things to occasionally break.
383    ///
384    /// Graduation tracking issue: [issue #7161](https://github.com/unicode-org/icu4x/issues/7161).
385    /// </div>
386    ///
387    /// ✨ *Enabled with the `unstable` Cargo feature.*
388    #[derive(Copy, Clone, Debug, PartialEq, Default)]
389    #[non_exhaustive]
390    pub enum MissingFieldsStrategy {
391        /// If the fields that are present do not fully constitute a Date,
392        /// return [`DateFromFieldsError::NotEnoughFields`].
393        ///
394        /// [`DateFromFieldsError::NotEnoughFields`]: crate::error::DateFromFieldsError::NotEnoughFields
395        #[default]
396        Reject,
397        /// If the fields that are present do not fully constitute a Date,
398        /// follow the ECMAScript specification when possible.
399        ///
400        /// ⚠️ This option causes a year or day to be implicitly added to the Date!
401        ///
402        /// This strategy makes the following changes:
403        ///
404        /// 1. If the fields identify a year and a month, but not a day, then set `day` to 1.
405        /// 2. If `month_code` and `day` are set but nothing else, then set the year to a
406        ///    _reference year_: some year in the calendar that contains the specified month
407        ///    and day, according to the ECMAScript specification.
408        ///
409        /// Note that the reference year is _not_ added if an ordinal month is present, since
410        /// the identity of an ordinal month changes from year to year.
411        Ecma,
412    }
413}
414#[cfg(test)]
415mod tests {
416    use crate::{error::DateFromFieldsError, types::DateFields, Date, Gregorian};
417    use itertools::Itertools;
418    use std::collections::{BTreeMap, BTreeSet};
419
420    use super::*;
421
422    #[test]
423    #[allow(clippy::field_reassign_with_default)] // want out-of-crate code style
424    fn test_missing_fields_strategy() {
425        // The sets of fields that identify a year, according to the table in the docs
426        let valid_year_field_sets = [
427            &["era", "era_year"][..],
428            &["extended_year"][..],
429            &["era", "era_year", "extended_year"][..],
430        ]
431        .into_iter()
432        .map(|field_names| field_names.iter().copied().collect())
433        .collect::<Vec<BTreeSet<&str>>>();
434
435        // The sets of fields that identify a month, according to the table in the docs
436        let valid_month_field_sets = [
437            &["month_code"][..],
438            &["ordinal_month"][..],
439            &["month_code", "ordinal_month"][..],
440        ]
441        .into_iter()
442        .map(|field_names| field_names.iter().copied().collect())
443        .collect::<Vec<BTreeSet<&str>>>();
444
445        // The sets of fields that identify a day, according to the table in the docs
446        let valid_day_field_sets = [&["day"][..]]
447            .into_iter()
448            .map(|field_names| field_names.iter().copied().collect())
449            .collect::<Vec<BTreeSet<&str>>>();
450
451        // All possible valid sets of fields
452        let all_valid_field_sets = valid_year_field_sets
453            .iter()
454            .cartesian_product(valid_month_field_sets.iter())
455            .cartesian_product(valid_day_field_sets.iter())
456            .map(|((year_fields, month_fields), day_fields)| {
457                year_fields
458                    .iter()
459                    .chain(month_fields.iter())
460                    .chain(day_fields.iter())
461                    .copied()
462                    .collect::<BTreeSet<&str>>()
463            })
464            .collect::<BTreeSet<BTreeSet<&str>>>();
465
466        // Field sets with year and month but without day that ECMA accepts
467        let field_sets_without_day = valid_year_field_sets
468            .iter()
469            .cartesian_product(valid_month_field_sets.iter())
470            .map(|(year_fields, month_fields)| {
471                year_fields
472                    .iter()
473                    .chain(month_fields.iter())
474                    .copied()
475                    .collect::<BTreeSet<&str>>()
476            })
477            .collect::<BTreeSet<BTreeSet<&str>>>();
478
479        // Field sets with month and day but without year that ECMA accepts
480        let field_sets_without_year = [&["month_code", "day"][..]]
481            .into_iter()
482            .map(|field_names| field_names.iter().copied().collect())
483            .collect::<Vec<BTreeSet<&str>>>();
484
485        // A map from field names to a function that sets that field
486        let mut field_fns = BTreeMap::<&str, &dyn Fn(&mut DateFields)>::new();
487        field_fns.insert("era", &|fields| fields.era = Some(b"ad"));
488        field_fns.insert("era_year", &|fields| fields.era_year = Some(2000));
489        field_fns.insert("extended_year", &|fields| fields.extended_year = Some(2000));
490        field_fns.insert("month_code", &|fields| fields.month_code = Some(b"M04"));
491        field_fns.insert("ordinal_month", &|fields| fields.ordinal_month = Some(4));
492        field_fns.insert("day", &|fields| fields.day = Some(20));
493
494        for field_set in field_fns.keys().copied().powerset() {
495            let field_set = field_set.into_iter().collect::<BTreeSet<&str>>();
496
497            // Check whether this case should succeed: whether it identifies a year,
498            // a month, and a day.
499            let should_succeed_rejecting = all_valid_field_sets.contains(&field_set);
500
501            // Check whether it should succeed in ECMA mode.
502            let should_succeed_ecma = should_succeed_rejecting
503                || field_sets_without_day.contains(&field_set)
504                || field_sets_without_year.contains(&field_set);
505
506            // Populate the fields in the field set
507            let mut fields = Default::default();
508            for field_name in field_set {
509                field_fns.get(field_name).unwrap()(&mut fields);
510            }
511
512            // Check whether we were able to successfully construct the date
513            let mut options = DateFromFieldsOptions::default();
514            options.missing_fields_strategy = Some(MissingFieldsStrategy::Reject);
515            match Date::try_from_fields(fields, options, Gregorian) {
516                Ok(_) => assert!(
517                    should_succeed_rejecting,
518                    "Succeeded, but should have rejected: {fields:?}"
519                ),
520                Err(DateFromFieldsError::NotEnoughFields) => assert!(
521                    !should_succeed_rejecting,
522                    "Rejected, but should have succeeded: {fields:?}"
523                ),
524                Err(e) => panic!("Unexpected error: {e} for {fields:?}"),
525            }
526
527            // Check ECMA mode
528            let mut options = DateFromFieldsOptions::default();
529            options.missing_fields_strategy = Some(MissingFieldsStrategy::Ecma);
530            match Date::try_from_fields(fields, options, Gregorian) {
531                Ok(_) => assert!(
532                    should_succeed_ecma,
533                    "Succeeded, but should have rejected (ECMA): {fields:?}"
534                ),
535                Err(DateFromFieldsError::NotEnoughFields) => assert!(
536                    !should_succeed_ecma,
537                    "Rejected, but should have succeeded (ECMA): {fields:?}"
538                ),
539                Err(e) => panic!("Unexpected error: {e} for {fields:?}"),
540            }
541        }
542    }
543
544    #[test]
545    fn test_constrain_large_months() {
546        let fields = DateFields {
547            extended_year: Some(2004),
548            ordinal_month: Some(15),
549            day: Some(1),
550            ..Default::default()
551        };
552        let options = DateFromFieldsOptions {
553            overflow: Some(Overflow::Constrain),
554            ..Default::default()
555        };
556
557        let _ = Date::try_from_fields(fields, options, crate::cal::Persian).unwrap();
558    }
559}