Skip to main content

icu_calendar/
duration.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/// A signed length of time in terms of days, weeks, months, and years.
6///
7/// This type represents the abstract concept of a date duration. For example, a duration of
8/// "1 month" is represented as "1 month" in the data model, without any context of how many
9/// days the month might be.
10///
11/// Use [`DateDuration`] for calculating the difference between two [`Date`]s and adding
12/// date units to a [`Date`].
13///
14/// [`Date`]: crate::Date
15///
16/// # Example
17///
18/// ```rust
19/// use icu::calendar::options::DateDifferenceOptions;
20/// use icu::calendar::types::DateDuration;
21/// use icu::calendar::types::DateDurationUnit;
22/// use icu::calendar::types::Weekday;
23/// use icu::calendar::Date;
24///
25/// // Creating ISO date: 1992-09-02.
26/// let mut date_iso = Date::try_new_iso(1992, 9, 2)
27///     .expect("Failed to initialize ISO Date instance.");
28///
29/// assert_eq!(date_iso.day_of_week(), Weekday::Wednesday);
30/// assert_eq!(date_iso.era_year().year, 1992);
31/// assert_eq!(date_iso.month().ordinal, 9);
32/// assert_eq!(date_iso.day_of_month().0, 2);
33///
34/// // Answering questions about days in month and year.
35/// assert_eq!(date_iso.days_in_year(), 366);
36/// assert_eq!(date_iso.days_in_month(), 30);
37///
38/// // Advancing date in-place by 1 year, 2 months, 3 weeks, 4 days.
39/// date_iso
40///     .try_add_with_options(
41///         DateDuration {
42///             is_negative: false,
43///             years: 1,
44///             months: 2,
45///             weeks: 3,
46///             days: 4,
47///         },
48///         Default::default(),
49///     )
50///     .unwrap();
51/// assert_eq!(date_iso.era_year().year, 1993);
52/// assert_eq!(date_iso.month().ordinal, 11);
53/// assert_eq!(date_iso.day_of_month().0, 27);
54///
55/// // Reverse date advancement.
56/// date_iso
57///     .try_add_with_options(
58///         DateDuration {
59///             is_negative: true,
60///             years: 1,
61///             months: 2,
62///             weeks: 3,
63///             days: 4,
64///         },
65///         Default::default(),
66///     )
67///     .unwrap();
68/// assert_eq!(date_iso.era_year().year, 1992);
69/// assert_eq!(date_iso.month().ordinal, 9);
70/// assert_eq!(date_iso.day_of_month().0, 2);
71///
72/// // Creating ISO date: 2022-01-30.
73/// let newer_date_iso = Date::try_new_iso(2022, 10, 30)
74///     .expect("Failed to initialize ISO Date instance.");
75///
76/// // Comparing dates: 2022-01-30 and 1992-09-02.
77/// let mut options = DateDifferenceOptions::default();
78/// options.largest_unit = Some(DateDurationUnit::Years);
79/// let Ok(duration) =
80///     newer_date_iso.try_until_with_options(&date_iso, options);
81/// assert_eq!(duration.years, 30);
82/// assert_eq!(duration.months, 1);
83/// assert_eq!(duration.days, 28);
84///
85/// // Create new date with date advancement. Reassign to new variable.
86/// let mutated_date_iso = date_iso
87///     .try_added_with_options(
88///         DateDuration {
89///             is_negative: false,
90///             years: 1,
91///             months: 2,
92///             weeks: 3,
93///             days: 4,
94///         },
95///         Default::default(),
96///     )
97///     .unwrap();
98/// assert_eq!(mutated_date_iso.era_year().year, 1993);
99/// assert_eq!(mutated_date_iso.month().ordinal, 11);
100/// assert_eq!(mutated_date_iso.day_of_month().0, 27);
101/// ```
102///
103/// Currently unstable for ICU4X 1.0
104///
105/// <div class="stab unstable">
106/// 🚧 This code is considered unstable; it may change at any time, in breaking or non-breaking ways,
107/// including in SemVer minor releases. Do not use this type unless you are prepared for things to occasionally break.
108///
109/// Graduation tracking issue: [issue #3964](https://github.com/unicode-org/icu4x/issues/3964).
110/// </div>
111///
112/// ✨ *Enabled with the `unstable` Cargo feature.*
113#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
114#[allow(clippy::exhaustive_structs)] // spec-defined in Temporal
115pub struct DateDuration {
116    /// Whether the duration is negative.
117    ///
118    /// A negative duration is an abstract concept that could result, for example, from
119    /// taking the difference between two [`Date`](crate::Date)s.
120    ///
121    /// The fields of the duration are either all positive or all negative. Mixed signs
122    /// are not allowed.
123    ///
124    /// By convention, this field should be `false` if the duration is zero.
125    pub is_negative: bool,
126    /// The number of years
127    pub years: u32,
128    /// The number of months
129    pub months: u32,
130    /// The number of weeks
131    pub weeks: u32,
132    /// The number of days
133    pub days: u64,
134}
135
136/// A "duration unit" used to specify the minimum or maximum duration of time to
137/// care about
138///
139/// <div class="stab unstable">
140/// 🚧 This code is considered unstable; it may change at any time, in breaking or non-breaking ways,
141/// including in SemVer minor releases. Do not use this type unless you are prepared for things to occasionally break.
142///
143/// Graduation tracking issue: [issue #3964](https://github.com/unicode-org/icu4x/issues/3964).
144/// </div>
145///
146/// ✨ *Enabled with the `unstable` Cargo feature.*
147#[derive(Copy, Clone, Eq, PartialEq, Debug)]
148#[allow(clippy::exhaustive_enums)] // this type should be stable
149pub enum DateDurationUnit {
150    /// Duration in years
151    Years,
152    /// Duration in months
153    Months,
154    /// Duration in weeks
155    Weeks,
156    /// Duration in days
157    Days,
158}
159
160impl DateDuration {
161    /// Returns a new [`DateDuration`] representing a number of years.
162    pub fn for_years(years: i32) -> Self {
163        Self {
164            is_negative: years.is_negative(),
165            years: years.unsigned_abs(),
166            ..Default::default()
167        }
168    }
169
170    /// Returns a new [`DateDuration`] representing a number of months.
171    pub fn for_months(months: i32) -> Self {
172        Self {
173            is_negative: months.is_negative(),
174            months: months.unsigned_abs(),
175            ..Default::default()
176        }
177    }
178
179    /// Returns a new [`DateDuration`] representing a number of weeks.
180    pub fn for_weeks(weeks: i32) -> Self {
181        Self {
182            is_negative: weeks.is_negative(),
183            weeks: weeks.unsigned_abs(),
184            ..Default::default()
185        }
186    }
187
188    /// Returns a new [`DateDuration`] representing a number of days.
189    pub fn for_days(days: i64) -> Self {
190        Self {
191            is_negative: days.is_negative(),
192            days: days.unsigned_abs(),
193            ..Default::default()
194        }
195    }
196
197    /// Do NOT pass this function values of mixed signs!
198    pub(crate) fn from_signed_ymwd(years: i64, months: i64, weeks: i64, days: i64) -> Self {
199        let is_negative = years.is_negative()
200            || months.is_negative()
201            || weeks.is_negative()
202            || days.is_negative();
203        if is_negative
204            && (years.is_positive()
205                || months.is_positive()
206                || weeks.is_positive()
207                || days.is_positive())
208        {
209            debug_assert!(false, "mixed signs in from_signed_ymd");
210        }
211        Self {
212            is_negative,
213            years: match u32::try_from(years.unsigned_abs()) {
214                Ok(x) => x,
215                Err(_) => {
216                    debug_assert!(false, "years out of range");
217                    u32::MAX
218                }
219            },
220            months: match u32::try_from(months.unsigned_abs()) {
221                Ok(x) => x,
222                Err(_) => {
223                    debug_assert!(false, "months out of range");
224                    u32::MAX
225                }
226            },
227            weeks: match u32::try_from(weeks.unsigned_abs()) {
228                Ok(x) => x,
229                Err(_) => {
230                    debug_assert!(false, "weeks out of range");
231                    u32::MAX
232                }
233            },
234            days: days.unsigned_abs(),
235        }
236    }
237
238    #[inline]
239    pub(crate) fn add_years_to(&self, year: i32) -> i32 {
240        if !self.is_negative {
241            match year.checked_add_unsigned(self.years) {
242                Some(x) => x,
243                None => {
244                    debug_assert!(false, "{year} + {self:?} out of year range");
245                    i32::MAX
246                }
247            }
248        } else {
249            match year.checked_sub_unsigned(self.years) {
250                Some(x) => x,
251                None => {
252                    debug_assert!(false, "{year} - {self:?} out of year range");
253                    i32::MIN
254                }
255            }
256        }
257    }
258
259    #[inline]
260    pub(crate) fn add_months_to(&self, month: u8) -> i64 {
261        if !self.is_negative {
262            i64::from(month) + i64::from(self.months)
263        } else {
264            i64::from(month) - i64::from(self.months)
265        }
266    }
267
268    #[inline]
269    pub(crate) fn add_weeks_and_days_to(&self, day: u8) -> i64 {
270        if !self.is_negative {
271            let day = i64::from(day) + i64::from(self.weeks) * 7;
272            match day.checked_add_unsigned(self.days) {
273                Some(x) => x,
274                None => {
275                    debug_assert!(false, "{day} + {self:?} out of day range");
276                    i64::MAX
277                }
278            }
279        } else {
280            let day = i64::from(day) - i64::from(self.weeks) * 7;
281            match day.checked_sub_unsigned(self.days) {
282                Some(x) => x,
283                None => {
284                    debug_assert!(false, "{day} - {self:?} out of day range");
285                    i64::MIN
286                }
287            }
288        }
289    }
290}