strftime/format/
week.rs

1//! Module containing week-related items.
2
3/// Start day of the week.
4#[derive(Debug, Copy, Clone, Eq, PartialEq)]
5pub(crate) enum WeekStart {
6    /// Sunday.
7    Sunday = 0,
8    /// Monday.
9    Monday = 1,
10}
11
12/// Compute the week number, beginning at the provided start day of the week.
13///
14/// ## Inputs
15///
16/// * `week_day`: Day of the week from Sunday in `0..=6`.
17/// * `year_day_1`: Day of the year in `1..=366`.
18/// * `week_start`: Start day of the week.
19///
20pub(crate) fn week_number(week_day: i64, year_day_1: i64, week_start: WeekStart) -> i64 {
21    let year_day = year_day_1 - 1;
22    let start_of_first_week = (year_day - week_day + week_start as i64).rem_euclid(7);
23    (year_day + 7 - start_of_first_week) / 7
24}
25
26/// Compute the ISO 8601 week-based year and week number.
27///
28/// The first week of `YYYY` starts with a Monday and includes `YYYY-01-04`.
29/// The days in the year before the first week are in the last week of the
30/// previous year.
31///
32/// ## Inputs
33///
34/// * `year`: Year.
35/// * `week_day`: Day of the week from Sunday in `0..=6`.
36/// * `year_day_1`: Day of the year in `1..=366`.
37///
38pub(crate) fn iso_8601_year_and_week_number(
39    year: i64,
40    week_day: i64,
41    year_day_1: i64,
42) -> (i64, i64) {
43    let year_day = year_day_1 - 1;
44
45    let mut start_of_first_week = (year_day - week_day + 1).rem_euclid(7);
46
47    if start_of_first_week > 3 {
48        start_of_first_week -= 7;
49    }
50
51    if year_day < start_of_first_week {
52        // Use previous year
53        let previous_year = year - 1;
54
55        let previous_year_day = if is_leap_year(previous_year) {
56            366 + year_day
57        } else {
58            365 + year_day
59        };
60
61        return iso_8601_year_and_week_number(previous_year, week_day, previous_year_day + 1);
62    }
63
64    let week_number = (year_day + 7 - start_of_first_week) / 7;
65
66    if week_number >= 52 {
67        let last_year_day = if is_leap_year(year) { 365 } else { 364 };
68
69        let week_day_of_last_year_day = (week_day + last_year_day - year_day) % 7;
70
71        if (1..=3).contains(&week_day_of_last_year_day) {
72            let last_monday = last_year_day - (week_day_of_last_year_day - 1);
73            if year_day >= last_monday {
74                // Use next year
75                return (year + 1, 1);
76            }
77        }
78    }
79
80    // Use current year
81    (year, week_number)
82}
83
84/// Check if a year is a leap year.
85fn is_leap_year(year: i64) -> bool {
86    year % 400 == 0 || (year % 4 == 0 && year % 100 != 0)
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    #[test]
94    fn test_week_number() {
95        assert_eq!(week_number(1, 0, WeekStart::Sunday), 0);
96        assert_eq!(week_number(2, 1, WeekStart::Sunday), 0);
97        assert_eq!(week_number(3, 2, WeekStart::Sunday), 0);
98        assert_eq!(week_number(4, 3, WeekStart::Sunday), 0);
99        assert_eq!(week_number(5, 4, WeekStart::Sunday), 0);
100        assert_eq!(week_number(6, 5, WeekStart::Sunday), 0);
101        assert_eq!(week_number(0, 6, WeekStart::Sunday), 1);
102        assert_eq!(week_number(1, 7, WeekStart::Sunday), 1);
103        assert_eq!(week_number(2, 8, WeekStart::Sunday), 1);
104
105        assert_eq!(week_number(0, 0, WeekStart::Monday), 0);
106        assert_eq!(week_number(1, 1, WeekStart::Monday), 1);
107        assert_eq!(week_number(2, 2, WeekStart::Monday), 1);
108        assert_eq!(week_number(3, 3, WeekStart::Monday), 1);
109        assert_eq!(week_number(4, 4, WeekStart::Monday), 1);
110        assert_eq!(week_number(5, 5, WeekStart::Monday), 1);
111        assert_eq!(week_number(6, 6, WeekStart::Monday), 1);
112        assert_eq!(week_number(7, 7, WeekStart::Monday), 1);
113        assert_eq!(week_number(8, 8, WeekStart::Monday), 2);
114
115        assert_eq!(week_number(0, 365, WeekStart::Sunday), 53);
116    }
117
118    #[test]
119    fn test_iso_8601_year_and_week() {
120        assert_eq!(iso_8601_year_and_week_number(2025, 0, 362), (2025, 52));
121        assert_eq!(iso_8601_year_and_week_number(2025, 1, 363), (2026, 1));
122        assert_eq!(iso_8601_year_and_week_number(2025, 2, 364), (2026, 1));
123        assert_eq!(iso_8601_year_and_week_number(2025, 3, 365), (2026, 1));
124        assert_eq!(iso_8601_year_and_week_number(2026, 4, 1), (2026, 1));
125        assert_eq!(iso_8601_year_and_week_number(2026, 5, 2), (2026, 1));
126        assert_eq!(iso_8601_year_and_week_number(2026, 6, 3), (2026, 1));
127        assert_eq!(iso_8601_year_and_week_number(2026, 0, 4), (2026, 1));
128        assert_eq!(iso_8601_year_and_week_number(2026, 1, 5), (2026, 2));
129
130        assert_eq!(iso_8601_year_and_week_number(2026, 0, 361), (2026, 52));
131        assert_eq!(iso_8601_year_and_week_number(2026, 1, 362), (2026, 53));
132        assert_eq!(iso_8601_year_and_week_number(2026, 2, 363), (2026, 53));
133        assert_eq!(iso_8601_year_and_week_number(2026, 3, 364), (2026, 53));
134        assert_eq!(iso_8601_year_and_week_number(2026, 4, 365), (2026, 53));
135        assert_eq!(iso_8601_year_and_week_number(2027, 5, 1), (2026, 53));
136        assert_eq!(iso_8601_year_and_week_number(2027, 6, 2), (2026, 53));
137        assert_eq!(iso_8601_year_and_week_number(2027, 0, 3), (2026, 53));
138        assert_eq!(iso_8601_year_and_week_number(2027, 1, 4), (2027, 1));
139
140        assert_eq!(iso_8601_year_and_week_number(2020, 0, 362), (2020, 52));
141        assert_eq!(iso_8601_year_and_week_number(2020, 1, 363), (2020, 53));
142        assert_eq!(iso_8601_year_and_week_number(2020, 2, 364), (2020, 53));
143        assert_eq!(iso_8601_year_and_week_number(2020, 3, 365), (2020, 53));
144        assert_eq!(iso_8601_year_and_week_number(2020, 4, 366), (2020, 53));
145        assert_eq!(iso_8601_year_and_week_number(2021, 5, 1), (2020, 53));
146        assert_eq!(iso_8601_year_and_week_number(2021, 6, 2), (2020, 53));
147        assert_eq!(iso_8601_year_and_week_number(2021, 0, 3), (2020, 53));
148        assert_eq!(iso_8601_year_and_week_number(2021, 1, 4), (2021, 1));
149    }
150
151    #[test]
152    fn test_is_leap_year() {
153        assert!(is_leap_year(2000));
154        assert!(!is_leap_year(2001));
155        assert!(is_leap_year(2004));
156        assert!(!is_leap_year(2100));
157        assert!(!is_leap_year(2200));
158        assert!(!is_leap_year(2300));
159        assert!(is_leap_year(2400));
160    }
161
162    #[cfg(feature = "alloc")]
163    #[test]
164    fn test_week_start_debug_is_non_empty() {
165        use alloc::format;
166
167        assert!(!format!("{:?}", WeekStart::Sunday).is_empty());
168        assert!(!format!("{:?}", WeekStart::Monday).is_empty());
169    }
170}