Skip to main content

jiff_core/civil/
date.rs

1use crate::{
2    bounds::{self as b, RangeError},
3    civil::{self, DateTime, Weekday},
4    macros::{ctry, rbail, rtry, unwrapr},
5};
6
7/// A Gregorian civil date.
8///
9/// A Gregorian civil date can be infallibly converted to and from
10/// [`UnixEpochDay`] values.
11///
12/// Note that since this supports a year `0`, this technically implements the
13/// ISO 8601 proleptic calendar and not the Gregorian calendar. Notably, the
14/// Gregorian calendar does not have a year `0`. Therefore, a `Date` with a
15/// year `0` is equivalent to the Gregorian `1 BCE` year.
16#[derive(Clone, Copy, Eq, Hash, PartialEq, PartialOrd, Ord)]
17#[cfg_attr(feature = "defmt", derive(defmt::Format))]
18pub struct Date {
19    year: i16,
20    month: i8,
21    day: i8,
22}
23
24impl Date {
25    /// The minimum allowed Gregorian date.
26    ///
27    /// This is guaranteed to be equivalent to [`UnixEpochDay::MIN`] when
28    /// converted to a Unix epoch day.
29    pub const MIN: Date = {
30        let min = UnixEpochDay::MIN.to_date();
31        assert!(min.year() == -9999);
32        assert!(min.month() == 1);
33        assert!(min.day() == 1);
34        min
35    };
36
37    /// The maximum allowed Gregorian date.
38    ///
39    /// This is guaranteed to be equivalent to [`UnixEpochDay::MAX`] when
40    /// converted to a Unix epoch day.
41    pub const MAX: Date = {
42        let max = UnixEpochDay::MAX.to_date();
43        assert!(max.year() == 9999);
44        assert!(max.month() == 12);
45        assert!(max.day() == 31);
46        max
47    };
48
49    // I'm hopeful we won't need a public unchecked constructor. But if
50    // we do, it cannot be marked safe. Otherwise callers cannot soundly rely
51    // on the values of, e.g., `Date::day()` being in range for memory safety.
52    /*
53    /// Returns a new `Date` without doing bounds checks on the values given.
54    ///
55    /// # Safety
56    ///
57    /// Callers must ensure that the year, month and day provided are a valid
58    /// Gregorian date **and** that `year` falls into the range specified by
59    /// [`Year`](b::Year).
60    ///
61    /// While memory safety may not be violated immediately from using an
62    /// invalid value, downstream callers may rely on the validity of routines
63    /// like `Date::month` for memory safety. If this routine was safe,
64    /// then such reliance would be unsound.
65    #[inline]
66    pub const unsafe fn new_unchecked(year: i16, month: i8, day: i8) -> Date {
67        Date { year, month, day }
68    }
69    */
70
71    /// Creates a new date in the Gregorian calendar.
72    ///
73    /// If the date is invalid or any of the values are out of their supported
74    /// ranges, then an error is returned.
75    ///
76    /// Note that, technically, the date returned is in the ISO 8601 proleptic
77    /// calendar. The only difference between it and the Gregorian calendar
78    /// is that ISO 8601 has a year `0`. ISO 8601's year `0` corresponds to
79    /// `-1 BCE` in the Gregorian calendar.
80    #[inline]
81    pub const fn new(
82        year: i16,
83        month: i8,
84        day: i8,
85    ) -> Result<Date, RangeError> {
86        let year = rtry!(b::Year::checkc(year as i64));
87        let month = rtry!(b::Month::checkc(month as i64));
88        if day < 1 {
89            rbail!(b::Day::error());
90        } else if day > 28 && day > civil::days_in_month(year, month) {
91            rbail!(b::SpecialBoundsError::DateInvalidDay { year, month });
92        }
93        Ok(Date { year, month, day })
94    }
95
96    /// Like `Date::new`, but constrains the day value to the last day of
97    /// `month`.
98    ///
99    /// This still returns an error when `day < 1` or when `year` or `month`
100    /// are invalid.
101    #[inline]
102    pub const fn new_constrain(
103        year: i16,
104        month: i8,
105        day: i8,
106    ) -> Result<Date, RangeError> {
107        let year = rtry!(b::Year::checkc(year as i64));
108        let month = rtry!(b::Month::checkc(month as i64));
109        let day = if day < 1 {
110            rbail!(b::Day::error());
111        } else if day > 28 {
112            let days_in_month = civil::days_in_month(year, month);
113            if day <= days_in_month {
114                day
115            } else {
116                days_in_month
117            }
118        } else {
119            day
120        };
121        Ok(Date { year, month, day })
122    }
123
124    /// Returns the date corresponding to the day of the given year. The day
125    /// of the year should be a value in `1..=366`, with `366` only being valid
126    /// if `year` is a leap year.
127    ///
128    /// Returns an error if `year` is not in the range specified by
129    /// [`Year`](b::Year), or if `day` is invalid for the given year.
130    #[inline]
131    pub const fn from_day_of_year(
132        year: i16,
133        day: i16,
134    ) -> Result<Date, RangeError> {
135        let year = rtry!(b::Year::checkc(year as i64));
136        let day = rtry!(b::DayOfYear::checkc(day as i64));
137        let start = Date { year, month: 1, day: 1 }.to_unix_epoch_day();
138        let end = match start.checked_add(day as i32 - 1) {
139            Ok(end) => end.to_date(),
140            // This can only happen when `year=9999` and `day=366`.
141            Err(_) => {
142                rbail!(b::SpecialBoundsError::DateInvalidDayOfYear { year })
143            }
144        };
145        // If we overflowed into the next year, then `day` is too big.
146        if year != end.year {
147            // Can only happen given day=366 and this is a leap year.
148            debug_assert!(day == 366);
149            debug_assert!(!civil::is_leap_year(year));
150            rbail!(b::SpecialBoundsError::DateInvalidDayOfYear { year })
151        }
152        Ok(end)
153    }
154
155    /// Returns the date corresponding to the day of the given year. The day
156    /// of the year must be a value in `1..=365`, with February 29 being
157    /// completely ignored. That is, it is guaranteed that February 29 will
158    /// never be returned by this function. It is impossible.
159    ///
160    /// Returns an error if `year` is not in the range specified by
161    /// [`Year`](b::Year), or if `day` is outside the range `1..=365`.
162    #[inline]
163    pub const fn from_day_of_year_no_leap(
164        year: i16,
165        day: i16,
166    ) -> Result<Date, RangeError> {
167        let year = rtry!(b::Year::checkc(year as i64));
168        let mut day = rtry!(b::DayOfYearNoLeap::checkc(day as i64));
169        if day >= 60 && civil::is_leap_year(year) {
170            day += 1;
171        }
172        // The boundary check above guarantees this always succeeds.
173        Ok(unwrapr!(Date::from_day_of_year(year, day), "valid day of year"))
174    }
175
176    /// Returns the year component of this date.
177    ///
178    /// The value returned is guaranteed to be in the range specified by
179    /// [`Year`](crate::bounds::Year).
180    #[inline]
181    pub const fn year(self) -> i16 {
182        self.year
183    }
184
185    /// Returns the month component of this date.
186    ///
187    /// The value returned is guaranteed to be in the range `1..=12`.
188    #[inline]
189    pub const fn month(self) -> i8 {
190        self.month
191    }
192
193    /// Returns the day component of this date.
194    ///
195    /// The value returned is guaranteed to be in the range
196    /// `1..=jiff_core::civil::days_in_month(date.year(), date.month())`.
197    #[inline]
198    pub const fn day(self) -> i8 {
199        self.day
200    }
201
202    /// Returns the weekday corresponding to this date.
203    #[inline]
204    pub const fn weekday(self) -> Weekday {
205        self.to_unix_epoch_day().weekday()
206    }
207
208    /// Returns the ordinal day of the year that this date resides in.
209    ///
210    /// For leap years, this always returns a value in the range `1..=366`.
211    /// Otherwise, the value is in the range `1..=365`.
212    #[inline]
213    pub const fn day_of_year(self) -> i16 {
214        const DAYS_BY_MONTH_NO_LEAP: [i16; 14] =
215            [0, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365];
216        const DAYS_BY_MONTH_LEAP: [i16; 14] =
217            [0, 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366];
218        const TABLES: [[i16; 14]; 2] =
219            [DAYS_BY_MONTH_NO_LEAP, DAYS_BY_MONTH_LEAP];
220        TABLES[self.in_leap_year() as usize][self.month() as usize]
221            + (self.day() as i16)
222    }
223
224    /// Returns the ordinal day of the year that this date resides in, but
225    /// ignores leap years.
226    ///
227    /// That is, the range of possible values returned by this routine is
228    /// `1..=365`, even if this date resides in a leap year. If this date is
229    /// February 29, then this routine returns `None`.
230    ///
231    /// The value `365` always corresponds to the last day in the year,
232    /// December 31, even for leap years.
233    #[inline]
234    pub const fn day_of_year_no_leap(self) -> Option<i16> {
235        let mut days = self.day_of_year();
236        if self.in_leap_year() {
237            // day=60 is Feb 29
238            if days == 60 {
239                return None;
240            } else if days > 60 {
241                days -= 1;
242            }
243        }
244        Some(days)
245    }
246
247    /// Returns the first date of the month for this date.
248    #[inline]
249    pub const fn first_of_month(self) -> Date {
250        Date { day: 1, ..self }
251    }
252
253    /// Returns the last date of the month for this date.
254    #[inline]
255    pub const fn last_of_month(self) -> Date {
256        Date { day: self.days_in_month(), ..self }
257    }
258
259    /// Returns the total number of days in the the month in which this date
260    /// resides.
261    ///
262    /// This is guaranteed to always return one of the following values,
263    /// depending on the year and the month: 28, 29, 30 or 31.
264    #[inline]
265    pub const fn days_in_month(self) -> i8 {
266        civil::days_in_month(self.year(), self.month())
267    }
268
269    /// Returns the first date of the year that this date resides in.
270    #[inline]
271    pub const fn first_of_year(self) -> Date {
272        Date { month: 1, day: 1, ..self }
273    }
274
275    /// Returns the last date of the year that this date resides in.
276    #[inline]
277    pub const fn last_of_year(self) -> Date {
278        Date { month: 12, day: 31, ..self }
279    }
280
281    /// Returns the number of days in the year for this date.
282    ///
283    /// It is guaranteed that the value returned is either `365` or `366`.
284    #[inline]
285    pub const fn days_in_year(self) -> i16 {
286        if self.in_leap_year() {
287            366
288        } else {
289            365
290        }
291    }
292
293    /// Returns true when this date is in a leap year.
294    #[inline]
295    pub const fn in_leap_year(self) -> bool {
296        civil::is_leap_year(self.year())
297    }
298
299    /// Returns the day before this date.
300    ///
301    /// Returns an error when this is the minimal date.
302    #[inline]
303    pub const fn yesterday(self) -> Result<Date, RangeError> {
304        if self.day() == 1 {
305            if self.month() == 1 {
306                let year = ctry!(self.prev_year());
307                return Ok(Date { year, month: 12, day: 31 });
308            }
309            let month = self.month() - 1;
310            let day = civil::days_in_month(self.year(), month);
311            return Ok(Date { month, day, ..self });
312        }
313        Ok(Date { day: self.day() - 1, ..self })
314    }
315
316    /// Returns the day after this date.
317    ///
318    /// Returns an error when this is the maximal date.
319    #[inline]
320    pub const fn tomorrow(self) -> Result<Date, RangeError> {
321        if self.day() >= 28 && self.day() == self.days_in_month() {
322            if self.month() == 12 {
323                let year = ctry!(self.next_year());
324                return Ok(Date { year, month: 1, day: 1 });
325            }
326            let month = self.month() + 1;
327            return Ok(Date { month, day: 1, ..self });
328        }
329        Ok(Date { day: self.day() + 1, ..self })
330    }
331
332    /// Returns the `nth` weekday of the month represented by this date.
333    ///
334    /// `nth` must be non-zero and otherwise in the range `-5..=5`. If it
335    /// isn't, an error is returned.
336    ///
337    /// This also returns an error if `abs(nth)==5` and there is no "5th"
338    /// weekday of this month.
339    #[inline]
340    pub const fn nth_weekday_of_month(
341        &self,
342        nth: i8,
343        weekday: Weekday,
344    ) -> Result<Date, RangeError> {
345        let nth = rtry!(b::NthWeekdayOfMonth::checkc(nth as i64));
346        if nth == 0 {
347            rbail!(b::NthWeekdayOfMonth::error());
348        } else if nth > 0 {
349            let first = self.first_of_month();
350            let first_weekday = first.weekday();
351            let diff = weekday.since(first_weekday);
352            let day = diff + 1 + (nth - 1) * 7;
353            Date::new(self.year(), self.month(), day)
354        } else {
355            let last = self.last_of_month();
356            let last_weekday = last.weekday();
357            let diff = last_weekday.since(weekday);
358            let day = last.day() - diff - (nth.abs() - 1) * 7;
359            Date::new(self.year(), self.month(), day)
360        }
361    }
362
363    /// Returns the "nth" weekday from this date, not including itself.
364    ///
365    /// The `nth` parameter can be positive or negative. A positive value
366    /// computes the "nth" weekday starting at the day after this date and
367    /// going forwards in time. A negative value computes the "nth" weekday
368    /// starting at the day before this date and going backwards in time.
369    ///
370    /// For example, if this date's weekday is a Sunday and the first Sunday is
371    /// asked for (that is, `date.nth_weekday(1, Weekday::Sunday)`), then the
372    /// result is a week from this date corresponding to the following Sunday.
373    #[inline]
374    pub const fn nth_weekday(
375        self,
376        nth: i32,
377        weekday: Weekday,
378    ) -> Result<Date, RangeError> {
379        self.to_unix_epoch_day().nth_weekday(nth, weekday)
380    }
381
382    /// Returns the year one year before this date.
383    ///
384    /// Returns an error if this would result in a year outside the range
385    /// specified by [`Year`](b::Year).
386    #[inline]
387    pub(crate) const fn prev_year(self) -> Result<i16, RangeError> {
388        Ok(rtry!(b::Year::checked_add(self.year(), -1)))
389    }
390
391    /// Returns the year one year from this date.
392    #[inline]
393    pub(crate) const fn next_year(self) -> Result<i16, RangeError> {
394        Ok(rtry!(b::Year::checked_add(self.year(), 1)))
395    }
396
397    /// Adds the given number of days to this date.
398    ///
399    /// Returns an error if the result is outside the valid range of a `Date`.
400    #[inline]
401    pub const fn checked_add(self, days: i32) -> Result<Date, RangeError> {
402        match days {
403            0 => Ok(self),
404            -1 => self.yesterday(),
405            1 => self.tomorrow(),
406            n => Ok(ctry!(self.to_unix_epoch_day().checked_add(n)).to_date()),
407        }
408    }
409
410    /// Subtracts the given number of days to this date.
411    ///
412    /// Returns an error if the result is outside the valid range of a `Date`.
413    #[inline]
414    pub const fn checked_sub(self, days: i32) -> Result<Date, RangeError> {
415        let Some(days) = days.checked_neg() else {
416            rbail!(b::UnixEpochDays::error());
417        };
418        self.checked_add(days)
419    }
420
421    /// Returns the number of days from this date until `other`.
422    ///
423    /// This routine never overflows because of the enforced range on
424    /// `Date` values.
425    ///
426    /// # Example
427    ///
428    /// ```
429    /// use jiff_core::civil::date;
430    ///
431    /// let date1 = date(2023, 7, 1);
432    /// let date2 = date(2024, 7, 1);
433    /// assert_eq!(366, date1.until(date2));
434    ///
435    /// // The value will be negative when the dates are flipped:
436    /// assert_eq!(-366, date2.until(date1));
437    /// ```
438    ///
439    /// # Example: overflow isn't possible
440    ///
441    /// Even when using the minimum or maximum values:
442    ///
443    /// ```
444    /// use jiff_core::civil::Date;
445    ///
446    /// assert_eq!(7_304_483, Date::MIN.until(Date::MAX));
447    /// assert_eq!(-7_304_483, Date::MAX.until(Date::MIN));
448    /// ```
449    #[inline]
450    pub const fn until(self, other: Date) -> i32 {
451        -self.since(other)
452    }
453
454    /// Returns the number of days from this date since `other`.
455    ///
456    /// This routine never overflows because of the enforced range on
457    /// `Date` values.
458    ///
459    /// # Example
460    ///
461    /// ```
462    /// use jiff_core::civil::date;
463    ///
464    /// let date1 = date(2023, 7, 1);
465    /// let date2 = date(2024, 7, 1);
466    /// assert_eq!(366, date2.since(date1));
467    ///
468    /// // The value will be negative when the dates are flipped:
469    /// assert_eq!(-366, date1.since(date2));
470    /// ```
471    ///
472    /// # Example: overflow isn't possible
473    ///
474    /// Even when using the minimum or maximum values:
475    ///
476    /// ```
477    /// use jiff_core::civil::Date;
478    ///
479    /// assert_eq!(7_304_483, Date::MAX.since(Date::MIN));
480    /// ```
481    #[inline]
482    pub const fn since(self, other: Date) -> i32 {
483        // Try to avoid conversions to Unix epoch days in some cases.
484        if self.year() == other.year() {
485            if self.month() == other.month() {
486                (self.day() - other.day()) as i32
487            } else {
488                (self.day_of_year() - other.day_of_year()) as i32
489            }
490        } else {
491            self.to_unix_epoch_day().since(other.to_unix_epoch_day())
492        }
493    }
494
495    /// Converts a Gregorian date to a Unix epoch day.
496    #[inline]
497    #[allow(non_upper_case_globals, non_snake_case)] // to mimic source
498    pub const fn to_unix_epoch_day(self) -> UnixEpochDay {
499        // This is Neri-Schneider. There's no branching or divisions.
500        //
501        // Ref: https://github.com/cassioneri/eaf/blob/684d3cc32d14eee371d0abe4f683d6d6a49ed5c1/algorithms/neri_schneider.hpp#L83
502        const s: u32 = 82;
503        const K: u32 = 719468 + 146097 * s;
504        const L: u32 = 400 * s;
505
506        let year = self.year as u32;
507        let month = self.month as u32;
508        let day = self.day as u32;
509
510        let J = month <= 2;
511        let Y = year.wrapping_add(L).wrapping_sub(J as u32);
512        let M = if J { month + 12 } else { month };
513        let D = day - 1;
514        let C = Y / 100;
515
516        let y_star = 1461 * Y / 4 - C + C / 4;
517        let m_star = (979 * M - 2919) / 32;
518        let N = y_star + m_star + D;
519
520        let N_U = N.wrapping_sub(K);
521        let epoch_day = N_U as i32;
522        UnixEpochDay { day: epoch_day }
523    }
524
525    /// Converts this Gregorian date to an ISO 8601 week date.
526    #[inline]
527    pub const fn to_iso_week_date(self) -> ISOWeekDate {
528        let epoch_day = self.to_unix_epoch_day();
529        let mut year = self.year();
530        let mut epoch_day_year_start = iso_week_start_from_year(year);
531        if epoch_day.day() < epoch_day_year_start.day() {
532            // If our date comes before the first day of the ISO 8601 year,
533            // then it must be the case that our date falls at the end of the
534            // previous ISO 8601 year. This can happen for Gregorian dates
535            // Jan 1, 2 or 3.
536            //
537            // And this subtraction is OK because `year - 1` is only a problem
538            // when `year=-9999`. But we can't be here when `year=-9999` since
539            // that would imply an `epoch_day` that occurs before the first day
540            // of this date's Gregorian year. That would in turn imply a date
541            // before `-9999-01-01`, which is impossible.
542            year -= 1;
543            epoch_day_year_start = iso_week_start_from_year(year);
544        } else if self.month() == 12 && self.day() >= 29 && year < b::Year::MAX
545        {
546            // Otherwise, it's possible for dates at the end of the Gregorian
547            // calendar year to actually have an ISO 8601 week year following
548            // the Gregorian calendar year. This can occur for only Dec 29, 30
549            // or 31. For `year=9999`, we don't need to do this check because
550            // in that specific instance, the last day of `9999` corresponds to
551            // `9999-W52-5`. So it's not possible for the ISO 8601 week year to
552            // be `10000`.
553            let epoch_day_next_year_week_start =
554                iso_week_start_from_year(year + 1);
555            if epoch_day.day() >= epoch_day_next_year_week_start.day() {
556                epoch_day_year_start = epoch_day_next_year_week_start;
557                year += 1;
558            }
559        }
560
561        // OK because the biggest difference between epoch days here can be
562        // 370 (for a long year), and dividing that by 7 and adding 1 always
563        // fits into an `i8`.
564        let week =
565            (((epoch_day.day() - epoch_day_year_start.day()) / 7) + 1) as i8;
566        let weekday = epoch_day.weekday();
567
568        ISOWeekDate { year, week, weekday }
569    }
570
571    /// A convenience function for constructing a [`DateTime`] from this date
572    /// at the time given by its components.
573    ///
574    /// # Panics
575    ///
576    /// This panics if the provided values do not correspond to a valid `Time`.
577    /// All of the following conditions must be true:
578    ///
579    /// * `0 <= hour <= 23`
580    /// * `0 <= minute <= 59`
581    /// * `0 <= second <= 59`
582    /// * `0 <= subsec_nanosecond <= 999,999,999`
583    ///
584    /// Similarly, when used in a const context, invalid parameters will
585    /// prevent your Rust program from compiling.
586    #[inline]
587    pub const fn at(
588        self,
589        hour: i8,
590        minute: i8,
591        second: i8,
592        subsec_nanosecond: i32,
593    ) -> DateTime {
594        DateTime::from_parts(
595            self,
596            civil::time(hour, minute, second, subsec_nanosecond),
597        )
598    }
599}
600
601impl core::fmt::Debug for Date {
602    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
603        if self.year() < 0 {
604            write!(f, "-{:06}", self.year().unsigned_abs())?;
605        } else {
606            write!(f, "{:04}", self.year())?;
607        }
608        write!(f, "-{:02}-{:02}", self.month(), self.day())
609    }
610}
611
612/// Returns the number of days between two `Date` values.
613///
614/// This routine never overflows because of the enforced range on
615/// `Date` values.
616///
617/// # Example
618///
619/// ```
620/// use jiff_core::civil::date;
621///
622/// let date1 = date(2023, 7, 1);
623/// let date2 = date(2024, 7, 1);
624/// assert_eq!(366, date2 - date1);
625///
626/// // The value will be negative when the dates are flipped:
627/// assert_eq!(-366, date1 - date2);
628/// ```
629///
630/// # Example: overflow isn't possible
631///
632/// Even when using the minimum or maximum values:
633///
634/// ```
635/// use jiff_core::civil::Date;
636///
637/// assert_eq!(7_304_483, Date::MAX - Date::MIN);
638/// ```
639impl core::ops::Sub for Date {
640    type Output = i32;
641
642    #[inline]
643    fn sub(self, rhs: Date) -> i32 {
644        self.since(rhs)
645    }
646}
647
648#[cfg(test)]
649impl quickcheck::Arbitrary for Date {
650    fn arbitrary(g: &mut quickcheck::Gen) -> Date {
651        let year = b::Year::arbitrary(g);
652        let month = b::Month::arbitrary(g);
653        let day = b::Day::arbitrary(g);
654        Date::new_constrain(year, month, day).unwrap()
655    }
656
657    fn shrink(&self) -> alloc::boxed::Box<dyn Iterator<Item = Date>> {
658        alloc::boxed::Box::new(
659            (self.year(), self.month(), self.day()).shrink().filter_map(
660                |(year, month, day)| {
661                    Date::new_constrain(year, month, day).ok()
662                },
663            ),
664        )
665    }
666}
667
668/// A civil date represented by a number of days since the Unix epoch
669/// (1970-01-01).
670///
671/// The date can be positive (occurs after 1970-01-01) or negative (occurs
672/// before 1970-01-01). A zero value corresponds to 1970-01-01.
673///
674/// Unix epoch days can be infallibly converted to and from [`Date`] values.
675#[derive(Clone, Copy, Eq, Hash, PartialEq, PartialOrd, Ord)]
676#[cfg_attr(feature = "defmt", derive(defmt::Format))]
677pub struct UnixEpochDay {
678    day: i32,
679}
680
681impl UnixEpochDay {
682    /// The minimum allowed Unix epoch day.
683    ///
684    /// This is guaranteed to be equivalent to [`Date::MIN`] when
685    /// converted to a Gregorian date.
686    pub const MIN: UnixEpochDay = UnixEpochDay { day: b::UnixEpochDays::MIN };
687
688    /// The maximum allowed Unix epoch day.
689    ///
690    /// This is guaranteed to be equivalent to [`Date::MAX`] when
691    /// converted to a Gregorian date.
692    pub const MAX: UnixEpochDay = UnixEpochDay { day: b::UnixEpochDays::MAX };
693
694    /// Creates a new Unix epoch day.
695    ///
696    /// The day given must correspond to a number of days (positive or
697    /// negative) since the Unix epoch (1970-01-01). A value of `0` corresponds
698    /// to `1970-01-01`.
699    #[inline]
700    pub const fn new(day: i32) -> Result<UnixEpochDay, RangeError> {
701        let day = rtry!(b::UnixEpochDays::checkc(day as i64));
702        Ok(UnixEpochDay { day })
703    }
704
705    /// Returns the underlying day value.
706    ///
707    /// This is guaranteed to be in the [`UnixEpochDays`](b::UnixEpochDays)
708    /// range.
709    #[inline]
710    pub const fn day(self) -> i32 {
711        self.day
712    }
713
714    /// Returns the day of the week for this epoch day.
715    #[inline]
716    pub const fn weekday(&self) -> Weekday {
717        // Fast technique to obtain weekday 1..7 (Mon..Sun)
718        // directly via an add-mul-shift. Relies on the fact
719        // that the Unix epoch was a Thursday and that 7 is a
720        // Mersenne number.
721        //
722        // Accurate over a limited range (-89478489 to 89478489)
723        // which is -243014-03-21 (Tue) to 246953-10-13 (Sat).
724        // This exceeds Jiff's range.
725        //
726        // Ref: https://www.benjoffe.com/fast-day-of-week
727        let result = Weekday::from_monday_one_offset({
728            const M: u32 = {
729                // MSRV(1.73): Just use `n.div_ceil` instead.
730                const fn div_ceil(lhs: u64, rhs: u64) -> u64 {
731                    let d = lhs / rhs;
732                    let r = lhs % rhs;
733                    if r > 0 {
734                        d + 1
735                    } else {
736                        d
737                    }
738                }
739
740                let n = div_ceil(1u64 << 32, 7) as u32;
741                assert!(n == 613_566_757);
742                n
743            };
744            const Z: u32 = 0x90000000; // Magic add: see link above.
745            let rd: u32 = self.day() as u32;
746            (rd.wrapping_mul(M).wrapping_add(Z) >> 29) as i8
747        });
748        unwrapr!(result, "weekday must be in range 1..=7")
749    }
750
751    /// Returns the "nth" weekday from this date, not including itself.
752    ///
753    /// The `nth` parameter can be positive or negative. A positive value
754    /// computes the "nth" weekday starting at the day after this date and
755    /// going forwards in time. A negative value computes the "nth" weekday
756    /// starting at the day before this date and going backwards in time.
757    ///
758    /// For example, if this date's weekday is a Sunday and the first Sunday is
759    /// asked for (that is, `date.nth_weekday(1, Weekday::Sunday)`), then the
760    /// result is a week from this date corresponding to the following Sunday.
761    #[inline]
762    pub const fn nth_weekday(
763        self,
764        nth: i32,
765        weekday: Weekday,
766    ) -> Result<Date, RangeError> {
767        // ref: http://howardhinnant.github.io/date_algorithms.html#next_weekday
768
769        let nth = rtry!(b::NthWeekday::checkc(nth as i64));
770        if nth == 0 {
771            rbail!(b::NthWeekday::error());
772        } else if nth > 0 {
773            let weekday_diff = weekday.since(self.weekday().next()) as i32;
774            let diff = (nth - 1) * 7 + weekday_diff;
775            let end = ctry!(self.checked_add(diff + 1));
776            Ok(end.to_date())
777        } else {
778            let weekday_diff = self.weekday().previous().since(weekday) as i32;
779            // OK because of the range on `NthWeekday`.
780            let nth = nth.abs();
781            // OK because of the range on `NthWeekday`.
782            let diff = -((nth - 1) * 7 + weekday_diff);
783            let end = ctry!(self.checked_add(diff - 1));
784            Ok(end.to_date())
785        }
786    }
787
788    /// Add the given number of days to this Unix epoch day.
789    ///
790    /// If this would overflow an `i32` or result in an out-of-bounds Unix
791    /// epoch day, then this returns an error.
792    #[inline]
793    pub const fn checked_add(
794        self,
795        days: i32,
796    ) -> Result<UnixEpochDay, RangeError> {
797        let day = rtry!(b::UnixEpochDays::checked_add(self.day(), days));
798        Ok(UnixEpochDay { day })
799    }
800
801    /// Subtracts the given number of days from this Unix epoch day.
802    ///
803    /// If this would overflow an `i32` or result in an out-of-bounds Unix
804    /// epoch day, then this returns an error.
805    #[inline]
806    pub const fn checked_sub(
807        self,
808        days: i32,
809    ) -> Result<UnixEpochDay, RangeError> {
810        let Some(days) = days.checked_neg() else {
811            rbail!(b::UnixEpochDays::error());
812        };
813        self.checked_add(days)
814    }
815
816    /// Returns the number of days from this date until `other`.
817    ///
818    /// This routine never overflows because of the enforced range on
819    /// `UnixEpochDay` values.
820    ///
821    /// # Example
822    ///
823    /// ```
824    /// use jiff_core::civil::date;
825    ///
826    /// let date1 = date(2023, 7, 1).to_unix_epoch_day();
827    /// let date2 = date(2024, 7, 1).to_unix_epoch_day();
828    /// assert_eq!(366, date1.until(date2));
829    ///
830    /// // The value will be negative when the dates are flipped:
831    /// assert_eq!(-366, date2.until(date1));
832    /// ```
833    ///
834    /// # Example: overflow isn't possible
835    ///
836    /// Even when using the minimum or maximum values:
837    ///
838    /// ```
839    /// use jiff_core::civil::UnixEpochDay;
840    ///
841    /// assert_eq!(7_304_483, UnixEpochDay::MIN.until(UnixEpochDay::MAX));
842    /// assert_eq!(-7_304_483, UnixEpochDay::MAX.until(UnixEpochDay::MIN));
843    /// ```
844    #[inline]
845    pub const fn until(self, other: UnixEpochDay) -> i32 {
846        -self.since(other)
847    }
848
849    /// Returns the number of days from this date since `other`.
850    ///
851    /// This routine never overflows because of the enforced range on
852    /// `UnixEpochDay` values.
853    ///
854    /// # Example
855    ///
856    /// ```
857    /// use jiff_core::civil::date;
858    ///
859    /// let date1 = date(2023, 7, 1).to_unix_epoch_day();
860    /// let date2 = date(2024, 7, 1).to_unix_epoch_day();
861    /// assert_eq!(366, date2.since(date1));
862    ///
863    /// // The value will be negative when the dates are flipped:
864    /// assert_eq!(-366, date1.since(date2));
865    /// ```
866    ///
867    /// # Example: overflow isn't possible
868    ///
869    /// Even when using the minimum or maximum values:
870    ///
871    /// ```
872    /// use jiff_core::civil::UnixEpochDay;
873    ///
874    /// assert_eq!(7_304_483, UnixEpochDay::MAX.since(UnixEpochDay::MIN));
875    /// ```
876    #[inline]
877    pub const fn since(self, other: UnixEpochDay) -> i32 {
878        self.day() - other.day()
879    }
880
881    /// Converts this Unix epoch day to a Gregorian date.
882    #[inline]
883    #[allow(non_upper_case_globals, non_snake_case)] // to mimic source
884    pub const fn to_date(self) -> Date {
885        // This is Neri-Schneider. There's no branching or divisions.
886        //
887        // Ref: <https://github.com/cassioneri/eaf/blob/684d3cc32d14eee371d0abe4f683d6d6a49ed5c1/algorithms/neri_schneider.hpp#L40C3-L40C34>
888        const s: u32 = 82;
889        const K: u32 = 719468 + 146097 * s;
890        const L: u32 = 400 * s;
891
892        let N_U = self.day as u32;
893        let N = N_U.wrapping_add(K);
894
895        let N_1 = 4 * N + 3;
896        let C = N_1 / 146097;
897        let N_C = (N_1 % 146097) / 4;
898
899        let N_2 = 4 * N_C + 3;
900        let P_2 = 2939745 * (N_2 as u64);
901        let Z = (P_2 / 4294967296) as u32;
902        let N_Y = (P_2 % 4294967296) as u32 / 2939745 / 4;
903        let Y = 100 * C + Z;
904
905        let N_3 = 2141 * N_Y + 197913;
906        let M = N_3 / 65536;
907        let D = (N_3 % 65536) / 2141;
908
909        let J = N_Y >= 306;
910        let year = Y.wrapping_sub(L).wrapping_add(J as u32) as i16;
911        let month = (if J { M - 12 } else { M }) as i8;
912        let day = (D + 1) as i8;
913        Date { year, month, day }
914    }
915}
916
917impl core::fmt::Debug for UnixEpochDay {
918    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
919        f.debug_tuple("UnixEpochDay").field(&self.day()).finish()
920    }
921}
922
923/// Returns the number of days between two `UnixEpochDay` values.
924///
925/// This routine never overflows because of the enforced range on
926/// `UnixEpochDay` values.
927///
928/// # Example
929///
930/// ```
931/// use jiff_core::civil::date;
932///
933/// let date1 = date(2023, 7, 1).to_unix_epoch_day();
934/// let date2 = date(2024, 7, 1).to_unix_epoch_day();
935/// assert_eq!(366, date2 - date1);
936///
937/// // The value will be negative when the dates are flipped:
938/// assert_eq!(-366, date1 - date2);
939/// ```
940///
941/// # Example: overflow isn't possible
942///
943/// Even when using the minimum or maximum values:
944///
945/// ```
946/// use jiff_core::civil::UnixEpochDay;
947///
948/// assert_eq!(7_304_483, UnixEpochDay::MAX - UnixEpochDay::MIN);
949/// ```
950impl core::ops::Sub for UnixEpochDay {
951    type Output = i32;
952
953    #[inline]
954    fn sub(self, rhs: UnixEpochDay) -> i32 {
955        self.since(rhs)
956    }
957}
958
959/// An ISO 8601 civil week date.
960///
961/// An ISO  8601 civil week date can be infallibly converted to and from
962/// Gregorian [`Date`] values.
963#[derive(Clone, Copy, Eq, Hash, PartialEq)]
964#[cfg_attr(feature = "defmt", derive(defmt::Format))]
965pub struct ISOWeekDate {
966    year: i16,
967    week: i8,
968    weekday: Weekday,
969}
970
971impl ISOWeekDate {
972    /// The maximum representable ISO week date.
973    pub const MIN: ISOWeekDate = ISOWeekDate {
974        year: b::ISOYear::MIN,
975        week: b::ISOWeek::MIN,
976        weekday: Weekday::Monday,
977    };
978
979    /// The minimum representable ISO week date.
980    pub const MAX: ISOWeekDate = ISOWeekDate {
981        year: b::ISOYear::MAX,
982        // Technical max is 52, but 9999 is not a leap year.
983        week: 52,
984        weekday: Weekday::Friday,
985    };
986
987    /// The zero ISO 8601 week date. It is the first day of the zeroth year.
988    pub const ZERO: ISOWeekDate =
989        ISOWeekDate { year: 0, week: 1, weekday: Weekday::Monday };
990
991    /// Create a new ISO week date from its constituent parts.
992    ///
993    /// If the week date is invalid or any of the values are out of their
994    /// supported ranges, then an error is returned.
995    #[inline]
996    pub const fn new(
997        year: i16,
998        week: i8,
999        weekday: Weekday,
1000    ) -> Result<ISOWeekDate, RangeError> {
1001        let year = rtry!(b::ISOYear::checkc(year as i64));
1002        let week = rtry!(b::ISOWeek::checkc(week as i64));
1003
1004        // All combinations of years, weeks and weekdays allowed by our
1005        // range types are valid ISO week dates with one exception: a week
1006        // number of 53 is only valid for "long" years. Or years with an ISO
1007        // leap week. It turns out this only happens when the last day of the
1008        // year is a Thursday.
1009        //
1010        // Note that if the ranges in this crate are changed, this could be
1011        // a little trickier if the range of ISOYear is different from Year.
1012        debug_assert!(b::Year::MIN == b::ISOYear::MIN);
1013        debug_assert!(b::Year::MAX == b::ISOYear::MAX);
1014        if week == 53 && !civil::is_long_iso_week_year(year) {
1015            rbail!(b::ISOWeek::error());
1016        }
1017        // And also, the maximum Date constrains what we can utter with
1018        // ISOWeekDate so that we can preserve infallible conversions between
1019        // them. So since 9999-12-31 maps to 9999 W52 Friday, it follows that
1020        // Saturday and Sunday are not allowed when the year is at the maximum
1021        // value. So reject them.
1022        //
1023        // We don't need to worry about the minimum because the minimum date
1024        // (-9999-01-01) corresponds also to the minimum possible combination
1025        // of an ISO week date's fields: -9999 W01 Monday. Nice.
1026        if year == b::ISOYear::MAX
1027            && week == 52
1028            && weekday.to_monday_zero_offset()
1029                > Weekday::Friday.to_monday_zero_offset()
1030        {
1031            rbail!(b::WeekdayMondayOne::error());
1032        }
1033        Ok(ISOWeekDate { year, week, weekday })
1034    }
1035
1036    /// Like `ISOWeekDate::new`, but constrains out-of-bounds week and weekday
1037    /// values to their closest valid equivalent.
1038    ///
1039    /// For example, given `9999 W52 Saturday`, this will return
1040    /// `9999 W52 Friday`.
1041    ///
1042    /// This still returns an error when `week < 1` or when `year` is invalid.
1043    #[inline]
1044    pub const fn new_constrain(
1045        year: i16,
1046        mut week: i8,
1047        mut weekday: Weekday,
1048    ) -> Result<ISOWeekDate, RangeError> {
1049        let year = rtry!(b::ISOYear::checkc(year as i64));
1050        if week < 1 {
1051            rbail!(b::ISOWeek::error());
1052        }
1053        if week == 53 && !civil::is_long_iso_week_year(year) {
1054            week = 52;
1055        }
1056        if year == b::ISOYear::MAX
1057            && week == 52
1058            && weekday.to_monday_zero_offset()
1059                > Weekday::Friday.to_monday_zero_offset()
1060        {
1061            weekday = Weekday::Friday;
1062        }
1063        Ok(ISOWeekDate { year, week, weekday })
1064    }
1065
1066    /// Returns the year component of this ISO 8601 week date.
1067    ///
1068    /// The value returned is guaranteed to be in the range specified by
1069    /// [`ISOYear`](crate::bounds::ISOYear).
1070    #[inline]
1071    pub const fn year(self) -> i16 {
1072        self.year
1073    }
1074
1075    /// Returns the week number component of this ISO 8601 week date.
1076    ///
1077    /// The value returned is guaranteed to be in the range specified by
1078    /// [`ISOWeek`](crate::bounds::ISOWeek).
1079    #[inline]
1080    pub const fn week(self) -> i8 {
1081        self.week
1082    }
1083
1084    /// Returns the weekday component of this ISO 8601 week date.
1085    #[inline]
1086    pub const fn weekday(self) -> Weekday {
1087        self.weekday
1088    }
1089
1090    /// Returns the ISO 8601 week date corresponding to the first day in the
1091    /// week of this week date. The date returned is guaranteed to have a
1092    /// weekday of [`Weekday::Monday`].
1093    ///
1094    /// # Errors
1095    ///
1096    /// Since `-9999-01-01` falls on a Monday, it follows that the minimum
1097    /// supported Gregorian date is exactly equivalent to the minimum supported
1098    /// ISO 8601 week date. This means that this routine can never actually
1099    /// fail, but only insomuch as the minimums line up. For that reason, and
1100    /// for consistency with [`ISOWeekDate::last_of_week`], the API is
1101    /// fallible.
1102    #[inline]
1103    pub const fn first_of_week(self) -> Result<ISOWeekDate, RangeError> {
1104        // I believe this can never return an error because `Monday` is in
1105        // bounds for all possible year-and-week combinations. This is *only*
1106        // because -9999-01-01 corresponds to -9999-W01-Monday. Which is kinda
1107        // lucky. And I guess if we ever change the ranges, this could become
1108        // fallible.
1109        Ok(ISOWeekDate { weekday: Weekday::Monday, ..self })
1110    }
1111
1112    /// Returns the ISO 8601 week date corresponding to the last day in the
1113    /// week of this week date. The date returned is guaranteed to have a
1114    /// weekday of [`Weekday::Sunday`].
1115    ///
1116    /// # Errors
1117    ///
1118    /// This can return an error if the last day of the week exceeds Jiff's
1119    /// maximum Gregorian date of `9999-12-31`. It turns out this can happen
1120    /// since `9999-12-31` falls on a Friday.
1121    #[inline]
1122    pub const fn last_of_week(self) -> Result<ISOWeekDate, RangeError> {
1123        ISOWeekDate::new(self.year(), self.week(), Weekday::Sunday)
1124    }
1125
1126    /// Returns the ISO 8601 week date corresponding to the first day in the
1127    /// year of this week date. The date returned is guaranteed to have a
1128    /// weekday of [`Weekday::Monday`].
1129    ///
1130    /// # Errors
1131    ///
1132    /// Since `-9999-01-01` falls on a Monday, it follows that the minimum
1133    /// support Gregorian date is exactly equivalent to the minimum supported
1134    /// ISO 8601 week date. This means that this routine can never actually
1135    /// fail, but only insomuch as the minimums line up. For that reason, and
1136    /// for consistency with [`ISOWeekDate::last_of_year`], the API is
1137    /// fallible.
1138    #[inline]
1139    pub const fn first_of_year(self) -> Result<ISOWeekDate, RangeError> {
1140        // I believe this can never return an error because `Monday` is in
1141        // bounds for all possible year-and-week combinations. This is *only*
1142        // because -9999-01-01 corresponds to -9999-W01-Monday. Which is kinda
1143        // lucky. And I guess if we ever change the ranges, this could become
1144        // fallible.
1145        Ok(ISOWeekDate { week: 1, weekday: Weekday::Monday, ..self })
1146    }
1147
1148    /// Returns the ISO 8601 week date corresponding to the last day in the
1149    /// year of this week date. The date returned is guaranteed to have a
1150    /// weekday of [`Weekday::Sunday`].
1151    ///
1152    /// # Errors
1153    ///
1154    /// This can return an error if the last day of the year exceeds Jiff's
1155    /// maximum Gregorian date of `9999-12-31`. It turns out this can happen
1156    /// since `9999-12-31` falls on a Friday.
1157    #[inline]
1158    pub const fn last_of_year(self) -> Result<ISOWeekDate, RangeError> {
1159        ISOWeekDate::new(self.year(), self.weeks_in_year(), Weekday::Sunday)
1160    }
1161
1162    /// Returns the total number of days in the year of this ISO 8601 week
1163    /// date.
1164    ///
1165    /// It is guaranteed that the value returned is either 364 or 371. The
1166    /// latter case occurs precisely when [`ISOWeekDate::in_long_year`]
1167    /// returns `true`.
1168    #[inline]
1169    pub const fn days_in_year(self) -> i16 {
1170        if self.in_long_year() {
1171            371
1172        } else {
1173            364
1174        }
1175    }
1176
1177    /// Returns the total number of weeks in the year of this ISO 8601 week
1178    /// date.
1179    ///
1180    /// It is guaranteed that the value returned is either 52 or 53. The
1181    /// latter case occurs precisely when [`ISOWeekDate::in_long_year`]
1182    /// returns `true`.
1183    #[inline]
1184    pub const fn weeks_in_year(self) -> i8 {
1185        civil::weeks_in_iso_week_year(self.year())
1186    }
1187
1188    /// Returns true if and only if the year of this week date is a "long"
1189    /// year.
1190    ///
1191    /// A long year is one that contains precisely 53 weeks. All other years
1192    /// contain precisely 52 weeks.
1193    #[inline]
1194    pub const fn in_long_year(self) -> bool {
1195        civil::is_long_iso_week_year(self.year())
1196    }
1197
1198    /// Returns the ISO 8601 date immediately following this one.
1199    ///
1200    /// # Errors
1201    ///
1202    /// This returns an error when this date is the maximum value.
1203    #[inline]
1204    pub const fn tomorrow(self) -> Result<ISOWeekDate, RangeError> {
1205        // The maximum week date is `9999-W52-5`, which is a Friday. It doesn't
1206        // end on a Sunday, so adjusting the logic below to check for it is
1207        // a bit weird. So we just check here.
1208        if self.year() == ISOWeekDate::MAX.year()
1209            && self.week() == ISOWeekDate::MAX.week()
1210            && matches!(self.weekday(), Weekday::Friday)
1211        {
1212            rbail!(b::ISOYear::error());
1213        }
1214        // I suppose we could probably implement this in a more efficient
1215        // manner by avoiding the roundtrip through Gregorian dates.
1216        // self.to_date().tomorrow().map(|d| d.to_iso_week_date())
1217        if matches!(self.weekday(), Weekday::Sunday) {
1218            if self.week() >= 52 && self.week() == self.weeks_in_year() {
1219                let year = self.year() + 1;
1220                return Ok(ISOWeekDate {
1221                    year,
1222                    week: 1,
1223                    weekday: Weekday::Monday,
1224                });
1225            }
1226            let week = self.week() + 1;
1227            return Ok(ISOWeekDate { week, weekday: Weekday::Monday, ..self });
1228        }
1229        Ok(ISOWeekDate { weekday: self.weekday().next(), ..self })
1230    }
1231
1232    /// Returns the ISO 8601 week date immediately preceding this one.
1233    ///
1234    /// # Errors
1235    ///
1236    /// This returns an error when this date is the minimum value.
1237    #[inline]
1238    pub fn yesterday(self) -> Result<ISOWeekDate, RangeError> {
1239        if matches!(self.weekday(), Weekday::Monday) {
1240            if self.week() == 1 {
1241                let year = rtry!(b::ISOYear::checked_add(self.year(), -1));
1242                let week = civil::weeks_in_iso_week_year(year);
1243                return Ok(ISOWeekDate {
1244                    year,
1245                    week,
1246                    weekday: Weekday::Sunday,
1247                });
1248            }
1249            let week = self.week() - 1;
1250            return Ok(ISOWeekDate { week, weekday: Weekday::Sunday, ..self });
1251        }
1252        Ok(ISOWeekDate { weekday: self.weekday().previous(), ..self })
1253    }
1254
1255    /// Converts this ISO 8601 week date to a Unix epoch day.
1256    #[inline]
1257    pub const fn to_unix_epoch_day(self) -> UnixEpochDay {
1258        let epoch_day_year = iso_week_start_from_year(self.year());
1259        let week = self.week() as i32;
1260        let weekday = self.weekday().to_monday_zero_offset() as i32;
1261        unwrapr!(
1262            epoch_day_year.checked_add(((week - 1) * 7) + weekday),
1263            "all valid ISO 8601 dates convert to a valid Unix epoch day",
1264        )
1265    }
1266
1267    /// Converts this ISO 8601 week date to a Gregorian date.
1268    #[inline]
1269    pub const fn to_date(self) -> Date {
1270        self.to_unix_epoch_day().to_date()
1271    }
1272}
1273
1274impl core::fmt::Debug for ISOWeekDate {
1275    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
1276        write!(
1277            f,
1278            "{:04}-W{:02}-{}",
1279            self.year,
1280            self.week,
1281            self.weekday.to_monday_one_offset()
1282        )
1283    }
1284}
1285
1286impl Ord for ISOWeekDate {
1287    #[inline]
1288    fn cmp(&self, other: &ISOWeekDate) -> core::cmp::Ordering {
1289        (self.year(), self.week(), self.weekday().to_monday_one_offset()).cmp(
1290            &(
1291                other.year(),
1292                other.week(),
1293                other.weekday().to_monday_one_offset(),
1294            ),
1295        )
1296    }
1297}
1298
1299impl PartialOrd for ISOWeekDate {
1300    #[inline]
1301    fn partial_cmp(&self, other: &ISOWeekDate) -> Option<core::cmp::Ordering> {
1302        Some(self.cmp(other))
1303    }
1304}
1305
1306/// Returns the Unix epoch day corresponding to the first day in the ISO 8601
1307/// week year given.
1308///
1309/// Callers must ensure that `year` is in the range specified by
1310/// [`Year`](b::Year).
1311///
1312/// Ref: http://howardhinnant.github.io/date_algorithms.html
1313const fn iso_week_start_from_year(year: i16) -> UnixEpochDay {
1314    debug_assert!(b::Year::checkc(year as i64).is_ok());
1315    // A week's year always corresponds to the Gregorian year in which the
1316    // Thursday of that week falls. Therefore, Jan 4 is *always* in the first
1317    // week of any ISO week year.
1318    let epoch_day_in_first_week =
1319        Date { year, month: 1, day: 4 }.to_unix_epoch_day();
1320    // The start of the first week is a Monday, so find the number of days
1321    // since Monday from a date that we know is in the first ISO week of
1322    // `year`.
1323    let diff_from_monday =
1324        epoch_day_in_first_week.weekday().since(Weekday::Monday);
1325    // OK because `diff_from_monday` is never bigger than 6 and is always
1326    // positive. Therefore, the only case where this could plausibly fail
1327    // is when `year=-9999`. But in that specific case, -9999-01-01 is on a
1328    // Monday, so the diff is guaranteed to give us the minimal Unix epoch
1329    // day value.
1330    unwrapr!(
1331        epoch_day_in_first_week.checked_sub(diff_from_monday as i32),
1332        "valid Unix epoch day"
1333    )
1334}
1335
1336#[cfg(test)]
1337impl quickcheck::Arbitrary for ISOWeekDate {
1338    fn arbitrary(g: &mut quickcheck::Gen) -> ISOWeekDate {
1339        let year = b::ISOYear::arbitrary(g);
1340        let week = b::ISOWeek::arbitrary(g);
1341        let weekday = Weekday::arbitrary(g);
1342        ISOWeekDate::new_constrain(year, week, weekday).unwrap()
1343    }
1344
1345    fn shrink(&self) -> alloc::boxed::Box<dyn Iterator<Item = ISOWeekDate>> {
1346        alloc::boxed::Box::new(
1347            (self.year(), self.week(), self.weekday()).shrink().filter_map(
1348                |(year, week, weekday)| {
1349                    ISOWeekDate::new_constrain(year, week, weekday).ok()
1350                },
1351            ),
1352        )
1353    }
1354}
1355
1356#[cfg(test)]
1357mod tests {
1358    use super::*;
1359
1360    fn date(year: i16, month: i8, day: i8) -> Date {
1361        Date::new(year, month, day).unwrap()
1362    }
1363
1364    fn week_date(year: i16, week: i8, weekday: Weekday) -> ISOWeekDate {
1365        ISOWeekDate::new(year, week, weekday).unwrap()
1366    }
1367
1368    #[test]
1369    fn date_min() {
1370        assert_eq!(Date::MIN, date(-9999, 1, 1));
1371    }
1372
1373    #[test]
1374    fn date_max() {
1375        assert_eq!(Date::MAX, date(9999, 12, 31));
1376    }
1377
1378    #[test]
1379    fn unix_epoch_to_date_and_back_again_min_to_max() {
1380        for i in 0.. {
1381            let Ok(epoch_day) = UnixEpochDay::MIN.checked_add(i) else {
1382                break;
1383            };
1384            let date = epoch_day.to_date();
1385            let got = date.to_unix_epoch_day();
1386            assert_eq!(epoch_day, got);
1387        }
1388    }
1389
1390    #[test]
1391    fn unix_epoch_to_date_and_back_again_max_to_min() {
1392        for i in 0.. {
1393            let Ok(epoch_day) = UnixEpochDay::MAX.checked_sub(i) else {
1394                break;
1395            };
1396            let date = epoch_day.to_date();
1397            let got = date.to_unix_epoch_day();
1398            assert_eq!(epoch_day, got);
1399        }
1400    }
1401
1402    #[test]
1403    fn date_to_unix_epoch_and_back_again() {
1404        for year in b::Year::MIN..=b::Year::MAX {
1405            for month in b::Month::MIN..=b::Month::MAX {
1406                for day in b::Day::MIN..=civil::days_in_month(year, month) {
1407                    let d = date(year, month, day);
1408                    let epoch_day = d.to_unix_epoch_day();
1409                    let got = epoch_day.to_date();
1410                    assert_eq!(d, got);
1411                }
1412            }
1413        }
1414    }
1415
1416    #[test]
1417    fn date_to_week_date_and_back_again() {
1418        for year in b::Year::MIN..=b::Year::MAX {
1419            for month in b::Month::MIN..=b::Month::MAX {
1420                for day in b::Day::MIN..=civil::days_in_month(year, month) {
1421                    let d = date(year, month, day);
1422                    let wd = d.to_iso_week_date();
1423                    let got = wd.to_date();
1424                    assert_eq!(d, got);
1425                }
1426            }
1427        }
1428    }
1429
1430    #[test]
1431    fn date_to_day_of_year_and_back_again() {
1432        for year in b::Year::MIN..=b::Year::MAX {
1433            for month in b::Month::MIN..=b::Month::MAX {
1434                for day in b::Day::MIN..=civil::days_in_month(year, month) {
1435                    let d = date(year, month, day);
1436                    let doy = d.day_of_year();
1437                    let got = Date::from_day_of_year(year, doy);
1438                    assert_eq!(Ok(d), got);
1439                }
1440            }
1441        }
1442    }
1443
1444    #[test]
1445    fn day_of_year_in_non_leap_year() {
1446        let year = 2026;
1447        let mut doy = 1;
1448        for month in b::Month::MIN..=b::Month::MAX {
1449            for day in b::Day::MIN..=civil::days_in_month(year, month) {
1450                let d = Date::from_day_of_year(year, doy).unwrap();
1451                assert_eq!(d, date(year, month, day));
1452                assert_eq!(d.day_of_year(), doy);
1453                doy += 1;
1454            }
1455        }
1456    }
1457
1458    #[test]
1459    fn day_of_year_in_leap_year() {
1460        let year = 2024;
1461        let mut doy = 1;
1462        for month in b::Month::MIN..=b::Month::MAX {
1463            for day in b::Day::MIN..=civil::days_in_month(year, month) {
1464                let d = Date::from_day_of_year(year, doy).unwrap();
1465                assert_eq!(d, date(year, month, day));
1466                assert_eq!(d.day_of_year(), doy);
1467                doy += 1;
1468            }
1469        }
1470    }
1471
1472    #[test]
1473    fn day_of_year_no_leap_in_non_leap_year() {
1474        let year = 2026;
1475        let mut doy = 1;
1476        for month in b::Month::MIN..=b::Month::MAX {
1477            for day in b::Day::MIN..=civil::days_in_month(year, month) {
1478                let d = Date::from_day_of_year_no_leap(year, doy).unwrap();
1479                assert_eq!(d, date(year, month, day));
1480                assert_eq!(d.day_of_year_no_leap(), Some(doy));
1481                doy += 1;
1482            }
1483        }
1484    }
1485
1486    #[test]
1487    fn day_of_year_no_leap_in_leap_year() {
1488        let year = 2024;
1489        let mut doy = 1;
1490        for month in b::Month::MIN..=b::Month::MAX {
1491            for day in b::Day::MIN..=civil::days_in_month(year, month) {
1492                if month == 2 && day == 29 {
1493                    continue;
1494                }
1495                let d = Date::from_day_of_year_no_leap(year, doy).unwrap();
1496                assert_eq!(d, date(year, month, day));
1497                assert_eq!(d.day_of_year_no_leap(), Some(doy));
1498                doy += 1;
1499            }
1500        }
1501    }
1502
1503    #[test]
1504    fn date_to_day_of_year_no_leap_and_back_again() {
1505        for year in b::Year::MIN..=b::Year::MAX {
1506            for month in b::Month::MIN..=b::Month::MAX {
1507                for day in b::Day::MIN..=civil::days_in_month(year, month) {
1508                    if month == 2 && day == 29 {
1509                        continue;
1510                    }
1511                    let d = date(year, month, day);
1512                    let doy = d.day_of_year_no_leap().unwrap();
1513                    let got = Date::from_day_of_year_no_leap(year, doy);
1514                    assert_eq!(Ok(d), got);
1515                }
1516            }
1517        }
1518    }
1519
1520    #[test]
1521    fn unix_epoch_day_weekday() {
1522        let mk = |day| date(2026, 2, day).weekday();
1523        assert_eq!(mk(14), Weekday::Saturday);
1524        assert_eq!(mk(15), Weekday::Sunday);
1525        assert_eq!(mk(16), Weekday::Monday);
1526        assert_eq!(mk(17), Weekday::Tuesday);
1527        assert_eq!(mk(18), Weekday::Wednesday);
1528        assert_eq!(mk(19), Weekday::Thursday);
1529        assert_eq!(mk(20), Weekday::Friday);
1530        assert_eq!(mk(21), Weekday::Saturday);
1531    }
1532
1533    #[test]
1534    fn first_of_last_of() {
1535        for year in b::Year::MIN..=b::Year::MAX {
1536            for month in b::Month::MIN..=b::Month::MAX {
1537                for day in b::Day::MIN..=civil::days_in_month(year, month) {
1538                    let d = date(year, month, day);
1539
1540                    assert_eq!(d.first_of_month(), date(year, month, 1));
1541                    assert_eq!(
1542                        d.last_of_month(),
1543                        date(year, month, d.days_in_month())
1544                    );
1545
1546                    assert_eq!(d.first_of_year(), date(year, 1, 1));
1547                    assert_eq!(d.last_of_year(), date(year, 12, 31));
1548                }
1549            }
1550        }
1551    }
1552
1553    #[test]
1554    fn days_in() {
1555        for year in b::Year::MIN..=b::Year::MAX {
1556            for month in b::Month::MIN..=b::Month::MAX {
1557                for day in b::Day::MIN..=civil::days_in_month(year, month) {
1558                    let d = date(year, month, day);
1559
1560                    assert!([365, 366].contains(&d.days_in_year()));
1561                    assert!([28, 29, 30, 31].contains(&d.days_in_month()));
1562                }
1563            }
1564        }
1565    }
1566
1567    #[test]
1568    fn nth_weekday_of_month_various() {
1569        let d1 = date(2017, 3, 1);
1570        let wday = Weekday::Friday;
1571        assert_eq!(d1.nth_weekday_of_month(2, wday), Ok(date(2017, 3, 10)));
1572
1573        let d1 = date(2024, 3, 1);
1574        let wday = Weekday::Thursday;
1575        assert_eq!(d1.nth_weekday_of_month(-1, wday), Ok(date(2024, 3, 28)));
1576
1577        let d1 = date(2024, 3, 25);
1578        let wday = Weekday::Monday;
1579        assert!(d1.nth_weekday_of_month(5, wday).is_err());
1580        assert!(d1.nth_weekday_of_month(-5, wday).is_err());
1581
1582        let d1 = date(1998, 1, 1);
1583        let wday = Weekday::Saturday;
1584        assert_eq!(d1.nth_weekday_of_month(5, wday), Ok(date(1998, 1, 31)));
1585    }
1586
1587    #[test]
1588    fn nth_weekday_of_month_errors() {
1589        let d = date(2017, 3, 1);
1590        let wday = Weekday::Tuesday;
1591
1592        assert!(d.nth_weekday_of_month(0, wday).is_err());
1593        assert!(d.nth_weekday_of_month(5, wday).is_err());
1594        assert!(d.nth_weekday_of_month(-5, wday).is_err());
1595        assert!(d.nth_weekday_of_month(6, wday).is_err());
1596        assert!(d.nth_weekday_of_month(-6, wday).is_err());
1597        assert!(d.nth_weekday_of_month(i8::MIN, wday).is_err());
1598        assert!(d.nth_weekday_of_month(i8::MAX, wday).is_err());
1599
1600        assert_eq!(
1601            d.nth_weekday_of_month(5, Weekday::Friday),
1602            Ok(date(2017, 3, 31))
1603        );
1604        assert_eq!(
1605            d.nth_weekday_of_month(-5, Weekday::Friday),
1606            Ok(date(2017, 3, 3))
1607        );
1608    }
1609
1610    #[test]
1611    fn nth_weekday_of_month_near_minimum_date() {
1612        let d = date(-9999, 1, 1);
1613
1614        assert_eq!(
1615            d.nth_weekday_of_month(1, Weekday::Monday),
1616            Ok(date(-9999, 1, 1))
1617        );
1618        assert_eq!(
1619            d.nth_weekday_of_month(2, Weekday::Monday),
1620            Ok(date(-9999, 1, 8))
1621        );
1622        assert_eq!(
1623            d.nth_weekday_of_month(3, Weekday::Monday),
1624            Ok(date(-9999, 1, 15))
1625        );
1626        assert_eq!(
1627            d.nth_weekday_of_month(4, Weekday::Monday),
1628            Ok(date(-9999, 1, 22))
1629        );
1630        assert_eq!(
1631            d.nth_weekday_of_month(5, Weekday::Monday),
1632            Ok(date(-9999, 1, 29))
1633        );
1634
1635        assert_eq!(
1636            d.nth_weekday_of_month(-5, Weekday::Monday),
1637            Ok(date(-9999, 1, 1))
1638        );
1639        assert_eq!(
1640            d.nth_weekday_of_month(-4, Weekday::Monday),
1641            Ok(date(-9999, 1, 8))
1642        );
1643        assert_eq!(
1644            d.nth_weekday_of_month(-3, Weekday::Monday),
1645            Ok(date(-9999, 1, 15))
1646        );
1647        assert_eq!(
1648            d.nth_weekday_of_month(-2, Weekday::Monday),
1649            Ok(date(-9999, 1, 22))
1650        );
1651        assert_eq!(
1652            d.nth_weekday_of_month(-1, Weekday::Monday),
1653            Ok(date(-9999, 1, 29))
1654        );
1655    }
1656
1657    #[test]
1658    fn nth_weekday_of_month_near_maximum_date() {
1659        let d = date(9999, 12, 1);
1660
1661        assert_eq!(
1662            d.nth_weekday_of_month(1, Weekday::Friday),
1663            Ok(date(9999, 12, 3))
1664        );
1665        assert_eq!(
1666            d.nth_weekday_of_month(2, Weekday::Friday),
1667            Ok(date(9999, 12, 10))
1668        );
1669        assert_eq!(
1670            d.nth_weekday_of_month(3, Weekday::Friday),
1671            Ok(date(9999, 12, 17))
1672        );
1673        assert_eq!(
1674            d.nth_weekday_of_month(4, Weekday::Friday),
1675            Ok(date(9999, 12, 24))
1676        );
1677        assert_eq!(
1678            d.nth_weekday_of_month(5, Weekday::Friday),
1679            Ok(date(9999, 12, 31))
1680        );
1681
1682        assert_eq!(
1683            d.nth_weekday_of_month(-5, Weekday::Friday),
1684            Ok(date(9999, 12, 3))
1685        );
1686        assert_eq!(
1687            d.nth_weekday_of_month(-4, Weekday::Friday),
1688            Ok(date(9999, 12, 10))
1689        );
1690        assert_eq!(
1691            d.nth_weekday_of_month(-3, Weekday::Friday),
1692            Ok(date(9999, 12, 17))
1693        );
1694        assert_eq!(
1695            d.nth_weekday_of_month(-2, Weekday::Friday),
1696            Ok(date(9999, 12, 24))
1697        );
1698        assert_eq!(
1699            d.nth_weekday_of_month(-1, Weekday::Friday),
1700            Ok(date(9999, 12, 31))
1701        );
1702    }
1703
1704    #[test]
1705    fn nth_weekday_of_month_every_month_has_four_weekdays() {
1706        for year in b::Year::MIN..=b::Year::MAX {
1707            for month in b::Month::MIN..=b::Month::MAX {
1708                let d = date(year, month, 1);
1709                for weekday in Weekday::Sunday.cycle_forward().take(7) {
1710                    for nth in [-4, -3, -2, -1, 1, 2, 3, 4] {
1711                        assert!(d.nth_weekday_of_month(nth, weekday).is_ok());
1712                        // never valid
1713                        assert!(d.nth_weekday_of_month(0, weekday).is_err());
1714                        assert!(d.nth_weekday_of_month(6, weekday).is_err());
1715                        assert!(d.nth_weekday_of_month(-6, weekday).is_err());
1716                    }
1717                }
1718            }
1719        }
1720    }
1721
1722    #[test]
1723    fn nth_weekday_various() {
1724        let d = date(2024, 3, 10);
1725
1726        assert_eq!(d.nth_weekday(1, Weekday::Monday), Ok(date(2024, 3, 11)));
1727        assert_eq!(d.nth_weekday(1, Weekday::Sunday), Ok(date(2024, 3, 17)));
1728        assert_eq!(d.nth_weekday(2, Weekday::Thursday), Ok(date(2024, 3, 21)));
1729
1730        assert_eq!(d.nth_weekday(-1, Weekday::Monday), Ok(date(2024, 3, 4)));
1731        assert_eq!(d.nth_weekday(-1, Weekday::Sunday), Ok(date(2024, 3, 3)));
1732        assert_eq!(
1733            d.nth_weekday(-2, Weekday::Thursday),
1734            Ok(date(2024, 2, 29))
1735        );
1736
1737        let d = date(9999, 12, 24);
1738        assert_eq!(d.nth_weekday(1, Weekday::Friday), Ok(date(9999, 12, 31)));
1739        let d = date(9999, 12, 30);
1740        assert_eq!(d.nth_weekday(1, Weekday::Friday), Ok(date(9999, 12, 31)));
1741        let d = date(9999, 12, 31);
1742        assert_eq!(d.nth_weekday(-1, Weekday::Friday), Ok(date(9999, 12, 24)));
1743        assert!(d.nth_weekday(1, Weekday::Friday).is_err());
1744
1745        let d = date(-9999, 1, 8);
1746        assert_eq!(d.nth_weekday(-1, Weekday::Monday), Ok(date(-9999, 1, 1)));
1747        let d = date(-9999, 1, 2);
1748        assert_eq!(d.nth_weekday(-1, Weekday::Monday), Ok(date(-9999, 1, 1)));
1749        let d = date(-9999, 1, 1);
1750        assert_eq!(d.nth_weekday(1, Weekday::Monday), Ok(date(-9999, 1, 8)));
1751        assert!(d.nth_weekday(-1, Weekday::Monday).is_err());
1752    }
1753
1754    #[test]
1755    fn nth_weekday_errors() {
1756        let d = date(2024, 3, 10);
1757        assert!(d.nth_weekday(0, Weekday::Monday).is_err());
1758    }
1759
1760    #[test]
1761    fn nth_weekday_extreme() {
1762        let weeks = 1_043_497;
1763
1764        let d1 = date(-9999, 1, 1);
1765        let d2 = d1.nth_weekday(weeks, Weekday::Monday).unwrap();
1766        assert_eq!(d2, date(9999, 12, 27));
1767        assert!(d1.nth_weekday(weeks + 1, Weekday::Monday).is_err());
1768        assert!(d1.nth_weekday(i32::MIN, Weekday::Monday).is_err());
1769        assert!(d1.nth_weekday(i32::MAX, Weekday::Monday).is_err());
1770
1771        let d1 = date(9999, 12, 31);
1772        let d2 = d1.nth_weekday(-weeks, Weekday::Friday).unwrap();
1773        assert_eq!(d2, date(-9999, 1, 5));
1774        assert!(d1.nth_weekday(weeks - 1, Weekday::Friday).is_err());
1775        assert!(d1.nth_weekday(i32::MIN, Weekday::Friday).is_err());
1776        assert!(d1.nth_weekday(i32::MAX, Weekday::Friday).is_err());
1777    }
1778
1779    #[test]
1780    fn yesterday() {
1781        assert_eq!(date(2024, 7, 3).yesterday(), Ok(date(2024, 7, 2)));
1782        assert_eq!(date(2024, 7, 1).yesterday(), Ok(date(2024, 6, 30)));
1783        assert_eq!(date(2024, 6, 1).yesterday(), Ok(date(2024, 5, 31)));
1784        assert_eq!(date(2024, 3, 1).yesterday(), Ok(date(2024, 2, 29)));
1785        assert_eq!(date(2023, 3, 1).yesterday(), Ok(date(2023, 2, 28)));
1786        assert_eq!(date(2023, 1, 1).yesterday(), Ok(date(2022, 12, 31)));
1787        assert_eq!(date(-9999, 1, 2).yesterday(), Ok(date(-9999, 1, 1)));
1788        assert_eq!(date(9999, 12, 31).yesterday(), Ok(date(9999, 12, 30)));
1789
1790        assert!(date(-9999, 1, 1).yesterday().is_err());
1791    }
1792
1793    #[test]
1794    fn tomorrow() {
1795        assert_eq!(date(2024, 7, 3).tomorrow(), Ok(date(2024, 7, 4)));
1796        assert_eq!(date(2024, 6, 30).tomorrow(), Ok(date(2024, 7, 1)));
1797        assert_eq!(date(2024, 5, 30).tomorrow(), Ok(date(2024, 5, 31)));
1798        assert_eq!(date(2024, 5, 31).tomorrow(), Ok(date(2024, 6, 1)));
1799        assert_eq!(date(2024, 2, 28).tomorrow(), Ok(date(2024, 2, 29)));
1800        assert_eq!(date(2024, 2, 29).tomorrow(), Ok(date(2024, 3, 1)));
1801        assert_eq!(date(2023, 2, 28).tomorrow(), Ok(date(2023, 3, 1)));
1802        assert_eq!(date(2023, 12, 31).tomorrow(), Ok(date(2024, 1, 1)));
1803        assert_eq!(date(-9999, 1, 1).tomorrow(), Ok(date(-9999, 1, 2)));
1804        assert_eq!(date(9999, 12, 30).tomorrow(), Ok(date(9999, 12, 31)));
1805
1806        assert!(date(9999, 12, 31).tomorrow().is_err());
1807    }
1808
1809    #[test]
1810    fn iso_week_date_tomorrow() {
1811        assert_eq!(
1812            week_date(2024, 27, Weekday::Wednesday).tomorrow(),
1813            Ok(week_date(2024, 27, Weekday::Thursday)),
1814        );
1815        assert_eq!(
1816            week_date(2024, 27, Weekday::Sunday).tomorrow(),
1817            Ok(week_date(2024, 28, Weekday::Monday)),
1818        );
1819        assert_eq!(
1820            week_date(2024, 52, Weekday::Sunday).tomorrow(),
1821            Ok(week_date(2025, 1, Weekday::Monday)),
1822        );
1823        assert_eq!(
1824            week_date(2025, 1, Weekday::Monday).tomorrow(),
1825            Ok(week_date(2025, 1, Weekday::Tuesday)),
1826        );
1827        assert_eq!(
1828            week_date(2025, 1, Weekday::Tuesday).tomorrow(),
1829            Ok(week_date(2025, 1, Weekday::Wednesday)),
1830        );
1831        assert_eq!(
1832            week_date(2026, 52, Weekday::Sunday).tomorrow(),
1833            Ok(week_date(2026, 53, Weekday::Monday)),
1834        );
1835        assert_eq!(
1836            week_date(2026, 53, Weekday::Sunday).tomorrow(),
1837            Ok(week_date(2027, 1, Weekday::Monday)),
1838        );
1839        assert_eq!(
1840            week_date(-9999, 1, Weekday::Monday).tomorrow(),
1841            Ok(week_date(-9999, 1, Weekday::Tuesday)),
1842        );
1843        assert_eq!(
1844            week_date(9999, 52, Weekday::Thursday).tomorrow(),
1845            Ok(week_date(9999, 52, Weekday::Friday)),
1846        );
1847
1848        assert!(week_date(9999, 52, Weekday::Friday).tomorrow().is_err());
1849    }
1850
1851    #[test]
1852    fn iso_week_date_yesterday() {
1853        assert_eq!(
1854            week_date(2024, 27, Weekday::Thursday).yesterday(),
1855            Ok(week_date(2024, 27, Weekday::Wednesday)),
1856        );
1857        assert_eq!(
1858            week_date(2024, 28, Weekday::Monday).yesterday(),
1859            Ok(week_date(2024, 27, Weekday::Sunday)),
1860        );
1861        assert_eq!(
1862            week_date(2025, 1, Weekday::Monday).yesterday(),
1863            Ok(week_date(2024, 52, Weekday::Sunday)),
1864        );
1865        assert_eq!(
1866            week_date(2025, 1, Weekday::Tuesday).yesterday(),
1867            Ok(week_date(2025, 1, Weekday::Monday)),
1868        );
1869        assert_eq!(
1870            week_date(2025, 1, Weekday::Wednesday).yesterday(),
1871            Ok(week_date(2025, 1, Weekday::Tuesday)),
1872        );
1873        assert_eq!(
1874            week_date(2026, 53, Weekday::Monday).yesterday(),
1875            Ok(week_date(2026, 52, Weekday::Sunday)),
1876        );
1877        assert_eq!(
1878            week_date(2027, 1, Weekday::Monday).yesterday(),
1879            Ok(week_date(2026, 53, Weekday::Sunday)),
1880        );
1881        assert_eq!(
1882            week_date(9999, 12, Weekday::Friday).yesterday(),
1883            Ok(week_date(9999, 12, Weekday::Thursday)),
1884        );
1885        assert_eq!(
1886            week_date(-9999, 1, Weekday::Tuesday).yesterday(),
1887            Ok(week_date(-9999, 1, Weekday::Monday)),
1888        );
1889
1890        assert!(week_date(-9999, 1, Weekday::Monday).yesterday().is_err());
1891    }
1892
1893    #[test]
1894    fn add() {
1895        assert_eq!(date(2024, 7, 3).checked_add(-1), Ok(date(2024, 7, 2)));
1896        assert_eq!(date(2024, 7, 1).checked_add(-1), Ok(date(2024, 6, 30)));
1897        assert_eq!(date(2024, 6, 1).checked_add(-1), Ok(date(2024, 5, 31)));
1898        assert_eq!(date(2024, 3, 1).checked_add(-1), Ok(date(2024, 2, 29)));
1899        assert_eq!(date(2023, 3, 1).checked_add(-1), Ok(date(2023, 2, 28)));
1900        assert_eq!(date(2023, 1, 1).checked_add(-1), Ok(date(2022, 12, 31)));
1901        assert_eq!(date(-9999, 1, 2).checked_add(-1), Ok(date(-9999, 1, 1)));
1902        assert_eq!(date(9999, 12, 31).checked_add(-1), Ok(date(9999, 12, 30)));
1903
1904        assert_eq!(date(2024, 7, 3).checked_add(1), Ok(date(2024, 7, 4)));
1905        assert_eq!(date(2024, 6, 30).checked_add(1), Ok(date(2024, 7, 1)));
1906        assert_eq!(date(2024, 5, 30).checked_add(1), Ok(date(2024, 5, 31)));
1907        assert_eq!(date(2024, 5, 31).checked_add(1), Ok(date(2024, 6, 1)));
1908        assert_eq!(date(2024, 2, 28).checked_add(1), Ok(date(2024, 2, 29)));
1909        assert_eq!(date(2024, 2, 29).checked_add(1), Ok(date(2024, 3, 1)));
1910        assert_eq!(date(2023, 2, 28).checked_add(1), Ok(date(2023, 3, 1)));
1911        assert_eq!(date(2023, 12, 31).checked_add(1), Ok(date(2024, 1, 1)));
1912        assert_eq!(date(-9999, 1, 1).checked_add(1), Ok(date(-9999, 1, 2)));
1913        assert_eq!(date(9999, 12, 30).checked_add(1), Ok(date(9999, 12, 31)));
1914
1915        assert_eq!(date(2024, 7, 3).checked_add(2), Ok(date(2024, 7, 5)));
1916        assert_eq!(date(2024, 6, 29).checked_add(2), Ok(date(2024, 7, 1)));
1917        assert_eq!(date(2024, 5, 29).checked_add(2), Ok(date(2024, 5, 31)));
1918        assert_eq!(date(2024, 5, 30).checked_add(2), Ok(date(2024, 6, 1)));
1919        assert_eq!(date(2024, 2, 27).checked_add(2), Ok(date(2024, 2, 29)));
1920        assert_eq!(date(2024, 2, 28).checked_add(2), Ok(date(2024, 3, 1)));
1921        assert_eq!(date(2023, 2, 27).checked_add(2), Ok(date(2023, 3, 1)));
1922        assert_eq!(date(2023, 12, 30).checked_add(2), Ok(date(2024, 1, 1)));
1923        assert_eq!(date(-9999, 1, 1).checked_add(2), Ok(date(-9999, 1, 3)));
1924        assert_eq!(date(9999, 12, 29).checked_add(2), Ok(date(9999, 12, 31)));
1925
1926        let max_days = (b::UnixEpochDays::LEN - 1) as i32;
1927
1928        assert_eq!(
1929            date(-9999, 1, 1).checked_add(max_days),
1930            Ok(date(9999, 12, 31))
1931        );
1932        assert_eq!(
1933            date(9999, 12, 31).checked_add(-max_days),
1934            Ok(date(-9999, 1, 1))
1935        );
1936
1937        assert!(date(-9999, 1, 1).checked_add(-1).is_err());
1938        assert!(date(9999, 12, 31).checked_add(1).is_err());
1939        assert!(date(-9999, 1, 1).checked_add(max_days + 1).is_err());
1940        assert!(date(9999, 12, 31).checked_add(-(max_days + 1)).is_err());
1941    }
1942
1943    #[test]
1944    fn sub() {
1945        assert_eq!(date(-9999, 1, 1).checked_sub(-1), Ok(date(-9999, 1, 2)));
1946        assert_eq!(date(-9999, 1, 1).checked_sub(-2), Ok(date(-9999, 1, 3)));
1947
1948        assert!(date(-9999, 1, 1).checked_sub(1).is_err());
1949        assert!(date(-9999, 1, 1).checked_sub(i32::MIN).is_err());
1950        assert!(date(9999, 12, 31).checked_sub(i32::MAX).is_err());
1951    }
1952
1953    #[test]
1954    fn prev_year() {
1955        assert_eq!(date(2024, 2, 29).prev_year(), Ok(2023));
1956        assert_eq!(date(2024, 1, 1).prev_year(), Ok(2023));
1957        assert_eq!(date(2023, 12, 31).prev_year(), Ok(2022));
1958        assert_eq!(date(9999, 12, 31).prev_year(), Ok(9998));
1959
1960        assert!(date(-9999, 12, 31).prev_year().is_err());
1961        assert!(date(-9999, 1, 1).prev_year().is_err());
1962    }
1963
1964    #[test]
1965    fn next_year() {
1966        assert_eq!(date(2024, 2, 29).next_year(), Ok(2025));
1967        assert_eq!(date(2024, 1, 1).next_year(), Ok(2025));
1968        assert_eq!(date(2023, 12, 31).next_year(), Ok(2024));
1969        assert_eq!(date(-9999, 12, 31).next_year(), Ok(-9998));
1970
1971        assert!(date(9999, 12, 31).next_year().is_err());
1972        assert!(date(9999, 1, 1).next_year().is_err());
1973    }
1974
1975    #[test]
1976    fn to_iso_week_date_various() {
1977        assert_eq!(
1978            date(1995, 1, 1).to_iso_week_date(),
1979            week_date(1994, 52, Weekday::Sunday),
1980        );
1981        assert_eq!(
1982            date(1996, 12, 31).to_iso_week_date(),
1983            week_date(1997, 1, Weekday::Tuesday),
1984        );
1985        assert_eq!(
1986            date(2019, 12, 30).to_iso_week_date(),
1987            week_date(2020, 1, Weekday::Monday),
1988        );
1989        assert_eq!(
1990            date(2031, 12, 29).to_iso_week_date(),
1991            week_date(2032, 1, Weekday::Monday),
1992        );
1993        assert_eq!(
1994            date(2024, 3, 9).to_iso_week_date(),
1995            week_date(2024, 10, Weekday::Saturday),
1996        );
1997        assert_eq!(
1998            Date::MIN.to_iso_week_date(),
1999            week_date(-9999, 1, Weekday::Monday),
2000        );
2001        assert_eq!(
2002            Date::MAX.to_iso_week_date(),
2003            week_date(9999, 52, Weekday::Friday),
2004        );
2005    }
2006
2007    quickcheck::quickcheck! {
2008        fn prop_tomorrow_yesterday_is_identity(d: Date) -> quickcheck::TestResult {
2009            let Ok(yesterday) = d.yesterday() else {
2010                return quickcheck::TestResult::discard()
2011            };
2012            quickcheck::TestResult::from_bool(yesterday.tomorrow() == Ok(d))
2013        }
2014
2015        fn prop_yesterday_tomorrow_is_identity(d: Date) -> quickcheck::TestResult {
2016            let Ok(tomorrow) = d.tomorrow() else {
2017                return quickcheck::TestResult::discard()
2018            };
2019            quickcheck::TestResult::from_bool(tomorrow.yesterday() == Ok(d))
2020        }
2021
2022        fn prop_add_equals_sub(d1: Date, days: i32) -> quickcheck::TestResult {
2023            let Ok(d2) = d1.checked_add(days) else {
2024                return quickcheck::TestResult::discard();
2025            };
2026            quickcheck::TestResult::from_bool(d2.checked_sub(days) == Ok(d1))
2027        }
2028
2029        fn prop_all_long_years_have_53rd_week(year: i16) -> quickcheck::TestResult {
2030            if b::ISOYear::check(year).is_err() {
2031                return quickcheck::TestResult::discard();
2032            }
2033            quickcheck::TestResult::from_bool(
2034                !civil::is_long_iso_week_year(year)
2035                 || ISOWeekDate::new(year, 53, Weekday::Sunday).is_ok()
2036            )
2037        }
2038
2039        fn prop_prev_day_is_less(wd: ISOWeekDate) -> quickcheck::TestResult {
2040            let Ok(prev_date) = wd.to_date().yesterday() else {
2041                return quickcheck::TestResult::discard();
2042            };
2043            quickcheck::TestResult::from_bool(
2044                prev_date.to_iso_week_date() < wd,
2045            )
2046        }
2047
2048        fn prop_next_day_is_greater(wd: ISOWeekDate) -> quickcheck::TestResult {
2049            let Ok(next_date) = wd.to_date().tomorrow() else {
2050                return quickcheck::TestResult::discard();
2051            };
2052            quickcheck::TestResult::from_bool(
2053                wd < next_date.to_iso_week_date(),
2054            )
2055        }
2056
2057        fn prop_iso_tomorrow_yesterday_is_identity(
2058            wd: ISOWeekDate
2059        ) -> quickcheck::TestResult {
2060            let Ok(yesterday) = wd.yesterday() else {
2061                return quickcheck::TestResult::discard()
2062            };
2063            quickcheck::TestResult::from_bool(yesterday.tomorrow() == Ok(wd))
2064        }
2065
2066        fn prop_iso_yesterday_tomorrow_is_identity(
2067            wd: ISOWeekDate
2068        ) -> quickcheck::TestResult {
2069            let Ok(tomorrow) = wd.tomorrow() else {
2070                return quickcheck::TestResult::discard()
2071            };
2072            quickcheck::TestResult::from_bool(tomorrow.yesterday() == Ok(wd))
2073        }
2074    }
2075}