Skip to main content

jiff_core/civil/
weekday.rs

1use crate::{
2    bounds::{self as b, RangeError},
3    macros::{rbail, unwrapr},
4};
5
6/// A representation for the day of the week.
7#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
8#[cfg_attr(feature = "defmt", derive(defmt::Format))]
9#[repr(u8)]
10#[allow(missing_docs)]
11pub enum Weekday {
12    Monday = 1,
13    Tuesday = 2,
14    Wednesday = 3,
15    Thursday = 4,
16    Friday = 5,
17    Saturday = 6,
18    Sunday = 7,
19}
20
21impl Weekday {
22    /// Convert a 0-offset to a `Weekday`. Monday corresponds to offset `0` and
23    /// Sunday corresponds to offset `6`.
24    #[inline]
25    pub const fn from_monday_zero_offset(
26        offset: i8,
27    ) -> Result<Weekday, RangeError> {
28        Ok(match offset {
29            0 => Weekday::Monday,
30            1 => Weekday::Tuesday,
31            2 => Weekday::Wednesday,
32            3 => Weekday::Thursday,
33            4 => Weekday::Friday,
34            5 => Weekday::Saturday,
35            6 => Weekday::Sunday,
36            _ => rbail!(b::WeekdayMondayZero::error()),
37        })
38    }
39
40    /// Convert a 1-offset to a `Weekday`. Monday corresponds to offset `1` and
41    /// Sunday corresponds to offset `7`.
42    #[inline]
43    pub const fn from_monday_one_offset(
44        offset: i8,
45    ) -> Result<Weekday, RangeError> {
46        Ok(match offset {
47            1 => Weekday::Monday,
48            2 => Weekday::Tuesday,
49            3 => Weekday::Wednesday,
50            4 => Weekday::Thursday,
51            5 => Weekday::Friday,
52            6 => Weekday::Saturday,
53            7 => Weekday::Sunday,
54            _ => rbail!(b::WeekdayMondayOne::error()),
55        })
56    }
57
58    /// Convert a 0-offset to a `Weekday`. Sunday corresponds to offset `0` and
59    /// Saturday corresponds to offset `6`.
60    #[inline]
61    pub const fn from_sunday_zero_offset(
62        offset: i8,
63    ) -> Result<Weekday, RangeError> {
64        Ok(match offset {
65            0 => Weekday::Sunday,
66            1 => Weekday::Monday,
67            2 => Weekday::Tuesday,
68            3 => Weekday::Wednesday,
69            4 => Weekday::Thursday,
70            5 => Weekday::Friday,
71            6 => Weekday::Saturday,
72            _ => rbail!(b::WeekdaySundayZero::error()),
73        })
74    }
75
76    /// Convert a 1-offset to a `Weekday`. Sunday corresponds to offset `1` and
77    /// Saturday corresponds to offset `7`.
78    #[inline]
79    pub const fn from_sunday_one_offset(
80        offset: i8,
81    ) -> Result<Weekday, RangeError> {
82        Ok(match offset {
83            1 => Weekday::Sunday,
84            2 => Weekday::Monday,
85            3 => Weekday::Tuesday,
86            4 => Weekday::Wednesday,
87            5 => Weekday::Thursday,
88            6 => Weekday::Friday,
89            7 => Weekday::Saturday,
90            _ => rbail!(b::WeekdaySundayOne::error()),
91        })
92    }
93
94    /// Returns the weekday as a 0-offset. Monday corresponds to offset `0`
95    /// and Sunday corresponds to offset `6`.
96    #[inline]
97    pub const fn to_monday_zero_offset(self) -> i8 {
98        self.to_monday_one_offset() - 1
99    }
100
101    /// Returns the weekday as a 1-offset. Monday corresponds to offset `1`
102    /// and Sunday corresponds to offset `7`.
103    #[inline]
104    pub const fn to_monday_one_offset(self) -> i8 {
105        self as i8
106    }
107
108    /// Returns the weekday as a 0-offset. Sunday corresponds to offset `0`
109    /// and Saturday corresponds to offset `6`.
110    #[inline]
111    pub const fn to_sunday_zero_offset(self) -> i8 {
112        let offset = self.to_monday_one_offset();
113        if offset == 7 {
114            0
115        } else {
116            offset
117        }
118    }
119
120    /// Returns the weekday as a 1-offset. Sunday corresponds to offset `1`
121    /// and Saturday corresponds to offset `7`.
122    #[inline]
123    pub const fn to_sunday_one_offset(self) -> i8 {
124        self.to_sunday_zero_offset() + 1
125    }
126
127    /// Add the given number of days to this weekday, using wrapping arithmetic,
128    /// and return the resulting weekday.
129    ///
130    /// Adding a multiple of `7` (including `0`) is guaranteed to produce the
131    /// same weekday as this one.
132    #[inline]
133    pub const fn wrapping_add(self, days: i64) -> Weekday {
134        let start = self.to_monday_zero_offset() as i64;
135        // We are careful to `rem_euclid` on `rhs` before doing
136        // wrapping arithmetic, otherwise the result is not
137        // correct. Namely, it would assume that, e.g., since
138        // `i64::MAX.rem_euclid(7)` is 0, then the next value would be
139        // `rem_euclid(7) == 1`. But `i64::MIN.rem_euclid(7)` is 6.
140        let end = (start.wrapping_add(days.rem_euclid(7)) % 7) as i8;
141        // Always valid because of the mod 7 above.
142        unwrapr!(
143            Weekday::from_monday_zero_offset(end),
144            "weekday is always 0..=6",
145        )
146    }
147
148    /// Subtract the given number of days from this weekday, using wrapping
149    /// arithmetic, and return the resulting weekday.
150    ///
151    /// Subtracting a multiple of `7` (including `0`) is guaranteed to produce
152    /// the same weekday as this one.
153    #[inline]
154    pub const fn wrapping_sub(self, days: i64) -> Weekday {
155        // i64::MIN.rem_euclid(7) == 6
156        let days = match days.checked_neg() {
157            Some(days) => days,
158            None => -6,
159        };
160        self.wrapping_add(days)
161    }
162
163    /// Returns the next weekday, wrapping around at the end of week to the
164    /// beginning of the week.
165    ///
166    /// This is a convenience routing for calling [`Weekday::wrapping_add`]
167    /// with a value of `1`.
168    #[inline]
169    pub const fn next(self) -> Weekday {
170        self.wrapping_add(1)
171    }
172
173    /// Returns the previous weekday, wrapping around at the beginning of week
174    /// to the end of the week.
175    ///
176    /// This is a convenience routing for calling [`Weekday::wrapping_sub`]
177    /// with a value of `1`.
178    #[inline]
179    pub const fn previous(self) -> Weekday {
180        self.wrapping_sub(1)
181    }
182
183    /// Returns the number of days from `other` to this weekday.
184    ///
185    /// Adding the returned number of days to `other` is guaranteed to sum to
186    /// this weekday. The number of days returned is guaranteed to be in the
187    /// range `0..=6`.
188    #[inline]
189    pub const fn since(self, other: Weekday) -> i8 {
190        (self.to_monday_zero_offset() - other.to_monday_zero_offset())
191            .rem_euclid(7)
192    }
193
194    /// Returns the number of days until `other` from this weekday.
195    ///
196    /// Adding the returned number of days to this weekday is guaranteed to sum
197    /// to `other` weekday. The number of days returned is guaranteed to be in
198    /// the range `0..=6`.
199    #[inline]
200    pub const fn until(self, other: Weekday) -> i8 {
201        other.since(self)
202    }
203
204    /// Starting with this weekday, this returns an unending iterator that
205    /// cycles forward through the days of the week.
206    #[inline]
207    pub const fn cycle_forward(self) -> WeekdaysForward {
208        WeekdaysForward { next: self }
209    }
210
211    /// Starting with this weekday, this returns an unending iterator that
212    /// cycles backward through the days of the week.
213    #[inline]
214    pub const fn cycle_reverse(self) -> WeekdaysReverse {
215        WeekdaysReverse { next: self }
216    }
217}
218
219/// An unending iterator of the days of the week.
220///
221/// This iterator is created by calling [`Weekday::cycle_forward`].
222#[derive(Clone, Debug)]
223pub struct WeekdaysForward {
224    next: Weekday,
225}
226
227impl Iterator for WeekdaysForward {
228    type Item = Weekday;
229
230    #[inline]
231    fn next(&mut self) -> Option<Weekday> {
232        let next = self.next;
233        self.next = self.next.wrapping_add(1);
234        Some(next)
235    }
236}
237
238impl core::iter::FusedIterator for WeekdaysForward {}
239
240/// An unending iterator of the days of the week in reverse.
241///
242/// This iterator is created by calling [`Weekday::cycle_reverse`].
243#[derive(Clone, Debug)]
244pub struct WeekdaysReverse {
245    next: Weekday,
246}
247
248impl Iterator for WeekdaysReverse {
249    type Item = Weekday;
250
251    #[inline]
252    fn next(&mut self) -> Option<Weekday> {
253        let next = self.next;
254        self.next = self.next.wrapping_sub(1);
255        Some(next)
256    }
257}
258
259impl core::iter::FusedIterator for WeekdaysReverse {}
260
261#[cfg(test)]
262impl quickcheck::Arbitrary for Weekday {
263    fn arbitrary(g: &mut quickcheck::Gen) -> Weekday {
264        let offset = b::WeekdayMondayZero::arbitrary(g);
265        Weekday::from_monday_zero_offset(offset).unwrap()
266    }
267
268    fn shrink(&self) -> alloc::boxed::Box<dyn Iterator<Item = Weekday>> {
269        alloc::boxed::Box::new(
270            self.to_monday_zero_offset()
271                .shrink()
272                .filter_map(|n| Weekday::from_monday_zero_offset(n).ok()),
273        )
274    }
275}
276
277#[cfg(test)]
278mod tests {
279    use alloc::vec::Vec;
280
281    use super::*;
282
283    static WEEKDAYS: &[Weekday] = &[
284        Weekday::Monday,
285        Weekday::Tuesday,
286        Weekday::Wednesday,
287        Weekday::Thursday,
288        Weekday::Friday,
289        Weekday::Saturday,
290        Weekday::Sunday,
291    ];
292
293    #[test]
294    fn weekday_from_monday_zero() {
295        use self::Weekday::*;
296
297        assert_eq!(Weekday::from_monday_zero_offset(0), Ok(Monday));
298        assert_eq!(Weekday::from_monday_zero_offset(1), Ok(Tuesday));
299        assert_eq!(Weekday::from_monday_zero_offset(2), Ok(Wednesday));
300        assert_eq!(Weekday::from_monday_zero_offset(3), Ok(Thursday));
301        assert_eq!(Weekday::from_monday_zero_offset(4), Ok(Friday));
302        assert_eq!(Weekday::from_monday_zero_offset(5), Ok(Saturday));
303        assert_eq!(Weekday::from_monday_zero_offset(6), Ok(Sunday));
304    }
305
306    #[test]
307    fn weekday_from_monday_one() {
308        use self::Weekday::*;
309
310        assert_eq!(Weekday::from_monday_one_offset(1), Ok(Monday));
311        assert_eq!(Weekday::from_monday_one_offset(2), Ok(Tuesday));
312        assert_eq!(Weekday::from_monday_one_offset(3), Ok(Wednesday));
313        assert_eq!(Weekday::from_monday_one_offset(4), Ok(Thursday));
314        assert_eq!(Weekday::from_monday_one_offset(5), Ok(Friday));
315        assert_eq!(Weekday::from_monday_one_offset(6), Ok(Saturday));
316        assert_eq!(Weekday::from_monday_one_offset(7), Ok(Sunday));
317    }
318
319    #[test]
320    fn weekday_from_sunday_zero() {
321        use self::Weekday::*;
322
323        assert_eq!(Weekday::from_sunday_zero_offset(1), Ok(Monday));
324        assert_eq!(Weekday::from_sunday_zero_offset(2), Ok(Tuesday));
325        assert_eq!(Weekday::from_sunday_zero_offset(3), Ok(Wednesday));
326        assert_eq!(Weekday::from_sunday_zero_offset(4), Ok(Thursday));
327        assert_eq!(Weekday::from_sunday_zero_offset(5), Ok(Friday));
328        assert_eq!(Weekday::from_sunday_zero_offset(6), Ok(Saturday));
329        assert_eq!(Weekday::from_sunday_zero_offset(0), Ok(Sunday));
330    }
331
332    #[test]
333    fn weekday_from_sunday_one() {
334        use self::Weekday::*;
335
336        assert_eq!(Weekday::from_sunday_one_offset(2), Ok(Monday));
337        assert_eq!(Weekday::from_sunday_one_offset(3), Ok(Tuesday));
338        assert_eq!(Weekday::from_sunday_one_offset(4), Ok(Wednesday));
339        assert_eq!(Weekday::from_sunday_one_offset(5), Ok(Thursday));
340        assert_eq!(Weekday::from_sunday_one_offset(6), Ok(Friday));
341        assert_eq!(Weekday::from_sunday_one_offset(7), Ok(Saturday));
342        assert_eq!(Weekday::from_sunday_one_offset(1), Ok(Sunday));
343    }
344
345    #[test]
346    fn weekday_to_monday_zero() {
347        for &weekday in WEEKDAYS {
348            assert_eq!(
349                Weekday::from_monday_zero_offset(
350                    weekday.to_monday_zero_offset()
351                ),
352                Ok(weekday)
353            );
354        }
355    }
356
357    #[test]
358    fn weekday_to_monday_one() {
359        for &weekday in WEEKDAYS {
360            assert_eq!(
361                Weekday::from_monday_one_offset(
362                    weekday.to_monday_one_offset()
363                ),
364                Ok(weekday)
365            );
366        }
367    }
368
369    #[test]
370    fn weekday_to_sunday_zero() {
371        for &weekday in WEEKDAYS {
372            assert_eq!(
373                Weekday::from_sunday_zero_offset(
374                    weekday.to_sunday_zero_offset()
375                ),
376                Ok(weekday)
377            );
378        }
379    }
380
381    #[test]
382    fn weekday_to_sunday_one() {
383        for &weekday in WEEKDAYS {
384            assert_eq!(
385                Weekday::from_sunday_one_offset(
386                    weekday.to_sunday_one_offset()
387                ),
388                Ok(weekday)
389            );
390        }
391    }
392
393    #[test]
394    fn weekday_wrapping_add() {
395        use self::Weekday::*;
396
397        assert_eq!(Sunday.wrapping_add(0), Sunday);
398        assert_eq!(Sunday.wrapping_add(1), Monday);
399        assert_eq!(Sunday.wrapping_add(2), Tuesday);
400        assert_eq!(Sunday.wrapping_add(3), Wednesday);
401        assert_eq!(Sunday.wrapping_add(4), Thursday);
402        assert_eq!(Sunday.wrapping_add(5), Friday);
403        assert_eq!(Sunday.wrapping_add(6), Saturday);
404        assert_eq!(Sunday.wrapping_add(7), Sunday);
405
406        assert_eq!(Wednesday.wrapping_add(0), Wednesday);
407        assert_eq!(Wednesday.wrapping_add(1), Thursday);
408        assert_eq!(Wednesday.wrapping_add(2), Friday);
409        assert_eq!(Wednesday.wrapping_add(3), Saturday);
410        assert_eq!(Wednesday.wrapping_add(4), Sunday);
411        assert_eq!(Wednesday.wrapping_add(5), Monday);
412        assert_eq!(Wednesday.wrapping_add(6), Tuesday);
413        assert_eq!(Wednesday.wrapping_add(7), Wednesday);
414
415        assert_eq!(Sunday.wrapping_add(-1), Saturday);
416        assert_eq!(Sunday.wrapping_add(-2), Friday);
417        assert_eq!(Sunday.wrapping_add(-3), Thursday);
418        assert_eq!(Sunday.wrapping_add(-4), Wednesday);
419        assert_eq!(Sunday.wrapping_add(-5), Tuesday);
420        assert_eq!(Sunday.wrapping_add(-6), Monday);
421        assert_eq!(Sunday.wrapping_add(-7), Sunday);
422
423        assert_eq!(Wednesday.wrapping_add(-1), Tuesday);
424        assert_eq!(Wednesday.wrapping_add(-2), Monday);
425        assert_eq!(Wednesday.wrapping_add(-3), Sunday);
426        assert_eq!(Wednesday.wrapping_add(-4), Saturday);
427        assert_eq!(Wednesday.wrapping_add(-5), Friday);
428        assert_eq!(Wednesday.wrapping_add(-6), Thursday);
429        assert_eq!(Wednesday.wrapping_add(-7), Wednesday);
430
431        // This caught a bug where our wrapping arithmetic in
432        // `Weekday::wrapping_add` was incorrect when overflow occurred.
433        assert_eq!(Tuesday.wrapping_add(9223372036854775807i64), Tuesday);
434    }
435
436    #[test]
437    fn weekday_wrapping_sub() {
438        use self::Weekday::*;
439
440        assert_eq!(Sunday.wrapping_sub(0), Sunday);
441        assert_eq!(Sunday.wrapping_sub(1), Saturday);
442        assert_eq!(Sunday.wrapping_sub(2), Friday);
443        assert_eq!(Sunday.wrapping_sub(3), Thursday);
444        assert_eq!(Sunday.wrapping_sub(4), Wednesday);
445        assert_eq!(Sunday.wrapping_sub(5), Tuesday);
446        assert_eq!(Sunday.wrapping_sub(6), Monday);
447        assert_eq!(Sunday.wrapping_sub(7), Sunday);
448
449        assert_eq!(Wednesday.wrapping_sub(0), Wednesday);
450        assert_eq!(Wednesday.wrapping_sub(1), Tuesday);
451        assert_eq!(Wednesday.wrapping_sub(2), Monday);
452        assert_eq!(Wednesday.wrapping_sub(3), Sunday);
453        assert_eq!(Wednesday.wrapping_sub(4), Saturday);
454        assert_eq!(Wednesday.wrapping_sub(5), Friday);
455        assert_eq!(Wednesday.wrapping_sub(6), Thursday);
456        assert_eq!(Wednesday.wrapping_sub(7), Wednesday);
457
458        assert_eq!(Sunday.wrapping_sub(-1), Monday);
459        assert_eq!(Sunday.wrapping_sub(-2), Tuesday);
460        assert_eq!(Sunday.wrapping_sub(-3), Wednesday);
461        assert_eq!(Sunday.wrapping_sub(-4), Thursday);
462        assert_eq!(Sunday.wrapping_sub(-5), Friday);
463        assert_eq!(Sunday.wrapping_sub(-6), Saturday);
464        assert_eq!(Sunday.wrapping_sub(-7), Sunday);
465
466        assert_eq!(Wednesday.wrapping_sub(-1), Thursday);
467        assert_eq!(Wednesday.wrapping_sub(-2), Friday);
468        assert_eq!(Wednesday.wrapping_sub(-3), Saturday);
469        assert_eq!(Wednesday.wrapping_sub(-4), Sunday);
470        assert_eq!(Wednesday.wrapping_sub(-5), Monday);
471        assert_eq!(Wednesday.wrapping_sub(-6), Tuesday);
472        assert_eq!(Wednesday.wrapping_sub(-7), Wednesday);
473
474        // This found a bug where we were negating the integer
475        // given and assuming it wouldn't panic.
476        assert_eq!(Monday.wrapping_sub(-9223372036854775805i64), Saturday);
477        assert_eq!(Monday.wrapping_sub(-9223372036854775806i64), Sunday);
478        assert_eq!(Monday.wrapping_sub(-9223372036854775807i64), Monday);
479        assert_eq!(Monday.wrapping_sub(-9223372036854775808i64), Tuesday);
480    }
481
482    #[test]
483    fn weekday_since() {
484        for &wd1 in WEEKDAYS {
485            for (distance, wd2) in wd1.cycle_forward().enumerate().take(7) {
486                assert_eq!(
487                    usize::try_from(wd2.since(wd1)).unwrap(),
488                    distance,
489                    "{wd2:?} since {wd1:?} should be {distance}",
490                );
491            }
492        }
493    }
494
495    #[test]
496    fn weekday_until() {
497        for &wd1 in WEEKDAYS {
498            for (distance, wd2) in wd1.cycle_forward().enumerate().take(7) {
499                assert_eq!(
500                    usize::try_from(wd1.until(wd2)).unwrap(),
501                    distance,
502                    "{wd1:?} until {wd2:?} should be {distance}",
503                );
504            }
505        }
506    }
507
508    #[test]
509    fn weekday_cycle_forward() {
510        assert_eq!(
511            WEEKDAYS,
512            Weekday::Monday.cycle_forward().take(7).collect::<Vec<_>>(),
513        );
514    }
515
516    #[test]
517    fn weekday_cycle_reverse() {
518        let mut got =
519            Weekday::Sunday.cycle_reverse().take(7).collect::<Vec<_>>();
520        got.reverse();
521        assert_eq!(WEEKDAYS, got);
522    }
523
524    quickcheck::quickcheck! {
525        fn prop_weekday_add_sub(wd: Weekday, n: i64) -> bool {
526            wd.wrapping_add(n).wrapping_sub(n) == wd
527        }
528
529        fn prop_weekday_since_until(wd1: Weekday, wd2: Weekday) -> bool {
530            wd1.until(wd2) == wd2.since(wd1)
531        }
532
533        fn prop_since_add_equals_self(wd1: Weekday, wd2: Weekday) -> bool {
534            let days = wd1.since(wd2);
535            wd2.wrapping_add(days.into()) == wd1
536        }
537
538        fn prop_until_add_equals_other(wd1: Weekday, wd2: Weekday) -> bool {
539            let days = wd1.until(wd2);
540            wd1.wrapping_add(days.into()) == wd2
541        }
542    }
543}