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}