Skip to main content

ferroui_base/utilities/
gregorian_calendar.rs

1//! The Gregorian calendar operations the controls use
2//! (.NET `System.Globalization.GregorianCalendar`) and ISO 8601 week numbers
3//! (.NET `System.Globalization.ISOWeek`).
4
5use super::{CalendarWeekRule, DateTime, DayOfWeek};
6
7const OUT_OF_RANGE: &str = "The result is out of the supported range for this calendar.";
8
9/// The proleptic Gregorian calendar. It has no state.
10#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
11pub struct GregorianCalendar;
12
13impl GregorianCalendar {
14    /// Creates the calendar.
15    pub const fn new() -> GregorianCalendar {
16        GregorianCalendar
17    }
18
19    /// The earliest date the calendar supports.
20    pub const fn min_supported_date_time(&self) -> DateTime {
21        DateTime::MIN_VALUE
22    }
23
24    /// The latest date the calendar supports.
25    pub const fn max_supported_date_time(&self) -> DateTime {
26        DateTime::MAX_VALUE
27    }
28
29    /// `time` moved by a number of days; `None` when the result is out of
30    /// range (.NET throws `ArgumentException`).
31    pub fn try_add_days(&self, time: DateTime, days: i32) -> Option<DateTime> {
32        time.try_add_days(days as f64)
33    }
34
35    /// `time` moved by a number of days. Panics when the result is out of range.
36    #[track_caller]
37    pub fn add_days(&self, time: DateTime, days: i32) -> DateTime {
38        self.try_add_days(time, days).expect(OUT_OF_RANGE)
39    }
40
41    /// `time` moved by a number of months, keeping the day where the
42    /// resulting month has it; `None` when `months` is not in
43    /// -120000..=120000 or the result is out of range.
44    pub fn try_add_months(&self, time: DateTime, months: i32) -> Option<DateTime> {
45        time.try_add_months(months)
46    }
47
48    /// `time` moved by a number of months. Panics when out of range.
49    #[track_caller]
50    pub fn add_months(&self, time: DateTime, months: i32) -> DateTime {
51        self.try_add_months(time, months).expect(OUT_OF_RANGE)
52    }
53
54    /// `time` moved by a number of years; `None` when the result is out of range.
55    pub fn try_add_years(&self, time: DateTime, years: i32) -> Option<DateTime> {
56        years.checked_mul(12).and_then(|months| time.try_add_months(months))
57    }
58
59    /// `time` moved by a number of years. Panics when out of range.
60    #[track_caller]
61    pub fn add_years(&self, time: DateTime, years: i32) -> DateTime {
62        self.try_add_years(time, years).expect(OUT_OF_RANGE)
63    }
64
65    /// The day of the week of `time`.
66    pub fn get_day_of_week(&self, time: DateTime) -> DayOfWeek {
67        time.day_of_week()
68    }
69
70    /// The number of days of a month. Panics for an invalid year or month.
71    #[track_caller]
72    pub fn get_days_in_month(&self, year: i32, month: i32) -> i32 {
73        DateTime::days_in_month(year, month)
74    }
75
76    /// The number of the week of the year that contains `time`, counted from
77    /// one, for the given rule and first day of the week.
78    ///
79    /// As in .NET, the last days of December are never counted into the first
80    /// week of the following year: they get the number 53 (or 52) even where
81    /// ISO 8601 says 1. Use [`IsoWeek::get_week_of_year`] for ISO numbers.
82    pub fn get_week_of_year(&self, time: DateTime, rule: CalendarWeekRule, first_day_of_week: DayOfWeek) -> i32 {
83        match rule {
84            CalendarWeekRule::FirstDay => {
85                let day_of_year = time.day_of_year() - 1;
86                let offset = (Self::day_of_week_of_january_first(time) - first_day_of_week as i32 + 14) % 7;
87                (day_of_year + offset) / 7 + 1
88            }
89            CalendarWeekRule::FirstFullWeek => Self::week_of_year_with_minimum_days(time, first_day_of_week as i32, 7),
90            CalendarWeekRule::FirstFourDayWeek => Self::week_of_year_with_minimum_days(time, first_day_of_week as i32, 4),
91        }
92    }
93
94    /// The day of the week (as a number, possibly negative modulo seven) of
95    /// January 1 of the year of `time`.
96    fn day_of_week_of_january_first(time: DateTime) -> i32 {
97        time.day_of_week() as i32 - (time.day_of_year() - 1) % 7
98    }
99
100    fn week_of_year_with_minimum_days(time: DateTime, first_day_of_week: i32, minimum_days: i32) -> i32 {
101        let mut time = time;
102        loop {
103            let day_of_year = time.day_of_year() - 1;
104            // Days of the year that precede its first week.
105            let mut offset = (first_day_of_week - Self::day_of_week_of_january_first(time) + 14) % 7;
106            if offset != 0 && offset >= minimum_days {
107                offset -= 7;
108            }
109            let day = day_of_year - offset;
110            if day >= 0 {
111                return day / 7 + 1;
112            }
113            // The date belongs to the last week of the previous year.
114            match time.try_add_days(-(day_of_year as f64 + 1.0)) {
115                Some(previous) => time = previous,
116                None => return Self::week_of_year_before_first_year(first_day_of_week, minimum_days),
117            }
118        }
119    }
120
121    /// The week number of the first days of year 1 when they belong to the
122    /// last week of the (unrepresentable, 365 days long) year before it.
123    fn week_of_year_before_first_year(first_day_of_week: i32, minimum_days: i32) -> i32 {
124        // 0001-01-01 is a Monday; a year of 365 days before it starts one weekday earlier, on a Sunday.
125        let january_first_of_previous_year = DayOfWeek::Monday as i32 - 1 - 364 % 7;
126        let partial_week = (first_day_of_week - january_first_of_previous_year + 14) % 7;
127        let mut day = 364 - partial_week;
128        if partial_week >= minimum_days {
129            day += 7;
130        }
131        day / 7 + 1
132    }
133}
134
135/// ISO 8601 week numbering (.NET `System.Globalization.ISOWeek`).
136pub struct IsoWeek;
137
138impl IsoWeek {
139    /// The ISO week number of `date`, 1..=53: weeks start on Monday and the
140    /// first week of a year is the one that contains its first Thursday.
141    pub fn get_week_of_year(date: DateTime) -> i32 {
142        date.to_date().iso_week() as i32
143    }
144}