Skip to main content

jiff/civil/
iso_week_date.rs

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