Coverage Report

Created: 2025-09-05 06:34

/src/chrono/src/naive/isoweek.rs
Line
Count
Source (jump to first uncovered line)
1
// This is a part of Chrono.
2
// See README.md and LICENSE.txt for details.
3
4
//! ISO 8601 week.
5
6
use core::fmt;
7
8
use super::internals::YearFlags;
9
10
#[cfg(any(feature = "rkyv", feature = "rkyv-16", feature = "rkyv-32", feature = "rkyv-64"))]
11
use rkyv::{Archive, Deserialize, Serialize};
12
13
/// ISO 8601 week.
14
///
15
/// This type, combined with [`Weekday`](../enum.Weekday.html),
16
/// constitutes the ISO 8601 [week date](./struct.NaiveDate.html#week-date).
17
/// One can retrieve this type from the existing [`Datelike`](../trait.Datelike.html) types
18
/// via the [`Datelike::iso_week`](../trait.Datelike.html#tymethod.iso_week) method.
19
#[derive(PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)]
20
#[cfg_attr(
21
    any(feature = "rkyv", feature = "rkyv-16", feature = "rkyv-32", feature = "rkyv-64"),
22
    derive(Archive, Deserialize, Serialize),
23
    archive(compare(PartialEq, PartialOrd)),
24
    archive_attr(derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Hash))
25
)]
26
#[cfg_attr(feature = "rkyv-validation", archive(check_bytes))]
27
pub struct IsoWeek {
28
    // Note that this allows for larger year range than `NaiveDate`.
29
    // This is crucial because we have an edge case for the first and last week supported,
30
    // which year number might not match the calendar year number.
31
    ywf: i32, // (year << 10) | (week << 4) | flag
32
}
33
34
impl IsoWeek {
35
    /// Returns the corresponding `IsoWeek` from the year and the `Of` internal value.
36
    //
37
    // Internal use only. We don't expose the public constructor for `IsoWeek` for now
38
    // because the year range for the week date and the calendar date do not match, and
39
    // it is confusing to have a date that is out of range in one and not in another.
40
    // Currently we sidestep this issue by making `IsoWeek` fully dependent of `Datelike`.
41
1.72k
    pub(super) fn from_yof(year: i32, ordinal: u32, year_flags: YearFlags) -> Self {
42
1.72k
        let rawweek = (ordinal + year_flags.isoweek_delta()) / 7;
43
1.72k
        let (year, week) = if rawweek < 1 {
44
            // previous year
45
169
            let prevlastweek = YearFlags::from_year(year - 1).nisoweeks();
46
169
            (year - 1, prevlastweek)
47
        } else {
48
1.55k
            let lastweek = year_flags.nisoweeks();
49
1.55k
            if rawweek > lastweek {
50
                // next year
51
109
                (year + 1, 1)
52
            } else {
53
1.44k
                (year, rawweek)
54
            }
55
        };
56
1.72k
        let flags = YearFlags::from_year(year);
57
1.72k
        IsoWeek { ywf: (year << 10) | (week << 4) as i32 | i32::from(flags.0) }
58
1.72k
    }
59
60
    /// Returns the year number for this ISO week.
61
    ///
62
    /// # Example
63
    ///
64
    /// ```
65
    /// use chrono::{Datelike, NaiveDate, Weekday};
66
    ///
67
    /// let d = NaiveDate::from_isoywd_opt(2015, 1, Weekday::Mon).unwrap();
68
    /// assert_eq!(d.iso_week().year(), 2015);
69
    /// ```
70
    ///
71
    /// This year number might not match the calendar year number.
72
    /// Continuing the example...
73
    ///
74
    /// ```
75
    /// # use chrono::{NaiveDate, Datelike, Weekday};
76
    /// # let d = NaiveDate::from_isoywd_opt(2015, 1, Weekday::Mon).unwrap();
77
    /// assert_eq!(d.year(), 2014);
78
    /// assert_eq!(d, NaiveDate::from_ymd_opt(2014, 12, 29).unwrap());
79
    /// ```
80
    #[inline]
81
1.72k
    pub const fn year(&self) -> i32 {
82
1.72k
        self.ywf >> 10
83
1.72k
    }
84
85
    /// Returns the ISO week number starting from 1.
86
    ///
87
    /// The return value ranges from 1 to 53. (The last week of year differs by years.)
88
    ///
89
    /// # Example
90
    ///
91
    /// ```
92
    /// use chrono::{Datelike, NaiveDate, Weekday};
93
    ///
94
    /// let d = NaiveDate::from_isoywd_opt(2015, 15, Weekday::Mon).unwrap();
95
    /// assert_eq!(d.iso_week().week(), 15);
96
    /// ```
97
    #[inline]
98
1.72k
    pub const fn week(&self) -> u32 {
99
1.72k
        ((self.ywf >> 4) & 0x3f) as u32
100
1.72k
    }
101
102
    /// Returns the ISO week number starting from 0.
103
    ///
104
    /// The return value ranges from 0 to 52. (The last week of year differs by years.)
105
    ///
106
    /// # Example
107
    ///
108
    /// ```
109
    /// use chrono::{Datelike, NaiveDate, Weekday};
110
    ///
111
    /// let d = NaiveDate::from_isoywd_opt(2015, 15, Weekday::Mon).unwrap();
112
    /// assert_eq!(d.iso_week().week0(), 14);
113
    /// ```
114
    #[inline]
115
0
    pub const fn week0(&self) -> u32 {
116
0
        ((self.ywf >> 4) & 0x3f) as u32 - 1
117
0
    }
118
}
119
120
/// The `Debug` output of the ISO week `w` is the same as
121
/// [`d.format("%G-W%V")`](../format/strftime/index.html)
122
/// where `d` is any `NaiveDate` value in that week.
123
///
124
/// # Example
125
///
126
/// ```
127
/// use chrono::{Datelike, NaiveDate};
128
///
129
/// assert_eq!(
130
///     format!("{:?}", NaiveDate::from_ymd_opt(2015, 9, 5).unwrap().iso_week()),
131
///     "2015-W36"
132
/// );
133
/// assert_eq!(format!("{:?}", NaiveDate::from_ymd_opt(0, 1, 3).unwrap().iso_week()), "0000-W01");
134
/// assert_eq!(
135
///     format!("{:?}", NaiveDate::from_ymd_opt(9999, 12, 31).unwrap().iso_week()),
136
///     "9999-W52"
137
/// );
138
/// ```
139
///
140
/// ISO 8601 requires an explicit sign for years before 1 BCE or after 9999 CE.
141
///
142
/// ```
143
/// # use chrono::{NaiveDate, Datelike};
144
/// assert_eq!(format!("{:?}", NaiveDate::from_ymd_opt(0, 1, 2).unwrap().iso_week()), "-0001-W52");
145
/// assert_eq!(
146
///     format!("{:?}", NaiveDate::from_ymd_opt(10000, 12, 31).unwrap().iso_week()),
147
///     "+10000-W52"
148
/// );
149
/// ```
150
impl fmt::Debug for IsoWeek {
151
0
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
152
0
        let year = self.year();
153
0
        let week = self.week();
154
0
        if (0..=9999).contains(&year) {
155
0
            write!(f, "{year:04}-W{week:02}")
156
        } else {
157
            // ISO 8601 requires the explicit sign for out-of-range years
158
0
            write!(f, "{year:+05}-W{week:02}")
159
        }
160
0
    }
