Skip to main content

rustolio_utils/time/calendar/
month.rs

1//
2// SPDX-License-Identifier: MPL-2.0
3//
4// Copyright (c) 2026 Tobias Binnewies. All rights reserved.
5//
6// This Source Code Form is subject to the terms of the Mozilla Public
7// License, v. 2.0. If a copy of the MPL was not distributed with this
8// file, You can obtain one at http://mozilla.org/MPL/2.0/.
9//
10
11use time::util::is_leap_year;
12
13use super::super::{Date, Month, Weekday};
14
15use super::CalendarWeek;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
18pub struct CalendarMonth {
19    year: i32,
20    month: Month,
21}
22
23impl std::fmt::Display for CalendarMonth {
24    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25        write!(f, "{} {}", self.month, self.year)
26    }
27}
28
29impl CalendarMonth {
30    pub(in super::super) const fn new(year: i32, month: Month) -> Self {
31        Self { year, month }
32    }
33
34    pub fn prev(&self) -> CalendarMonth {
35        let (month, year) = match self.month {
36            Month::January => (Month::December, self.year - 1),
37            _ => (self.month.previous(), self.year),
38        };
39        Self { year, month }
40    }
41
42    pub fn next(&self) -> CalendarMonth {
43        let (month, year) = match self.month {
44            Month::December => (Month::January, self.year + 1),
45            _ => (self.month.next(), self.year),
46        };
47        Self { year, month }
48    }
49
50    pub fn days(&self) -> u8 {
51        match self.month {
52            Month::April | Month::June | Month::September | Month::November => 30,
53            Month::February => {
54                if is_leap_year(self.year) {
55                    29
56                } else {
57                    28
58                }
59            }
60            _ => 31,
61        }
62    }
63
64    pub fn weeks(&self, starting_weekday: Weekday) -> Vec<CalendarWeek> {
65        let days = self.days();
66
67        let first_day = Date::new(1, self.month, self.year).unwrap();
68        let last_day = Date::new(days, self.month, self.year).unwrap();
69        let first_weekday = first_day.weekday();
70        let first_week = first_day.iso_week();
71        let last_week = last_day.iso_week();
72
73        let to_fill = apply_starting_weekday(first_weekday, starting_weekday);
74
75        let mut weeks = Vec::with_capacity(6);
76        let mut current_weekdays = [(0, false); 7];
77
78        let mut idx = 0;
79
80        // Fill first week with last days of prev months
81        let prev_month_days = self.prev().days();
82        for i in (0..to_fill).rev() {
83            current_weekdays[idx] = (prev_month_days - i, false);
84            idx += 1;
85        }
86
87        // Fill weeks with days
88        let mut current_week = first_week;
89        for day in 1..=days {
90            if idx == 7 {
91                weeks.push(CalendarWeek::new(
92                    current_week,
93                    current_weekdays,
94                    starting_weekday,
95                ));
96                current_week = Date::new(day, self.month, self.year).unwrap().iso_week();
97                idx = 0;
98            }
99            current_weekdays[idx] = (day, true);
100            idx += 1;
101        }
102
103        // Fill last week with first days of next month
104        let mut next_month_day = 1;
105        for i in idx..7 {
106            current_weekdays[i] = (next_month_day, false);
107            next_month_day += 1;
108        }
109
110        weeks.push(CalendarWeek::new(
111            last_week,
112            current_weekdays,
113            starting_weekday,
114        ));
115
116        weeks
117    }
118}
119
120fn apply_starting_weekday(weekday: Weekday, start_with: Weekday) -> u8 {
121    let raw = weekday as i8 - start_with as i8;
122    (match raw < 0 {
123        true => raw + 7,
124        false => raw,
125    }) as u8
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn test_calendar_weeks() {
134        let cm = CalendarMonth::new(2026, Month::January);
135        let weeks = cm.weeks(Weekday::Monday);
136        let expected = vec![
137            CalendarWeek::new(
138                1,
139                [
140                    (29, false),
141                    (30, false),
142                    (31, false),
143                    (01, true),
144                    (02, true),
145                    (03, true),
146                    (04, true),
147                ],
148                Weekday::Monday,
149            ),
150            CalendarWeek::new(
151                2,
152                [
153                    (05, true),
154                    (06, true),
155                    (07, true),
156                    (08, true),
157                    (09, true),
158                    (10, true),
159                    (11, true),
160                ],
161                Weekday::Monday,
162            ),
163            CalendarWeek::new(
164                3,
165                [
166                    (12, true),
167                    (13, true),
168                    (14, true),
169                    (15, true),
170                    (16, true),
171                    (17, true),
172                    (18, true),
173                ],
174                Weekday::Monday,
175            ),
176            CalendarWeek::new(
177                4,
178                [
179                    (19, true),
180                    (20, true),
181                    (21, true),
182                    (22, true),
183                    (23, true),
184                    (24, true),
185                    (25, true),
186                ],
187                Weekday::Monday,
188            ),
189            CalendarWeek::new(
190                5,
191                [
192                    (26, true),
193                    (27, true),
194                    (28, true),
195                    (29, true),
196                    (30, true),
197                    (31, true),
198                    (01, false),
199                ],
200                Weekday::Monday,
201            ),
202        ];
203        assert_eq!(weeks, expected);
204
205        let cm = CalendarMonth::new(2025, Month::December);
206        let weeks = cm.weeks(Weekday::Monday);
207        let expected = vec![
208            CalendarWeek::new(
209                49,
210                [
211                    (01, true),
212                    (02, true),
213                    (03, true),
214                    (04, true),
215                    (05, true),
216                    (06, true),
217                    (07, true),
218                ],
219                Weekday::Monday,
220            ),
221            CalendarWeek::new(
222                50,
223                [
224                    (08, true),
225                    (09, true),
226                    (10, true),
227                    (11, true),
228                    (12, true),
229                    (13, true),
230                    (14, true),
231                ],
232                Weekday::Monday,
233            ),
234            CalendarWeek::new(
235                51,
236                [
237                    (15, true),
238                    (16, true),
239                    (17, true),
240                    (18, true),
241                    (19, true),
242                    (20, true),
243                    (21, true),
244                ],
245                Weekday::Monday,
246            ),
247            CalendarWeek::new(
248                52,
249                [
250                    (22, true),
251                    (23, true),
252                    (24, true),
253                    (25, true),
254                    (26, true),
255                    (27, true),
256                    (28, true),
257                ],
258                Weekday::Monday,
259            ),
260            CalendarWeek::new(
261                1,
262                [
263                    (29, true),
264                    (30, true),
265                    (31, true),
266                    (01, false),
267                    (02, false),
268                    (03, false),
269                    (04, false),
270                ],
271                Weekday::Monday,
272            ),
273        ];
274        assert_eq!(weeks, expected);
275
276        let cm = CalendarMonth::new(2027, Month::January);
277        let weeks = cm.weeks(Weekday::Monday);
278        let expected = vec![
279            CalendarWeek::new(
280                53,
281                [
282                    (28, false),
283                    (29, false),
284                    (30, false),
285                    (31, false),
286                    (01, true),
287                    (02, true),
288                    (03, true),
289                ],
290                Weekday::Monday,
291            ),
292            CalendarWeek::new(
293                1,
294                [
295                    (04, true),
296                    (05, true),
297                    (06, true),
298                    (07, true),
299                    (08, true),
300                    (09, true),
301                    (10, true),
302                ],
303                Weekday::Monday,
304            ),
305            CalendarWeek::new(
306                2,
307                [
308                    (11, true),
309                    (12, true),
310                    (13, true),
311                    (14, true),
312                    (15, true),
313                    (16, true),
314                    (17, true),
315                ],
316                Weekday::Monday,
317            ),
318            CalendarWeek::new(
319                3,
320                [
321                    (18, true),
322                    (19, true),
323                    (20, true),
324                    (21, true),
325                    (22, true),
326                    (23, true),
327                    (24, true),
328                ],
329                Weekday::Monday,
330            ),
331            CalendarWeek::new(
332                4,
333                [
334                    (25, true),
335                    (26, true),
336                    (27, true),
337                    (28, true),
338                    (29, true),
339                    (30, true),
340                    (31, true),
341                ],
342                Weekday::Monday,
343            ),
344        ];
345        assert_eq!(weeks, expected);
346    }
347}