// This file is part of ICU4X. For terms of use, please see the file
// called LICENSE at the top level of the ICU4X source tree
// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).
//! Options used by types in this crate
#[cfg(feature = "unstable")]
pub use unstable::{
DateAddOptions, DateDifferenceOptions, DateFromFieldsOptions, MissingFieldsStrategy, Overflow,
};
#[cfg(not(feature = "unstable"))]
pub(crate) use unstable::{
DateAddOptions, DateDifferenceOptions, DateFromFieldsOptions, MissingFieldsStrategy, Overflow,
};
mod unstable {
/// Options bag for [`Date::try_from_fields`](crate::Date::try_from_fields).
///
///
/// 🚧 This code is considered unstable; it may change at any time, in breaking or non-breaking ways,
/// including in SemVer minor releases. Do not use this type unless you are prepared for things to occasionally break.
///
/// Graduation tracking issue: [issue #7161](https://github.com/unicode-org/icu4x/issues/7161).
///
///
/// ✨ *Enabled with the `unstable` Cargo feature.*
#[derive(Copy, Clone, Debug, PartialEq, Default)]
#[non_exhaustive]
pub struct DateFromFieldsOptions {
/// How to behave with out-of-bounds fields.
///
/// Defaults to [`Overflow::Reject`].
///
/// # Examples
///
/// ```
/// use icu::calendar::options::DateFromFieldsOptions;
/// use icu::calendar::options::Overflow;
/// use icu::calendar::types::DateFields;
/// use icu::calendar::Date;
/// use icu::calendar::Iso;
///
/// // There is no day 31 in September.
/// let mut fields = DateFields::default();
/// fields.extended_year = Some(2025);
/// fields.ordinal_month = Some(9);
/// fields.day = Some(31);
///
/// let options_default = DateFromFieldsOptions::default();
/// assert!(Date::try_from_fields(fields, options_default, Iso).is_err());
///
/// let mut options_reject = options_default;
/// options_reject.overflow = Some(Overflow::Reject);
/// assert!(Date::try_from_fields(fields, options_reject, Iso).is_err());
///
/// let mut options_constrain = options_default;
/// options_constrain.overflow = Some(Overflow::Constrain);
/// assert_eq!(
/// Date::try_from_fields(fields, options_constrain, Iso).unwrap(),
/// Date::try_new_iso(2025, 9, 30).unwrap()
/// );
/// ```
pub overflow: Option,
/// How to behave when the fields that are present do not fully constitute a Date.
///
/// This option can be used to fill in a missing year given a month and a day according to
/// the ECMAScript Temporal specification.
///
/// # Examples
///
/// ```
/// use icu::calendar::options::DateFromFieldsOptions;
/// use icu::calendar::options::MissingFieldsStrategy;
/// use icu::calendar::types::DateFields;
/// use icu::calendar::Date;
/// use icu::calendar::Iso;
///
/// // These options are missing a year.
/// let mut fields = DateFields::default();
/// fields.month_code = Some(b"M02");
/// fields.day = Some(1);
///
/// let options_default = DateFromFieldsOptions::default();
/// assert!(Date::try_from_fields(fields, options_default, Iso).is_err());
///
/// let mut options_reject = options_default;
/// options_reject.missing_fields_strategy =
/// Some(MissingFieldsStrategy::Reject);
/// assert!(Date::try_from_fields(fields, options_reject, Iso).is_err());
///
/// let mut options_ecma = options_default;
/// options_ecma.missing_fields_strategy = Some(MissingFieldsStrategy::Ecma);
/// assert_eq!(
/// Date::try_from_fields(fields, options_ecma, Iso).unwrap(),
/// Date::try_new_iso(1972, 2, 1).unwrap()
/// );
/// ```
pub missing_fields_strategy: Option,
}
impl DateFromFieldsOptions {
pub(crate) fn from_add_options(options: DateAddOptions) -> Self {
Self {
overflow: options.overflow,
missing_fields_strategy: Default::default(),
}
}
}
/// Options for adding a duration to a date.
///
///
/// 🚧 This code is considered unstable; it may change at any time, in breaking or non-breaking ways,
/// including in SemVer minor releases. Do not use this type unless you are prepared for things to occasionally break.
///
/// Graduation tracking issue: [issue #3964](https://github.com/unicode-org/icu4x/issues/3964).
///
///
/// ✨ *Enabled with the `unstable` Cargo feature.*
#[derive(Copy, Clone, PartialEq, Debug, Default)]
#[non_exhaustive]
pub struct DateAddOptions {
/// How to behave with out-of-bounds fields during arithmetic.
///
/// Defaults to [`Overflow::Constrain`].
///
/// # Examples
///
/// ```
/// use icu::calendar::options::DateAddOptions;
/// use icu::calendar::options::Overflow;
/// use icu::calendar::types::DateDuration;
/// use icu::calendar::Date;
///
/// // There is a day 31 in October but not in November.
/// let d1 = Date::try_new_iso(2025, 10, 31).unwrap();
/// let duration = DateDuration::for_months(1);
///
/// let options_default = DateAddOptions::default();
/// assert_eq!(
/// d1.try_added_with_options(duration, options_default)
/// .unwrap(),
/// Date::try_new_iso(2025, 11, 30).unwrap()
/// );
///
/// let mut options_reject = options_default;
/// options_reject.overflow = Some(Overflow::Reject);
/// assert!(d1.try_added_with_options(duration, options_reject).is_err());
///
/// let mut options_constrain = options_default;
/// options_constrain.overflow = Some(Overflow::Constrain);
/// assert_eq!(
/// d1.try_added_with_options(duration, options_constrain)
/// .unwrap(),
/// Date::try_new_iso(2025, 11, 30).unwrap()
/// );
/// ```
pub overflow: Option,
}
/// Options for taking the difference between two dates.
///
///
/// 🚧 This code is considered unstable; it may change at any time, in breaking or non-breaking ways,
/// including in SemVer minor releases. Do not use this type unless you are prepared for things to occasionally break.
///
/// Graduation tracking issue: [issue #3964](https://github.com/unicode-org/icu4x/issues/3964).
///
///
/// ✨ *Enabled with the `unstable` Cargo feature.*
#[derive(Copy, Clone, PartialEq, Debug, Default)]
#[non_exhaustive]
pub struct DateDifferenceOptions {
/// Which date field to allow as the largest in a duration when taking the difference.
///
/// When choosing [`Months`] or [`Years`], the resulting [`DateDuration`] might not be
/// associative or commutative in subsequent arithmetic operations, and it might require
/// [`Overflow::Constrain`] in addition.
///
/// # Examples
///
/// ```
/// use icu::calendar::options::DateDifferenceOptions;
/// use icu::calendar::types::DateDuration;
/// use icu::calendar::types::DateDurationUnit;
/// use icu::calendar::Date;
///
/// let d1 = Date::try_new_iso(2025, 3, 31).unwrap();
/// let d2 = Date::try_new_iso(2026, 5, 15).unwrap();
///
/// let options_default = DateDifferenceOptions::default();
/// assert_eq!(
/// d1.try_until_with_options(&d2, options_default).unwrap(),
/// DateDuration::for_days(410)
/// );
///
/// let mut options_days = options_default;
/// options_days.largest_unit = Some(DateDurationUnit::Days);
/// assert_eq!(
/// d1.try_until_with_options(&d2, options_default).unwrap(),
/// DateDuration::for_days(410)
/// );
///
/// let mut options_weeks = options_default;
/// options_weeks.largest_unit = Some(DateDurationUnit::Weeks);
/// assert_eq!(
/// d1.try_until_with_options(&d2, options_weeks).unwrap(),
/// DateDuration {
/// weeks: 58,
/// days: 4,
/// ..Default::default()
/// }
/// );
///
/// let mut options_months = options_default;
/// options_months.largest_unit = Some(DateDurationUnit::Months);
/// assert_eq!(
/// d1.try_until_with_options(&d2, options_months).unwrap(),
/// DateDuration {
/// months: 13,
/// days: 15,
/// ..Default::default()
/// }
/// );
///
/// let mut options_years = options_default;
/// options_years.largest_unit = Some(DateDurationUnit::Years);
/// assert_eq!(
/// d1.try_until_with_options(&d2, options_years).unwrap(),
/// DateDuration {
/// years: 1,
/// months: 1,
/// days: 15,
/// ..Default::default()
/// }
/// );
/// ```
///
/// [`Months`]: crate::types::DateDurationUnit::Months
/// [`Years`]: crate::types::DateDurationUnit::Years
/// [`DateDuration`]: crate::types::DateDuration
pub largest_unit: Option,
}
/// Whether to constrain or reject out-of-bounds values when constructing a Date.
///
/// The behavior conforms to the ECMAScript Temporal specification.
///
///
/// 🚧 This code is considered unstable; it may change at any time, in breaking or non-breaking ways,
/// including in SemVer minor releases. Do not use this type unless you are prepared for things to occasionally break.
///
/// Graduation tracking issue: [issue #7161](https://github.com/unicode-org/icu4x/issues/7161).
///
///
/// ✨ *Enabled with the `unstable` Cargo feature.*
#[derive(Copy, Clone, Debug, PartialEq, Default)]
#[non_exhaustive]
pub enum Overflow {
/// Constrain out-of-bounds fields to the nearest in-bounds value.
///
/// Only the out-of-bounds field is constrained. If the other fields are not themselves
/// out of bounds, they are not changed.
///
/// This is the [default option](
/// https://tc39.es/proposal-temporal/#sec-temporal-gettemporaloverflowoption),
/// following the ECMAScript Temporal specification. See also the [docs on MDN](
/// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDate#invalid_date_clamping).
///
/// # Examples
///
/// ```
/// use icu::calendar::cal::Hebrew;
/// use icu::calendar::options::DateFromFieldsOptions;
/// use icu::calendar::options::Overflow;
/// use icu::calendar::types::DateFields;
/// use icu::calendar::Date;
/// use icu::calendar::DateError;
///
/// let mut options = DateFromFieldsOptions::default();
/// options.overflow = Some(Overflow::Constrain);
///
/// // 5784, a leap year, contains M05L, but the day is too big.
/// let mut fields = DateFields::default();
/// fields.extended_year = Some(5784);
/// fields.month_code = Some(b"M05L");
/// fields.day = Some(50);
///
/// let date = Date::try_from_fields(fields, options, Hebrew).unwrap();
///
/// // Constrained to the 30th day of M05L of year 5784
/// assert_eq!(date.year().extended_year(), 5784);
/// assert_eq!(date.month().standard_code.0, "M05L");
/// assert_eq!(date.day_of_month().0, 30);
///
/// // 5785, a common year, does not contain M05L.
/// fields.extended_year = Some(5785);
///
/// let date = Date::try_from_fields(fields, options, Hebrew).unwrap();
///
/// // Constrained to the 29th day of M06 of year 5785
/// assert_eq!(date.year().extended_year(), 5785);
/// assert_eq!(date.month().standard_code.0, "M06");
/// assert_eq!(date.day_of_month().0, 29);
/// ```
Constrain,
/// Return an error if any fields are out of bounds.
///
/// # Examples
///
/// ```
/// use icu::calendar::cal::Hebrew;
/// use icu::calendar::error::DateFromFieldsError;
/// use icu::calendar::options::DateFromFieldsOptions;
/// use icu::calendar::options::Overflow;
/// use icu::calendar::types::DateFields;
/// use icu::calendar::Date;
/// use tinystr::tinystr;
///
/// let mut options = DateFromFieldsOptions::default();
/// options.overflow = Some(Overflow::Reject);
///
/// // 5784, a leap year, contains M05L, but the day is too big.
/// let mut fields = DateFields::default();
/// fields.extended_year = Some(5784);
/// fields.month_code = Some(b"M05L");
/// fields.day = Some(50);
///
/// let err = Date::try_from_fields(fields, options, Hebrew)
/// .expect_err("Day is out of bounds");
/// assert!(matches!(err, DateFromFieldsError::Range { .. }));
///
/// // Set the day to one that exists
/// fields.day = Some(1);
/// Date::try_from_fields(fields, options, Hebrew)
/// .expect("A valid Hebrew date");
///
/// // 5785, a common year, does not contain M05L.
/// fields.extended_year = Some(5785);
///
/// let err = Date::try_from_fields(fields, options, Hebrew)
/// .expect_err("Month is out of bounds");
/// assert!(matches!(err, DateFromFieldsError::MonthCodeNotInYear));
/// ```
#[default]
Reject,
}
/// How to infer missing fields when the fields that are present do not fully constitute a Date.
///
/// In order for the fields to fully constitute a Date, they must identify a year, a month,
/// and a day. The fields `era`, `era_year`, and `extended_year` identify the year:
///
/// | Era? | Era Year? | Extended Year? | Outcome |
/// |------|-----------|----------------|---------|
/// | - | - | - | Error |
/// | Some | - | - | Error |
/// | - | Some | - | Error |
/// | - | - | Some | OK |
/// | Some | Some | - | OK |
/// | Some | - | Some | Error (era requires era year) |
/// | - | Some | Some | Error (era year requires era) |
/// | Some | Some | Some | OK (but error if inconsistent) |
///
/// The fields `month_code` and `ordinal_month` identify the month:
///
/// | Month Code? | Ordinal Month? | Outcome |
/// |-------------|----------------|---------|
/// | - | - | Error |
/// | Some | - | OK |
/// | - | Some | OK |
/// | Some | Some | OK (but error if inconsistent) |
///
/// The field `day` identifies the day.
///
/// If the fields identify a year, a month, and a day, then there are no missing fields, so
/// the strategy chosen here has no effect (fields that are out-of-bounds or inconsistent
/// are handled by other errors).
///
///
/// 🚧 This code is considered unstable; it may change at any time, in breaking or non-breaking ways,
/// including in SemVer minor releases. Do not use this type unless you are prepared for things to occasionally break.
///
/// Graduation tracking issue: [issue #7161](https://github.com/unicode-org/icu4x/issues/7161).
///
///
/// ✨ *Enabled with the `unstable` Cargo feature.*
#[derive(Copy, Clone, Debug, PartialEq, Default)]
#[non_exhaustive]
pub enum MissingFieldsStrategy {
/// If the fields that are present do not fully constitute a Date,
/// return [`DateFromFieldsError::NotEnoughFields`].
///
/// [`DateFromFieldsError::NotEnoughFields`]: crate::error::DateFromFieldsError::NotEnoughFields
#[default]
Reject,
/// If the fields that are present do not fully constitute a Date,
/// follow the ECMAScript specification when possible.
///
/// ⚠️ This option causes a year or day to be implicitly added to the Date!
///
/// This strategy makes the following changes:
///
/// 1. If the fields identify a year and a month, but not a day, then set `day` to 1.
/// 2. If `month_code` and `day` are set but nothing else, then set the year to a
/// _reference year_: some year in the calendar that contains the specified month
/// and day, according to the ECMAScript specification.
///
/// Note that the reference year is _not_ added if an ordinal month is present, since
/// the identity of an ordinal month changes from year to year.
Ecma,
}
}
#[cfg(test)]
mod tests {
use crate::{error::DateFromFieldsError, types::DateFields, Date, Gregorian};
use itertools::Itertools;
use std::collections::{BTreeMap, BTreeSet};
use super::*;
#[test]
#[allow(clippy::field_reassign_with_default)] // want out-of-crate code style
fn test_missing_fields_strategy() {
// The sets of fields that identify a year, according to the table in the docs
let valid_year_field_sets = [
&["era", "era_year"][..],
&["extended_year"][..],
&["era", "era_year", "extended_year"][..],
]
.into_iter()
.map(|field_names| field_names.iter().copied().collect())
.collect::>>();
// The sets of fields that identify a month, according to the table in the docs
let valid_month_field_sets = [
&["month_code"][..],
&["ordinal_month"][..],
&["month_code", "ordinal_month"][..],
]
.into_iter()
.map(|field_names| field_names.iter().copied().collect())
.collect::>>();
// The sets of fields that identify a day, according to the table in the docs
let valid_day_field_sets = [&["day"][..]]
.into_iter()
.map(|field_names| field_names.iter().copied().collect())
.collect::>>();
// All possible valid sets of fields
let all_valid_field_sets = valid_year_field_sets
.iter()
.cartesian_product(valid_month_field_sets.iter())
.cartesian_product(valid_day_field_sets.iter())
.map(|((year_fields, month_fields), day_fields)| {
year_fields
.iter()
.chain(month_fields.iter())
.chain(day_fields.iter())
.copied()
.collect::>()
})
.collect::>>();
// Field sets with year and month but without day that ECMA accepts
let field_sets_without_day = valid_year_field_sets
.iter()
.cartesian_product(valid_month_field_sets.iter())
.map(|(year_fields, month_fields)| {
year_fields
.iter()
.chain(month_fields.iter())
.copied()
.collect::>()
})
.collect::>>();
// Field sets with month and day but without year that ECMA accepts
let field_sets_without_year = [&["month_code", "day"][..]]
.into_iter()
.map(|field_names| field_names.iter().copied().collect())
.collect::>>();
// A map from field names to a function that sets that field
let mut field_fns = BTreeMap::<&str, &dyn Fn(&mut DateFields)>::new();
field_fns.insert("era", &|fields| fields.era = Some(b"ad"));
field_fns.insert("era_year", &|fields| fields.era_year = Some(2000));
field_fns.insert("extended_year", &|fields| fields.extended_year = Some(2000));
field_fns.insert("month_code", &|fields| fields.month_code = Some(b"M04"));
field_fns.insert("ordinal_month", &|fields| fields.ordinal_month = Some(4));
field_fns.insert("day", &|fields| fields.day = Some(20));
for field_set in field_fns.keys().copied().powerset() {
let field_set = field_set.into_iter().collect::>();
// Check whether this case should succeed: whether it identifies a year,
// a month, and a day.
let should_succeed_rejecting = all_valid_field_sets.contains(&field_set);
// Check whether it should succeed in ECMA mode.
let should_succeed_ecma = should_succeed_rejecting
|| field_sets_without_day.contains(&field_set)
|| field_sets_without_year.contains(&field_set);
// Populate the fields in the field set
let mut fields = Default::default();
for field_name in field_set {
field_fns.get(field_name).unwrap()(&mut fields);
}
// Check whether we were able to successfully construct the date
let mut options = DateFromFieldsOptions::default();
options.missing_fields_strategy = Some(MissingFieldsStrategy::Reject);
match Date::try_from_fields(fields, options, Gregorian) {
Ok(_) => assert!(
should_succeed_rejecting,
"Succeeded, but should have rejected: {fields:?}"
),
Err(DateFromFieldsError::NotEnoughFields) => assert!(
!should_succeed_rejecting,
"Rejected, but should have succeeded: {fields:?}"
),
Err(e) => panic!("Unexpected error: {e} for {fields:?}"),
}
// Check ECMA mode
let mut options = DateFromFieldsOptions::default();
options.missing_fields_strategy = Some(MissingFieldsStrategy::Ecma);
match Date::try_from_fields(fields, options, Gregorian) {
Ok(_) => assert!(
should_succeed_ecma,
"Succeeded, but should have rejected (ECMA): {fields:?}"
),
Err(DateFromFieldsError::NotEnoughFields) => assert!(
!should_succeed_ecma,
"Rejected, but should have succeeded (ECMA): {fields:?}"
),
Err(e) => panic!("Unexpected error: {e} for {fields:?}"),
}
}
}
#[test]
fn test_constrain_large_months() {
let fields = DateFields {
extended_year: Some(2004),
ordinal_month: Some(15),
day: Some(1),
..Default::default()
};
let options = DateFromFieldsOptions {
overflow: Some(Overflow::Constrain),
..Default::default()
};
let _ = Date::try_from_fields(fields, options, crate::cal::Persian).unwrap();
}
}