Skip to main content

jiff/civil/
iso_week_date.rs

1use crate::{
2    civil::{Date, DateTime, Weekday},
3    error::Error,
4    fmt::temporal::{DEFAULT_DATETIME_PARSER, DEFAULT_DATETIME_PRINTER},
5    util::b,
6    Zoned,
7};
8
9/// A type representing an [ISO 8601 week date].
10///
11/// The ISO 8601 week date scheme devises a calendar where days are identified
12/// by their year, week number and weekday. All years have either precisely
13/// 52 or 53 weeks.
14///
15/// The first week of an ISO 8601 year corresponds to the week containing the
16/// first Thursday of the year. For this reason, an ISO 8601 week year can be
17/// mismatched with the day's corresponding Gregorian year. For example, the
18/// ISO 8601 week date for `1995-01-01` is `1994-W52-7` (with `7` corresponding
19/// to Sunday).
20///
21/// ISO 8601 also considers Monday to be the start of the week, and uses
22/// a 1-based numbering system. That is, Monday corresponds to `1` while
23/// Sunday corresponds to `7` and is the last day of the week. Weekdays are
24/// encapsulated by the [`Weekday`] type, which provides routines for easily
25/// converting between different schemes (such as weeks where Sunday is the
26/// beginning).
27///
28/// [ISO 8601 week date]: https://en.wikipedia.org/wiki/ISO_week_date
29///
30/// # Use case
31///
32/// Some domains use this method of timekeeping. Otherwise, unless you
33/// specifically want a week oriented calendar, it's likely that you'll never
34/// need to care about this type.
35///
36/// # Parsing and printing
37///
38/// The `ISOWeekDate` type provides convenient trait implementations of
39/// [`std::str::FromStr`] and [`std::fmt::Display`]. These use the format
40/// specified by ISO 8601 for week dates:
41///
42/// ```
43/// use jiff::civil::ISOWeekDate;
44///
45/// let week_date: ISOWeekDate = "2024-W24-7".parse()?;
46/// assert_eq!(week_date.to_string(), "2024-W24-7");
47/// assert_eq!(week_date.date().to_string(), "2024-06-16");
48///
49/// # Ok::<(), Box<dyn std::error::Error>>(())
50/// ```
51///
52/// ISO 8601 allows the `-` separator to be absent:
53///
54/// ```
55/// use jiff::civil::ISOWeekDate;
56///
57/// let week_date: ISOWeekDate = "2024W241".parse()?;
58/// assert_eq!(week_date.to_string(), "2024-W24-1");
59/// assert_eq!(week_date.date().to_string(), "2024-06-10");
60///
61/// // But you cannot mix and match. Either `-` separates
62/// // both the year and week, or neither.
63/// assert!("2024W24-1".parse::<ISOWeekDate>().is_err());
64/// assert!("2024-W241".parse::<ISOWeekDate>().is_err());
65///
66/// # Ok::<(), Box<dyn std::error::Error>>(())
67/// ```
68///
69/// And the `W` may also be lowercase:
70///
71/// ```
72/// use jiff::civil::ISOWeekDate;
73///
74/// let week_date: ISOWeekDate = "2024-w24-2".parse()?;
75/// assert_eq!(week_date.to_string(), "2024-W24-2");
76/// assert_eq!(week_date.date().to_string(), "2024-06-11");
77///
78/// # Ok::<(), Box<dyn std::error::Error>>(())
79/// ```
80///
81/// # Default value
82///
83/// For convenience, this type implements the `Default` trait. Its default
84/// value is the first day of the zeroth year. i.e., `0000-W1-1`.
85///
86/// # Example: sample dates
87///
88/// This example shows a couple ISO 8601 week dates and their corresponding
89/// Gregorian equivalents:
90///
91/// ```
92/// use jiff::civil::{ISOWeekDate, Weekday, date};
93///
94/// let d = date(2019, 12, 30);
95/// let weekdate = ISOWeekDate::new(2020, 1, Weekday::Monday).unwrap();
96/// assert_eq!(d.iso_week_date(), weekdate);
97///
98/// let d = date(2024, 3, 9);
99/// let weekdate = ISOWeekDate::new(2024, 10, Weekday::Saturday).unwrap();
100/// assert_eq!(d.iso_week_date(), weekdate);
101/// ```
102///
103/// # Example: overlapping leap and long years
104///
105/// A "long" ISO 8601 week year is a year with 53 weeks. That is, it is a year
106/// that includes a leap week. This example shows all years in the 20th
107/// century that are both Gregorian leap years and long years.
108///
109/// ```
110/// use jiff::civil::date;
111///
112/// let mut overlapping = vec![];
113/// for year in 1900..=1999 {
114///     let date = date(year, 1, 1);
115///     if date.in_leap_year() && date.iso_week_date().in_long_year() {
116///         overlapping.push(year);
117///     }
118/// }
119/// assert_eq!(overlapping, vec![
120///     1904, 1908, 1920, 1932, 1936, 1948, 1960, 1964, 1976, 1988, 1992,
121/// ]);
122/// ```
123///
124/// # Example: printing all weeks in a year
125///
126/// The ISO 8601 week calendar can be useful when you want to categorize
127/// things into buckets of weeks where all weeks are exactly 7 days, _and_
128/// you don't care as much about the precise Gregorian year. Here's an example
129/// that prints all of the ISO 8601 weeks in one ISO 8601 week year:
130///
131/// ```
132/// use jiff::{civil::{ISOWeekDate, Weekday}, ToSpan};
133///
134/// let target_year = 2024;
135/// let iso_week_date = ISOWeekDate::new(target_year, 1, Weekday::Monday)?;
136/// // Create a series of dates via the Gregorian calendar. But since a
137/// // Gregorian week and an ISO 8601 week calendar week are both 7 days,
138/// // this works fine.
139/// let weeks = iso_week_date
140///     .date()
141///     .series(1.week())
142///     .map(|d| d.iso_week_date())
143///     .take_while(|wd| wd.year() == target_year);
144/// for start_of_week in weeks {
145///     let end_of_week = start_of_week.last_of_week()?;
146///     println!(
147///         "ISO week {}: {} - {}",
148///         start_of_week.week(),
149///         start_of_week.date(),
150///         end_of_week.date()
151///     );
152/// }
153/// # Ok::<(), Box<dyn std::error::Error>>(())
154/// ```
155#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
156pub struct ISOWeekDate {
157    year: i16,
158    week: i8,
159    weekday: Weekday,
160}
161
162impl ISOWeekDate {
163    /// The maximum representable ISO week date.
164    ///
165    /// The maximum corresponds to the ISO week date of the maximum [`Date`]
166    /// value. That is, `-9999-01-01`.
167    pub const MIN: ISOWeekDate = ISOWeekDate {
168        year: b::ISOYear::MIN,
169        week: b::ISOWeek::MIN,
170        weekday: Weekday::Monday,
171    };
172
173    /// The minimum representable ISO week date.
174    ///
175    /// The minimum corresponds to the ISO week date of the minimum [`Date`]
176    /// value. That is, `9999-12-31`.
177    pub const MAX: ISOWeekDate = ISOWeekDate {
178        year: b::ISOYear::MAX,
179        // Technical max is 52, but 9999 is not a leap year.
180        week: 52,
181        weekday: Weekday::Friday,
182    };
183
184    /// The first day of the zeroth year.
185    ///
186    /// This is guaranteed to be equivalent to `ISOWeekDate::default()`. Note
187    /// that this is not equivalent to `Date::default()`.
188    ///
189    /// # Example
190    ///
191    /// ```
192    /// use jiff::civil::{ISOWeekDate, date};
193    ///
194    /// assert_eq!(ISOWeekDate::ZERO, ISOWeekDate::default());
195    /// // The first day of the 0th year in the ISO week calendar is actually
196    /// // the third day of the 0th year in the proleptic Gregorian calendar!
197    /// assert_eq!(ISOWeekDate::default().date(), date(0, 1, 3));
198    /// ```
199    pub const ZERO: ISOWeekDate =
200        ISOWeekDate { year: 0, week: 1, weekday: Weekday::Monday };
201
202    /// Create a new ISO week date from it constituent parts.
203    ///
204    /// If the given values are out of range (based on what is representable
205    /// as a [`Date`]), then this returns an error. This will also return an
206    /// error if a leap week is given (week number `53`) for a year that does
207    /// not contain a leap week.
208    ///
209    /// # Example
210    ///
211    /// This example shows some the boundary conditions involving minimum
212    /// and maximum dates:
213    ///
214    /// ```
215    /// use jiff::civil::{ISOWeekDate, Weekday, date};
216    ///
217    /// // The year 1949 does not contain a leap week.
218    /// assert!(ISOWeekDate::new(1949, 53, Weekday::Monday).is_err());
219    ///
220    /// // Examples of dates at or exceeding the maximum.
221    /// let max = ISOWeekDate::new(9999, 52, Weekday::Friday).unwrap();
222    /// assert_eq!(max, ISOWeekDate::MAX);
223    /// assert_eq!(max.date(), date(9999, 12, 31));
224    /// assert!(ISOWeekDate::new(9999, 52, Weekday::Saturday).is_err());
225    /// assert!(ISOWeekDate::new(9999, 53, Weekday::Monday).is_err());
226    ///
227    /// // Examples of dates at or exceeding the minimum.
228    /// let min = ISOWeekDate::new(-9999, 1, Weekday::Monday).unwrap();
229    /// assert_eq!(min, ISOWeekDate::MIN);
230    /// assert_eq!(min.date(), date(-9999, 1, 1));
231    /// assert!(ISOWeekDate::new(-10000, 52, Weekday::Sunday).is_err());
232    /// ```
233    #[inline]
234    pub fn new(
235        year: i16,
236        week: i8,
237        weekday: Weekday,
238    ) -> Result<ISOWeekDate, Error> {
239        let year = b::ISOYear::check(year)?;
240        let week = b::ISOWeek::check(week)?;
241
242        // All combinations of years, weeks and weekdays allowed by our
243        // range types are valid ISO week dates with one exception: a week
244        // number of 53 is only valid for "long" years. Or years with an ISO
245        // leap week. It turns out this only happens when the last day of the
246        // year is a Thursday.
247        //
248        // Note that if the ranges in this crate are changed, this could be
249        // a little trickier if the range of ISOYear is different from Year.
250        debug_assert_eq!(b::Year::MIN, b::ISOYear::MIN);
251        debug_assert_eq!(b::Year::MAX, b::ISOYear::MAX);
252        if week == 53 && !is_long_year(year) {
253            return Err(b::ISOWeek::error().into());
254        }
255        // And also, the maximum Date constrains what we can utter with
256        // ISOWeekDate so that we can preserve infallible conversions between
257        // them. So since 9999-12-31 maps to 9999 W52 Friday, it follows that
258        // Saturday and Sunday are not allowed when the year is at the maximum
259        // value. So reject them.
260        //
261        // We don't need to worry about the minimum because the minimum date
262        // (-9999-01-01) corresponds also to the minimum possible combination
263        // of an ISO week date's fields: -9999 W01 Monday. Nice.
264        if year == b::ISOYear::MAX
265            && week == 52
266            && weekday.to_monday_zero_offset()
267                > Weekday::Friday.to_monday_zero_offset()
268        {
269            return Err(b::WeekdayMondayOne::error().into());
270        }
271        Ok(ISOWeekDate { year, week, weekday })
272    }
273
274    /// Like `ISOWeekDate::new`, but constrains out-of-bounds values
275    /// to their closest valid equivalent.
276    ///
277    /// For example, given `9999 W52 Saturday`, this will return
278    /// `9999 W52 Friday`.
279    #[cfg(test)]
280    #[inline]
281    fn new_constrain(
282        year: i16,
283        mut week: i8,
284        mut weekday: Weekday,
285    ) -> ISOWeekDate {
286        debug_assert_eq!(b::Year::MIN, b::ISOYear::MIN);
287        debug_assert_eq!(b::Year::MAX, b::ISOYear::MAX);
288        if week == 53 && !is_long_year(year) {
289            week = 52;
290        }
291        if year == b::ISOYear::MAX
292            && week == 52
293            && weekday.to_monday_zero_offset()
294                > Weekday::Friday.to_monday_zero_offset()
295        {
296            weekday = Weekday::Friday;
297        }
298        ISOWeekDate { year, week, weekday }
299    }
300
301    /// Converts a Gregorian date to an ISO week date.
302    ///
303    /// The minimum and maximum allowed values of an ISO week date are
304    /// set based on the minimum and maximum values of a `Date`. Therefore,
305    /// converting to and from `Date` values is non-lossy and infallible.
306    ///
307    /// This routine is equivalent to [`Date::iso_week_date`]. This routine
308    /// is also available via a `From<Date>` trait implementation for
309    /// `ISOWeekDate`.
310    ///
311    /// # Example
312    ///
313    /// ```
314    /// use jiff::civil::{ISOWeekDate, Weekday, date};
315    ///
316    /// let weekdate = ISOWeekDate::from_date(date(1948, 2, 10));
317    /// assert_eq!(
318    ///     weekdate,
319    ///     ISOWeekDate::new(1948, 7, Weekday::Tuesday).unwrap(),
320    /// );
321    /// ```
322    #[inline]
323    pub fn from_date(date: Date) -> ISOWeekDate {
324        date.iso_week_date()
325    }
326
327    // N.B. I tried defining a `ISOWeekDate::constant` for defining ISO week
328    // dates as constants, but it was too annoying to do. We could do it if
329    // there was a compelling reason for it though.
330
331    /// Returns the year component of this ISO 8601 week date.
332    ///
333    /// The value returned is guaranteed to be in the range `-9999..=9999`.
334    ///
335    /// # Example
336    ///
337    /// ```
338    /// use jiff::civil::date;
339    ///
340    /// let weekdate = date(2019, 12, 30).iso_week_date();
341    /// assert_eq!(weekdate.year(), 2020);
342    /// ```
343    #[inline]
344    pub fn year(self) -> i16 {
345        self.year
346    }
347
348    /// Returns the week component of this ISO 8601 week date.
349    ///
350    /// The value returned is guaranteed to be in the range `1..=53`. A
351    /// value of `53` can only occur for "long" years. That is, years
352    /// with a leap week. This occurs precisely in cases for which
353    /// [`ISOWeekDate::in_long_year`] returns `true`.
354    ///
355    /// # Example
356    ///
357    /// ```
358    /// use jiff::civil::date;
359    ///
360    /// let weekdate = date(2019, 12, 30).iso_week_date();
361    /// assert_eq!(weekdate.year(), 2020);
362    /// assert_eq!(weekdate.week(), 1);
363    ///
364    /// let weekdate = date(1948, 12, 31).iso_week_date();
365    /// assert_eq!(weekdate.year(), 1948);
366    /// assert_eq!(weekdate.week(), 53);
367    /// ```
368    #[inline]
369    pub fn week(self) -> i8 {
370        self.week
371    }
372
373    /// Returns the day component of this ISO 8601 week date.
374    ///
375    /// One can use methods on `Weekday` such as
376    /// [`Weekday::to_monday_one_offset`]
377    /// and
378    /// [`Weekday::to_sunday_zero_offset`]
379    /// to convert the weekday to a number.
380    ///
381    /// # Example
382    ///
383    /// ```
384    /// use jiff::civil::{date, Weekday};
385    ///
386    /// let weekdate = date(1948, 12, 31).iso_week_date();
387    /// assert_eq!(weekdate.year(), 1948);
388    /// assert_eq!(weekdate.week(), 53);
389    /// assert_eq!(weekdate.weekday(), Weekday::Friday);
390    /// assert_eq!(weekdate.weekday().to_monday_zero_offset(), 4);
391    /// assert_eq!(weekdate.weekday().to_monday_one_offset(), 5);
392    /// assert_eq!(weekdate.weekday().to_sunday_zero_offset(), 5);
393    /// assert_eq!(weekdate.weekday().to_sunday_one_offset(), 6);
394    /// ```
395    #[inline]
396    pub fn weekday(self) -> Weekday {
397        self.weekday
398    }
399
400    /// Returns the ISO 8601 week date corresponding to the first day in the
401    /// week of this week date. The date returned is guaranteed to have a
402    /// weekday of [`Weekday::Monday`].
403    ///
404    /// # Errors
405    ///
406    /// Since `-9999-01-01` falls on a Monday, it follows that the minimum
407    /// support Gregorian date is exactly equivalent to the minimum supported
408    /// ISO 8601 week date. This means that this routine can never actually
409    /// fail, but only insomuch as the minimums line up. For that reason, and
410    /// for consistency with [`ISOWeekDate::last_of_week`], the API is
411    /// fallible.
412    ///
413    /// # Example
414    ///
415    /// ```
416    /// use jiff::civil::{ISOWeekDate, Weekday, date};
417    ///
418    /// let wd = ISOWeekDate::new(2025, 5, Weekday::Wednesday).unwrap();
419    /// assert_eq!(wd.date(), date(2025, 1, 29));
420    /// assert_eq!(
421    ///     wd.first_of_week()?,
422    ///     ISOWeekDate::new(2025, 5, Weekday::Monday).unwrap(),
423    /// );
424    ///
425    /// // Works even for the minimum date.
426    /// assert_eq!(
427    ///     ISOWeekDate::MIN.first_of_week()?,
428    ///     ISOWeekDate::new(-9999, 1, Weekday::Monday).unwrap(),
429    /// );
430    ///
431    /// # Ok::<(), Box<dyn std::error::Error>>(())
432    /// ```
433    #[inline]
434    pub fn first_of_week(self) -> Result<ISOWeekDate, Error> {
435        // I believe this can never return an error because `Monday` is in
436        // bounds for all possible year-and-week combinations. This is *only*
437        // because -9999-01-01 corresponds to -9999-W01-Monday. Which is kinda
438        // lucky. And I guess if we ever change the ranges, this could become
439        // fallible.
440        ISOWeekDate::new(self.year(), self.week(), Weekday::Monday)
441    }
442
443    /// Returns the ISO 8601 week date corresponding to the last day in the
444    /// week of this week date. The date returned is guaranteed to have a
445    /// weekday of [`Weekday::Sunday`].
446    ///
447    /// # Errors
448    ///
449    /// This can return an error if the last day of the week exceeds Jiff's
450    /// maximum Gregorian date of `9999-12-31`. It turns out this can happen
451    /// since `9999-12-31` falls on a Friday.
452    ///
453    /// # Example
454    ///
455    /// ```
456    /// use jiff::civil::{ISOWeekDate, Weekday, date};
457    ///
458    /// let wd = ISOWeekDate::new(2025, 5, Weekday::Wednesday).unwrap();
459    /// assert_eq!(wd.date(), date(2025, 1, 29));
460    /// assert_eq!(
461    ///     wd.last_of_week()?,
462    ///     ISOWeekDate::new(2025, 5, Weekday::Sunday).unwrap(),
463    /// );
464    ///
465    /// // Unlike `first_of_week`, this routine can actually fail on real
466    /// // values, although, only when close to the maximum supported date.
467    /// assert_eq!(
468    ///     ISOWeekDate::MAX.last_of_week().unwrap_err().to_string(),
469    ///     "parameter 'weekday (Monday 1-indexed)' \
470    ///      is not in the required range of 1..=7",
471    /// );
472    ///
473    /// # Ok::<(), Box<dyn std::error::Error>>(())
474    /// ```
475    #[inline]
476    pub fn last_of_week(self) -> Result<ISOWeekDate, Error> {
477        // This can return an error when in the last week of the maximum year
478        // supported by Jiff. That's because the Saturday and Sunday of that
479        // week are actually in Gregorian year 10,000.
480        ISOWeekDate::new(self.year(), self.week(), Weekday::Sunday)
481    }
482
483    /// Returns the ISO 8601 week date corresponding to the first day in the
484    /// year of this week date. The date returned is guaranteed to have a
485    /// weekday of [`Weekday::Monday`].
486    ///
487    /// # Errors
488    ///
489    /// Since `-9999-01-01` falls on a Monday, it follows that the minimum
490    /// support Gregorian date is exactly equivalent to the minimum supported
491    /// ISO 8601 week date. This means that this routine can never actually
492    /// fail, but only insomuch as the minimums line up. For that reason, and
493    /// for consistency with [`ISOWeekDate::last_of_year`], the API is
494    /// fallible.
495    ///
496    /// # Example
497    ///
498    /// ```
499    /// use jiff::civil::{ISOWeekDate, Weekday, date};
500    ///
501    /// let wd = ISOWeekDate::new(2025, 5, Weekday::Wednesday).unwrap();
502    /// assert_eq!(wd.date(), date(2025, 1, 29));
503    /// assert_eq!(
504    ///     wd.first_of_year()?,
505    ///     ISOWeekDate::new(2025, 1, Weekday::Monday).unwrap(),
506    /// );
507    ///
508    /// // Works even for the minimum date.
509    /// assert_eq!(
510    ///     ISOWeekDate::MIN.first_of_year()?,
511    ///     ISOWeekDate::new(-9999, 1, Weekday::Monday).unwrap(),
512    /// );
513    ///
514    /// # Ok::<(), Box<dyn std::error::Error>>(())
515    /// ```
516    #[inline]
517    pub fn first_of_year(self) -> Result<ISOWeekDate, Error> {
518        // I believe this can never return an error because `Monday` is in
519        // bounds for all possible years. This is *only* because -9999-01-01
520        // corresponds to -9999-W01-Monday. Which is kinda lucky. And I guess
521        // if we ever change the ranges, this could become fallible.
522        ISOWeekDate::new(self.year(), 1, Weekday::Monday)
523    }
524
525    /// Returns the ISO 8601 week date corresponding to the last day in the
526    /// year of this week date. The date returned is guaranteed to have a
527    /// weekday of [`Weekday::Sunday`].
528    ///
529    /// # Errors
530    ///
531    /// This can return an error if the last day of the year exceeds Jiff's
532    /// maximum Gregorian date of `9999-12-31`. It turns out this can happen
533    /// since `9999-12-31` falls on a Friday.
534    ///
535    /// # Example
536    ///
537    /// ```
538    /// use jiff::civil::{ISOWeekDate, Weekday, date};
539    ///
540    /// let wd = ISOWeekDate::new(2025, 5, Weekday::Wednesday).unwrap();
541    /// assert_eq!(wd.date(), date(2025, 1, 29));
542    /// assert_eq!(
543    ///     wd.last_of_year()?,
544    ///     ISOWeekDate::new(2025, 52, Weekday::Sunday).unwrap(),
545    /// );
546    ///
547    /// // Works correctly for "long" years.
548    /// let wd = ISOWeekDate::new(2026, 5, Weekday::Wednesday).unwrap();
549    /// assert_eq!(wd.date(), date(2026, 1, 28));
550    /// assert_eq!(
551    ///     wd.last_of_year()?,
552    ///     ISOWeekDate::new(2026, 53, Weekday::Sunday).unwrap(),
553    /// );
554    ///
555    /// // Unlike `first_of_year`, this routine can actually fail on real
556    /// // values, although, only when close to the maximum supported date.
557    /// assert_eq!(
558    ///     ISOWeekDate::MAX.last_of_year().unwrap_err().to_string(),
559    ///     "parameter 'weekday (Monday 1-indexed)' \
560    ///      is not in the required range of 1..=7",
561    /// );
562    ///
563    /// # Ok::<(), Box<dyn std::error::Error>>(())
564    /// ```
565    #[inline]
566    pub fn last_of_year(self) -> Result<ISOWeekDate, Error> {
567        // This can return an error when in the maximum year supported by
568        // Jiff. That's because the last Saturday and Sunday of that year are
569        // actually in Gregorian year 10,000.
570        ISOWeekDate::new(self.year(), self.weeks_in_year(), Weekday::Sunday)
571    }
572
573    /// Returns the total number of days in the year of this ISO 8601 week
574    /// date.
575    ///
576    /// It is guaranteed that the value returned is either 364 or 371. The
577    /// latter case occurs precisely when [`ISOWeekDate::in_long_year`]
578    /// returns `true`.
579    ///
580    /// # Example
581    ///
582    /// ```
583    /// use jiff::civil::{ISOWeekDate, Weekday};
584    ///
585    /// let weekdate = ISOWeekDate::new(2025, 7, Weekday::Monday).unwrap();
586    /// assert_eq!(weekdate.days_in_year(), 364);
587    /// let weekdate = ISOWeekDate::new(2026, 7, Weekday::Monday).unwrap();
588    /// assert_eq!(weekdate.days_in_year(), 371);
589    /// ```
590    #[inline]
591    pub fn days_in_year(self) -> i16 {
592        if self.in_long_year() {
593            371
594        } else {
595            364
596        }
597    }
598
599    /// Returns the total number of weeks in the year of this ISO 8601 week
600    /// date.
601    ///
602    /// It is guaranteed that the value returned is either 52 or 53. The
603    /// latter case occurs precisely when [`ISOWeekDate::in_long_year`]
604    /// returns `true`.
605    ///
606    /// # Example
607    ///
608    /// ```
609    /// use jiff::civil::{ISOWeekDate, Weekday};
610    ///
611    /// let weekdate = ISOWeekDate::new(2025, 7, Weekday::Monday).unwrap();
612    /// assert_eq!(weekdate.weeks_in_year(), 52);
613    /// let weekdate = ISOWeekDate::new(2026, 7, Weekday::Monday).unwrap();
614    /// assert_eq!(weekdate.weeks_in_year(), 53);
615    /// ```
616    #[inline]
617    pub fn weeks_in_year(self) -> i8 {
618        if self.in_long_year() {
619            53
620        } else {
621            52
622        }
623    }
624
625    /// Returns true if and only if the year of this week date is a "long"
626    /// year.
627    ///
628    /// A long year is one that contains precisely 53 weeks. All other years
629    /// contain precisely 52 weeks.
630    ///
631    /// # Example
632    ///
633    /// ```
634    /// use jiff::civil::{ISOWeekDate, Weekday};
635    ///
636    /// let weekdate = ISOWeekDate::new(1948, 7, Weekday::Monday).unwrap();
637    /// assert!(weekdate.in_long_year());
638    /// let weekdate = ISOWeekDate::new(1949, 7, Weekday::Monday).unwrap();
639    /// assert!(!weekdate.in_long_year());
640    /// ```
641    #[inline]
642    pub fn in_long_year(self) -> bool {
643        is_long_year(self.year())
644    }
645
646    /// Returns the ISO 8601 date immediately following this one.
647    ///
648    /// # Errors
649    ///
650    /// This returns an error when this date is the maximum value.
651    ///
652    /// # Example
653    ///
654    /// ```
655    /// use jiff::civil::{ISOWeekDate, Weekday};
656    ///
657    /// let wd = ISOWeekDate::new(2025, 5, Weekday::Wednesday).unwrap();
658    /// assert_eq!(
659    ///     wd.tomorrow()?,
660    ///     ISOWeekDate::new(2025, 5, Weekday::Thursday).unwrap(),
661    /// );
662    ///
663    /// // The max doesn't have a tomorrow.
664    /// assert!(ISOWeekDate::MAX.tomorrow().is_err());
665    ///
666    /// # Ok::<(), Box<dyn std::error::Error>>(())
667    /// ```
668    #[inline]
669    pub fn tomorrow(self) -> Result<ISOWeekDate, Error> {
670        // I suppose we could probably implement this in a more efficient
671        // manner but avoiding the roundtrip through Gregorian dates.
672        self.date().tomorrow().map(|d| d.iso_week_date())
673    }
674
675    /// Returns the ISO 8601 week date immediately preceding this one.
676    ///
677    /// # Errors
678    ///
679    /// This returns an error when this date is the minimum value.
680    ///
681    /// # Example
682    ///
683    /// ```
684    /// use jiff::civil::{ISOWeekDate, Weekday};
685    ///
686    /// let wd = ISOWeekDate::new(2025, 5, Weekday::Wednesday).unwrap();
687    /// assert_eq!(
688    ///     wd.yesterday()?,
689    ///     ISOWeekDate::new(2025, 5, Weekday::Tuesday).unwrap(),
690    /// );
691    ///
692    /// // The min doesn't have a yesterday.
693    /// assert!(ISOWeekDate::MIN.yesterday().is_err());
694    ///
695    /// # Ok::<(), Box<dyn std::error::Error>>(())
696    /// ```
697    #[inline]
698    pub fn yesterday(self) -> Result<ISOWeekDate, Error> {
699        // I suppose we could probably implement this in a more efficient
700        // manner but avoiding the roundtrip through Gregorian dates.
701        self.date().yesterday().map(|d| d.iso_week_date())
702    }
703
704    /// Converts this ISO week date to a Gregorian [`Date`].
705    ///
706    /// The minimum and maximum allowed values of an ISO week date are
707    /// set based on the minimum and maximum values of a `Date`. Therefore,
708    /// converting to and from `Date` values is non-lossy and infallible.
709    ///
710    /// This routine is equivalent to [`Date::from_iso_week_date`].
711    ///
712    /// # Example
713    ///
714    /// ```
715    /// use jiff::civil::{ISOWeekDate, Weekday, date};
716    ///
717    /// let weekdate = ISOWeekDate::new(1948, 7, Weekday::Tuesday).unwrap();
718    /// assert_eq!(weekdate.date(), date(1948, 2, 10));
719    /// ```
720    #[inline]
721    pub fn date(self) -> Date {
722        Date::from_iso_week_date(self)
723    }
724}
725
726impl Default for ISOWeekDate {
727    fn default() -> ISOWeekDate {
728        ISOWeekDate::ZERO
729    }
730}
731
732impl core::fmt::Display for ISOWeekDate {
733    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
734        use crate::fmt::StdFmtWrite;
735
736        DEFAULT_DATETIME_PRINTER
737            .print_iso_week_date(self, StdFmtWrite(f))
738            .map_err(|_| core::fmt::Error)
739    }
740}
741
742impl core::str::FromStr for ISOWeekDate {
743    type Err = Error;
744
745    fn from_str(string: &str) -> Result<ISOWeekDate, Error> {
746        DEFAULT_DATETIME_PARSER.parse_iso_week_date(string)
747    }
748}
749
750impl Ord for ISOWeekDate {
751    #[inline]
752    fn cmp(&self, other: &ISOWeekDate) -> core::cmp::Ordering {
753        (self.year(), self.week(), self.weekday().to_monday_one_offset()).cmp(
754            &(
755                other.year(),
756                other.week(),
757                other.weekday().to_monday_one_offset(),
758            ),
759        )
760    }
761}
762
763impl PartialOrd for ISOWeekDate {
764    #[inline]
765    fn partial_cmp(&self, other: &ISOWeekDate) -> Option<core::cmp::Ordering> {
766        Some(self.cmp(other))
767    }
768}
769
770impl From<Date> for ISOWeekDate {
771    #[inline]
772    fn from(date: Date) -> ISOWeekDate {
773        ISOWeekDate::from_date(date)
774    }
775}
776
777impl From<DateTime> for ISOWeekDate {
778    #[inline]
779    fn from(dt: DateTime) -> ISOWeekDate {
780        ISOWeekDate::from(dt.date())
781    }
782}
783
784impl From<Zoned> for ISOWeekDate {
785    #[inline]
786    fn from(zdt: Zoned) -> ISOWeekDate {
787        ISOWeekDate::from(zdt.date())
788    }
789}
790
791impl<'a> From<&'a Zoned> for ISOWeekDate {
792    #[inline]
793    fn from(zdt: &'a Zoned) -> ISOWeekDate {
794        ISOWeekDate::from(zdt.date())
795    }
796}
797
798#[cfg(feature = "serde")]
799impl serde_core::Serialize for ISOWeekDate {
800    #[inline]
801    fn serialize<S: serde_core::Serializer>(
802        &self,
803        serializer: S,
804    ) -> Result<S::Ok, S::Error> {
805        serializer.collect_str(self)
806    }
807}
808
809#[cfg(feature = "serde")]
810impl<'de> serde_core::Deserialize<'de> for ISOWeekDate {
811    #[inline]
812    fn deserialize<D: serde_core::Deserializer<'de>>(
813        deserializer: D,
814    ) -> Result<ISOWeekDate, D::Error> {
815        use serde_core::de;
816
817        struct ISOWeekDateVisitor;
818
819        impl<'de> de::Visitor<'de> for ISOWeekDateVisitor {
820            type Value = ISOWeekDate;
821
822            fn expecting(
823                &self,
824                f: &mut core::fmt::Formatter,
825            ) -> core::fmt::Result {
826                f.write_str("an ISO 8601 week date string")
827            }
828
829            #[inline]
830            fn visit_bytes<E: de::Error>(
831                self,
832                value: &[u8],
833            ) -> Result<ISOWeekDate, E> {
834                DEFAULT_DATETIME_PARSER
835                    .parse_iso_week_date(value)
836                    .map_err(de::Error::custom)
837            }
838
839            #[inline]
840            fn visit_str<E: de::Error>(
841                self,
842                value: &str,
843            ) -> Result<ISOWeekDate, E> {
844                self.visit_bytes(value.as_bytes())
845            }
846        }
847
848        deserializer.deserialize_str(ISOWeekDateVisitor)
849    }
850}
851
852#[cfg(test)]
853impl quickcheck::Arbitrary for ISOWeekDate {
854    fn arbitrary(g: &mut quickcheck::Gen) -> ISOWeekDate {
855        let year = b::ISOYear::arbitrary(g);
856        let week = b::ISOWeek::arbitrary(g);
857        let weekday = Weekday::arbitrary(g);
858        ISOWeekDate::new_constrain(year, week, weekday)
859    }
860
861    fn shrink(&self) -> alloc::boxed::Box<dyn Iterator<Item = ISOWeekDate>> {
862        alloc::boxed::Box::new(
863            (self.year(), self.week(), self.weekday()).shrink().map(
864                |(year, week, weekday)| {
865                    ISOWeekDate::new_constrain(year, week, weekday)
866                },
867            ),
868        )
869    }
870}
871
872/// Returns true if the given ISO year is a "long" year or not.
873///
874/// A "long" year is a year with 53 weeks. Otherwise, it's a "short" year
875/// with 52 weeks.
876fn is_long_year(year: i16) -> bool {
877    // Inspired by: https://en.wikipedia.org/wiki/ISO_week_date#Weeks_per_year
878    let last =
879        Date::new(year, 12, 31).expect("last day of year is always valid");
880    let weekday = last.weekday();
881    weekday == Weekday::Thursday
882        || (last.in_leap_year() && weekday == Weekday::Friday)
883}
884
885#[cfg(not(miri))]
886#[cfg(test)]
887mod tests {
888    use super::*;
889
890    quickcheck::quickcheck! {
891        fn prop_all_long_years_have_53rd_week(year: i16) -> quickcheck::TestResult {
892            if b::Year::check(year).is_err() {
893                return quickcheck::TestResult::discard();
894            }
895            quickcheck::TestResult::from_bool(!is_long_year(year)
896                || ISOWeekDate::new(year, 53, Weekday::Sunday).is_ok())
897        }
898
899        fn prop_prev_day_is_less(wd: ISOWeekDate) -> quickcheck::TestResult {
900            use crate::ToSpan;
901
902            if wd == ISOWeekDate::MIN {
903                return quickcheck::TestResult::discard();
904            }
905            let prev_date = wd.date().checked_add(-1.days()).unwrap();
906            quickcheck::TestResult::from_bool(prev_date.iso_week_date() < wd)
907        }
908
909        fn prop_next_day_is_greater(wd: ISOWeekDate) -> quickcheck::TestResult {
910            use crate::ToSpan;
911
912            if wd == ISOWeekDate::MAX {
913                return quickcheck::TestResult::discard();
914            }
915            let next_date = wd.date().checked_add(1.days()).unwrap();
916            quickcheck::TestResult::from_bool(wd < next_date.iso_week_date())
917        }
918    }
919}