icu_calendar/
calendar_arithmetic.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
5use crate::{types, Calendar, CalendarError, DateDuration, DateDurationUnit};
6use core::cmp::Ordering;
7use core::convert::TryInto;
8use core::fmt::Debug;
9use core::hash::{Hash, Hasher};
10use core::marker::PhantomData;
11use tinystr::tinystr;
12
13// Note: The Ord/PartialOrd impls can be derived because the fields are in the correct order.
14#[derive(Debug)]
15#[allow(clippy::exhaustive_structs)] // this type is stable
16pub(crate) struct ArithmeticDate<C: CalendarArithmetic> {
17    pub year: i32,
18    /// 1-based month of year
19    pub month: u8,
20    /// 1-based day of month
21    pub day: u8,
22    /// Invariant: MUST be updated to match the info for `year` whenever `year` is updated or set.
23    pub year_info: C::YearInfo,
24    marker: PhantomData<C>,
25}
26
27// Manual impls since the derive will introduce a C: Trait bound
28// and many of these impls can ignore the year_info field
29impl<C: CalendarArithmetic> Copy for ArithmeticDate<C> {}
30impl<C: CalendarArithmetic> Clone for ArithmeticDate<C> {
31    fn clone(&self) -> Self {
32        *self
33    }
34}
35
36impl<C: CalendarArithmetic> PartialEq for ArithmeticDate<C> {
37    fn eq(&self, other: &Self) -> bool {
38        self.year == other.year && self.month == other.month && self.day == other.day
39    }
40}
41
42impl<C: CalendarArithmetic> Eq for ArithmeticDate<C> {}
43
44impl<C: CalendarArithmetic> Ord for ArithmeticDate<C> {
45    fn cmp(&self, other: &Self) -> Ordering {
46        self.year
47            .cmp(&other.year)
48            .then(self.month.cmp(&other.month))
49            .then(self.day.cmp(&other.day))
50    }
51}
52
53impl<C: CalendarArithmetic> PartialOrd for ArithmeticDate<C> {
54    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
55        Some(self.cmp(other))
56    }
57}
58
59impl<C: CalendarArithmetic> Hash for ArithmeticDate<C> {
60    fn hash<H>(&self, state: &mut H)
61    where
62        H: Hasher,
63    {
64        self.year.hash(state);
65        self.month.hash(state);
66        self.day.hash(state);
67    }
68}
69
70/// Maximum number of iterations when iterating through the days of a month; can be increased if necessary
71#[allow(dead_code)] // TODO: Remove dead code tag after use
72pub(crate) const MAX_ITERS_FOR_DAYS_OF_MONTH: u8 = 33;
73
74pub(crate) trait CalendarArithmetic: Calendar {
75    /// In case we plan to cache per-year data, this stores
76    /// useful computational information for the current year
77    /// as a field on ArithmeticDate
78    type YearInfo: Copy + Debug;
79
80    // TODO(#3933): potentially make these methods take &self instead, and absorb certain y/m parameters
81    // based on usage patterns (e.g month_days is only ever called with self.year)
82    fn month_days(year: i32, month: u8, year_info: Self::YearInfo) -> u8;
83    fn months_for_every_year(year: i32, year_info: Self::YearInfo) -> u8;
84    fn is_leap_year(year: i32, year_info: Self::YearInfo) -> bool;
85    fn last_month_day_in_year(year: i32, year_info: Self::YearInfo) -> (u8, u8);
86
87    /// Calculate the days in a given year
88    /// Can be overridden with simpler implementations for solar calendars
89    /// (typically, 366 in leap, 365 otherwise) Leave this as the default
90    /// for lunar calendars
91    ///
92    /// The name has `provided` in it to avoid clashes with Calendar
93    fn days_in_provided_year(year: i32, year_info: Self::YearInfo) -> u16 {
94        let months_in_year = Self::months_for_every_year(year, year_info);
95        let mut days: u16 = 0;
96        for month in 1..=months_in_year {
97            days += Self::month_days(year, month, year_info) as u16;
98        }
99        days
100    }
101}
102
103pub(crate) trait PrecomputedDataSource<YearInfo> {
104    /// Given a calendar year, load (or compute) the YearInfo for it
105    ///
106    /// In the future we may pass in an optional previous YearInfo alongside the year
107    /// it matches to allow code to take shortcuts.
108    fn load_or_compute_info(&self, year: i32) -> YearInfo;
109}
110
111impl PrecomputedDataSource<()> for () {
112    fn load_or_compute_info(&self, _year: i32) {}
113}
114
115impl<C: CalendarArithmetic> ArithmeticDate<C> {
116    /// Create a new `ArithmeticDate` without checking that `month` and `day` are in bounds.
117    #[inline]
118    pub const fn new_unchecked(year: i32, month: u8, day: u8) -> Self
119    where
120        C: CalendarArithmetic<YearInfo = ()>,
121    {
122        Self::new_unchecked_with_info(year, month, day, ())
123    }
124    /// Create a new `ArithmeticDate` without checking that `month` and `day` are in bounds.
125    #[inline]
126    pub const fn new_unchecked_with_info(
127        year: i32,
128        month: u8,
129        day: u8,
130        year_info: C::YearInfo,
131    ) -> Self {
132        ArithmeticDate {
133            year,
134            month,
135            day,
136            year_info,
137            marker: PhantomData,
138        }
139    }
140
141    #[inline]
142    pub fn min_date() -> Self
143    where
144        C: CalendarArithmetic<YearInfo = ()>,
145    {
146        ArithmeticDate {
147            year: i32::MIN,
148            month: 1,
149            day: 1,
150            year_info: (),
151            marker: PhantomData,
152        }
153    }
154
155    #[inline]
156    pub fn max_date() -> Self
157    where
158        C: CalendarArithmetic<YearInfo = ()>,
159    {
160        let year = i32::MAX;
161        let (month, day) = C::last_month_day_in_year(year, ());
162        ArithmeticDate {
163            year: i32::MAX,
164            month,
165            day,
166            year_info: (),
167            marker: PhantomData,
168        }
169    }
170
171    #[inline]
172    fn offset_days(&mut self, mut day_offset: i32, data: &impl PrecomputedDataSource<C::YearInfo>) {
173        while day_offset != 0 {
174            let month_days = C::month_days(self.year, self.month, self.year_info);
175            if self.day as i32 + day_offset > month_days as i32 {
176                self.offset_months(1, data);
177                day_offset -= month_days as i32;
178            } else if self.day as i32 + day_offset < 1 {
179                self.offset_months(-1, data);
180                day_offset += C::month_days(self.year, self.month, self.year_info) as i32;
181            } else {
182                self.day = (self.day as i32 + day_offset) as u8;
183                day_offset = 0;
184            }
185        }
186    }
187
188    #[inline]
189    fn offset_months(
190        &mut self,
191        mut month_offset: i32,
192        data: &impl PrecomputedDataSource<C::YearInfo>,
193    ) {
194        while month_offset != 0 {
195            let year_months = C::months_for_every_year(self.year, self.year_info);
196            if self.month as i32 + month_offset > year_months as i32 {
197                self.year += 1;
198                self.year_info = data.load_or_compute_info(self.year);
199                month_offset -= year_months as i32;
200            } else if self.month as i32 + month_offset < 1 {
201                self.year -= 1;
202                self.year_info = data.load_or_compute_info(self.year);
203                month_offset += C::months_for_every_year(self.year, self.year_info) as i32;
204            } else {
205                self.month = (self.month as i32 + month_offset) as u8;
206                month_offset = 0
207            }
208        }
209    }
210
211    #[inline]
212    pub fn offset_date(
213        &mut self,
214        offset: DateDuration<C>,
215        data: &impl PrecomputedDataSource<C::YearInfo>,
216    ) {
217        if offset.years != 0 {
218            // For offset_date to work with lunar calendars, need to handle an edge case where the original month is not valid in the future year.
219            self.year += offset.years;
220            self.year_info = data.load_or_compute_info(self.year);
221        }
222
223        self.offset_months(offset.months, data);
224
225        let day_offset = offset.days + offset.weeks * 7 + self.day as i32 - 1;
226        self.day = 1;
227        self.offset_days(day_offset, data);
228    }
229
230    #[inline]
231    pub fn until(
232        &self,
233        date2: ArithmeticDate<C>,
234        _largest_unit: DateDurationUnit,
235        _smaller_unit: DateDurationUnit,
236    ) -> DateDuration<C> {
237        // This simple implementation does not need C::PrecomputedDataSource right now, but it
238        // likely will once we've written a proper implementation
239        DateDuration::new(
240            self.year - date2.year,
241            self.month as i32 - date2.month as i32,
242            0,
243            self.day as i32 - date2.day as i32,
244        )
245    }
246
247    #[inline]
248    pub fn days_in_year(&self) -> u16 {
249        C::days_in_provided_year(self.year, self.year_info)
250    }
251
252    #[inline]
253    pub fn months_in_year(&self) -> u8 {
254        C::months_for_every_year(self.year, self.year_info)
255    }
256
257    #[inline]
258    pub fn days_in_month(&self) -> u8 {
259        C::month_days(self.year, self.month, self.year_info)
260    }
261
262    #[inline]
263    pub fn day_of_year(&self) -> u16 {
264        let mut day_of_year = 0;
265        for month in 1..self.month {
266            day_of_year += C::month_days(self.year, month, self.year_info) as u16;
267        }
268        day_of_year + (self.day as u16)
269    }
270
271    #[inline]
272    pub fn date_from_year_day(year: i32, year_day: u32) -> ArithmeticDate<C>
273    where
274        C: CalendarArithmetic<YearInfo = ()>,
275    {
276        let mut month = 1;
277        let mut day = year_day as i32;
278        while month <= C::months_for_every_year(year, ()) {
279            let month_days = C::month_days(year, month, ()) as i32;
280            if day <= month_days {
281                break;
282            } else {
283                day -= month_days;
284                month += 1;
285            }
286        }
287
288        debug_assert!(day <= C::month_days(year, month, ()) as i32);
289        #[allow(clippy::unwrap_used)]
290        // The day is expected to be within the range of month_days of C
291        ArithmeticDate {
292            year,
293            month,
294            day: day.try_into().unwrap_or(0),
295            year_info: (),
296            marker: PhantomData,
297        }
298    }
299
300    #[inline]
301    pub fn day_of_month(&self) -> types::DayOfMonth {
302        types::DayOfMonth(self.day.into())
303    }
304
305    /// The [`types::FormattableMonth`] for the current month (with month code) for a solar calendar
306    /// Lunar calendars should not use this method and instead manually implement a month code
307    /// resolver.
308    /// Originally "solar_month" but renamed because it can be used for some lunar calendars
309    ///
310    /// Returns "und" if run with months that are out of bounds for the current
311    /// calendar.
312    #[inline]
313    pub fn month(&self) -> types::FormattableMonth {
314        let code = match self.month {
315            a if a > C::months_for_every_year(self.year, self.year_info) => tinystr!(4, "und"),
316            1 => tinystr!(4, "M01"),
317            2 => tinystr!(4, "M02"),
318            3 => tinystr!(4, "M03"),
319            4 => tinystr!(4, "M04"),
320            5 => tinystr!(4, "M05"),
321            6 => tinystr!(4, "M06"),
322            7 => tinystr!(4, "M07"),
323            8 => tinystr!(4, "M08"),
324            9 => tinystr!(4, "M09"),
325            10 => tinystr!(4, "M10"),
326            11 => tinystr!(4, "M11"),
327            12 => tinystr!(4, "M12"),
328            13 => tinystr!(4, "M13"),
329            _ => tinystr!(4, "und"),
330        };
331        types::FormattableMonth {
332            ordinal: self.month as u32,
333            code: types::MonthCode(code),
334        }
335    }
336
337    /// Construct a new arithmetic date from a year, month code, and day, bounds checking
338    /// the month and day
339    /// Originally (new_from_solar_codes) but renamed because it works for some lunar calendars
340    pub fn new_from_codes<C2: Calendar>(
341        // Separate type since the debug_name() impl may differ when DateInner types
342        // are nested (e.g. in GregorianDateInner)
343        cal: &C2,
344        year: i32,
345        month_code: types::MonthCode,
346        day: u8,
347    ) -> Result<Self, CalendarError>
348    where
349        C: CalendarArithmetic<YearInfo = ()>,
350    {
351        let month = if let Some((ordinal, false)) = month_code.parsed() {
352            ordinal
353        } else {
354            return Err(CalendarError::UnknownMonthCode(
355                month_code.0,
356                cal.debug_name(),
357            ));
358        };
359
360        if month > C::months_for_every_year(year, ()) {
361            return Err(CalendarError::UnknownMonthCode(
362                month_code.0,
363                cal.debug_name(),
364            ));
365        }
366
367        let max_day = C::month_days(year, month, ());
368        if day > max_day {
369            return Err(CalendarError::Overflow {
370                field: "day",
371                max: max_day as usize,
372            });
373        }
374
375        Ok(Self::new_unchecked(year, month, day))
376    }
377
378    /// Construct a new arithmetic date from a year, month ordinal, and day, bounds checking
379    /// the month and day
380    /// Originally (new_from_solar_ordinals) but renamed because it works for some lunar calendars
381    pub fn new_from_ordinals(year: i32, month: u8, day: u8) -> Result<Self, CalendarError>
382    where
383        C: CalendarArithmetic<YearInfo = ()>,
384    {
385        Self::new_from_ordinals_with_info(year, month, day, ())
386    }
387
388    /// Construct a new arithmetic date from a year, month ordinal, and day, bounds checking
389    /// the month and day
390    pub fn new_from_ordinals_with_info(
391        year: i32,
392        month: u8,
393        day: u8,
394        info: C::YearInfo,
395    ) -> Result<Self, CalendarError> {
396        let max_month = C::months_for_every_year(year, info);
397        if month > max_month {
398            return Err(CalendarError::Overflow {
399                field: "month",
400                max: max_month as usize,
401            });
402        }
403        let max_day = C::month_days(year, month, info);
404        if day > max_day {
405            return Err(CalendarError::Overflow {
406                field: "day",
407                max: max_day as usize,
408            });
409        }
410
411        Ok(Self::new_unchecked_with_info(year, month, day, info))
412    }
413}
414
415#[cfg(test)]
416mod tests {
417    use super::*;
418    use crate::Iso;
419
420    #[test]
421    fn test_ord() {
422        let dates_in_order = [
423            ArithmeticDate::<Iso>::new_unchecked(-10, 1, 1),
424            ArithmeticDate::<Iso>::new_unchecked(-10, 1, 2),
425            ArithmeticDate::<Iso>::new_unchecked(-10, 2, 1),
426            ArithmeticDate::<Iso>::new_unchecked(-1, 1, 1),
427            ArithmeticDate::<Iso>::new_unchecked(-1, 1, 2),
428            ArithmeticDate::<Iso>::new_unchecked(-1, 2, 1),
429            ArithmeticDate::<Iso>::new_unchecked(0, 1, 1),
430            ArithmeticDate::<Iso>::new_unchecked(0, 1, 2),
431            ArithmeticDate::<Iso>::new_unchecked(0, 2, 1),
432            ArithmeticDate::<Iso>::new_unchecked(1, 1, 1),
433            ArithmeticDate::<Iso>::new_unchecked(1, 1, 2),
434            ArithmeticDate::<Iso>::new_unchecked(1, 2, 1),
435            ArithmeticDate::<Iso>::new_unchecked(10, 1, 1),
436            ArithmeticDate::<Iso>::new_unchecked(10, 1, 2),
437            ArithmeticDate::<Iso>::new_unchecked(10, 2, 1),
438        ];
439        for (i, i_date) in dates_in_order.iter().enumerate() {
440            for (j, j_date) in dates_in_order.iter().enumerate() {
441                let result1 = i_date.cmp(j_date);
442                let result2 = j_date.cmp(i_date);
443                assert_eq!(result1.reverse(), result2);
444                assert_eq!(i.cmp(&j), i_date.cmp(j_date));
445            }
446        }
447    }
448}