161
}
162
163
#[cfg(test)]
164
mod tests {
165
    #[cfg(feature = "rkyv-validation")]
166
    use super::IsoWeek;
167
    use crate::Datelike;
168
    use crate::naive::date::{self, NaiveDate};
169
170
    #[test]
171
    fn test_iso_week_extremes() {
172
        let minweek = NaiveDate::MIN.iso_week();
173
        let maxweek = NaiveDate::MAX.iso_week();
174
175
        assert_eq!(minweek.year(), date::MIN_YEAR);
176
        assert_eq!(minweek.week(), 1);
177
        assert_eq!(minweek.week0(), 0);
178
        #[cfg(feature = "alloc")]
179
        assert_eq!(format!("{minweek:?}"), NaiveDate::MIN.format("%G-W%V").to_string());
180
181
        assert_eq!(maxweek.year(), date::MAX_YEAR + 1);
182
        assert_eq!(maxweek.week(), 1);
183
        assert_eq!(maxweek.week0(), 0);
184
        #[cfg(feature = "alloc")]
185
        assert_eq!(format!("{maxweek:?}"), NaiveDate::MAX.format("%G-W%V").to_string());
186
    }
187
188
    #[test]
189
    fn test_iso_week_equivalence_for_first_week() {
190
        let monday = NaiveDate::from_ymd_opt(2024, 12, 30).unwrap();
191
        let friday = NaiveDate::from_ymd_opt(2025, 1, 3).unwrap();
192
193
        assert_eq!(monday.iso_week(), friday.iso_week());
194
    }
195
196
    #[test]
197
    fn test_iso_week_equivalence_for_last_week() {
198
        let monday = NaiveDate::from_ymd_opt(2026, 12, 28).unwrap();
199
        let friday = NaiveDate::from_ymd_opt(2027, 1, 1).unwrap();
200
201
        assert_eq!(monday.iso_week(), friday.iso_week());
202
    }
203
204
    #[test]
205
    fn test_iso_week_ordering_for_first_week() {
206
        let monday = NaiveDate::from_ymd_opt(2024, 12, 30).unwrap();
207
        let friday = NaiveDate::from_ymd_opt(2025, 1, 3).unwrap();
208
209
        assert!(monday.iso_week() >= friday.iso_week());
210
        assert!(monday.iso_week() <= friday.iso_week());
211
    }
212
213
    #[test]
214
    fn test_iso_week_ordering_for_last_week() {
215
        let monday = NaiveDate::from_ymd_opt(2026, 12, 28).unwrap();
216
        let friday = NaiveDate::from_ymd_opt(2027, 1, 1).unwrap();
217
218
        assert!(monday.iso_week() >= friday.iso_week());
219
        assert!(monday.iso_week() <= friday.iso_week());
220
    }
221
222
    #[test]
223
    #[cfg(feature = "rkyv-validation")]
224
    fn test_rkyv_validation() {
225
        let minweek = NaiveDate::MIN.iso_week();
226
        let bytes = rkyv::to_bytes::<_, 4>(&minweek).unwrap();
227
        assert_eq!(rkyv::from_bytes::<IsoWeek>(&bytes).unwrap(), minweek);
228
229
        let maxweek = NaiveDate::MAX.iso_week();
230
        let bytes = rkyv::to_bytes::<_, 4>(&maxweek).unwrap();
231
        assert_eq!(rkyv::from_bytes::<IsoWeek>(&bytes).unwrap(), maxweek);
232
    }
233
}