diff --git a/tests/date.rs b/tests/date.rs index ed564fadc..88b155448 100644 --- a/tests/date.rs +++ b/tests/date.rs @@ -982,3 +982,77 @@ fn replace_day() { assert!(date!(2022 - 02 - 18).replace_day(0).is_err()); // 0 isn't a valid day assert!(date!(2022 - 02 - 18).replace_day(30).is_err()); // 30 isn't a valid day in February } + +#[test] +fn next_occurrence_test() { + assert_eq!(date!(2023 - 06 - 25).next_occurrence(Weekday::Monday), date!(2023 - 06 - 26)); + assert_eq!(date!(2023 - 06 - 26).next_occurrence(Weekday::Monday), date!(2023 - 07 - 03)); + assert_eq!(date!(2023 - 06 - 27).next_occurrence(Weekday::Monday), date!(2023 - 07 - 03)); + assert_eq!(date!(2023 - 06 - 28).next_occurrence(Weekday::Monday), date!(2023 - 07 - 03)); + assert_eq!(date!(2023 - 06 - 29).next_occurrence(Weekday::Monday), date!(2023 - 07 - 03)); + assert_eq!(date!(2023 - 06 - 30).next_occurrence(Weekday::Monday), date!(2023 - 07 - 03)); + assert_eq!(date!(2023 - 07 - 01).next_occurrence(Weekday::Monday), date!(2023 - 07 - 03)); + assert_eq!(date!(2023 - 07 - 02).next_occurrence(Weekday::Monday), date!(2023 - 07 - 03)); + assert_eq!(date!(2023 - 07 - 03).next_occurrence(Weekday::Monday), date!(2023 - 07 - 10)); +} + +#[test] +fn prev_occurrence_test() { + assert_eq!(date!(2023 - 07 - 07).prev_occurrence(Weekday::Thursday), date!(2023 - 07 - 06)); + assert_eq!(date!(2023 - 07 - 06).prev_occurrence(Weekday::Thursday), date!(2023 - 06 - 29)); + assert_eq!(date!(2023 - 07 - 05).prev_occurrence(Weekday::Thursday), date!(2023 - 06 - 29)); + assert_eq!(date!(2023 - 07 - 04).prev_occurrence(Weekday::Thursday), date!(2023 - 06 - 29)); + assert_eq!(date!(2023 - 07 - 03).prev_occurrence(Weekday::Thursday), date!(2023 - 06 - 29)); + assert_eq!(date!(2023 - 07 - 02).prev_occurrence(Weekday::Thursday), date!(2023 - 06 - 29)); + assert_eq!(date!(2023 - 07 - 01).prev_occurrence(Weekday::Thursday), date!(2023 - 06 - 29)); + assert_eq!(date!(2023 - 06 - 30).prev_occurrence(Weekday::Thursday), date!(2023 - 06 - 29)); + assert_eq!(date!(2023 - 06 - 29).prev_occurrence(Weekday::Thursday), date!(2023 - 06 - 22)); +} + +#[test] +fn nth_next_occurrence_test() { + assert_eq!(date!(2023 - 06 - 25).nth_next_occurrence(Weekday::Monday, 5), date!(2023 - 07 - 24)); + assert_eq!(date!(2023 - 06 - 26).nth_next_occurrence(Weekday::Monday, 5), date!(2023 - 07 - 31)); +} + +#[test] +fn nth_prev_occurrence_test() { + assert_eq!(date!(2023 - 06 - 27).nth_prev_occurrence(Weekday::Monday, 3), date!(2023 - 06 - 12)); + assert_eq!(date!(2023 - 06 - 26).nth_prev_occurrence(Weekday::Monday, 3), date!(2023 - 06 - 05)); +} + +#[test] +#[should_panic] +fn next_occurrence_overflow_test() { + date!(+999999 - 12 - 25).next_occurrence(Weekday::Saturday); +} + +#[test] +#[should_panic] +fn prev_occurrence_overflow_test() { + date!(-999999 - 01 - 07).prev_occurrence(Weekday::Sunday); +} + +#[test] +#[should_panic] +fn nth_next_occurrence_overflow_test() { + date!(+999999 - 12 - 25).nth_next_occurrence(Weekday::Saturday, 1); +} + +#[test] +#[should_panic] +fn nth_next_occurence_zeroth_occurence_test() { + date!(2023 - 06 - 25).nth_next_occurrence(Weekday::Monday, 0); +} + +#[test] +#[should_panic] +fn nth_prev_occurence_zeroth_occurence_test() { + date!(2023 - 06 - 27).nth_prev_occurrence(Weekday::Monday, 0); +} + +#[test] +#[should_panic] +fn nth_prev_occurrence_overflow_test() { + date!(-999999 - 01 - 07).nth_prev_occurrence(Weekday::Sunday, 1); +} diff --git a/time/src/date.rs b/time/src/date.rs index 9f194b434..ad8565a8e 100644 --- a/time/src/date.rs +++ b/time/src/date.rs @@ -529,6 +529,106 @@ impl Date { } } + /// Calculates the first occurrence of a weekday that is strictly later than a given `Date`. + /// + /// # Panics + /// Panics if an overflow occurred. + /// + /// # Examples + /// ``` + /// # use time::Weekday; + /// # use time_macros::date; + /// assert_eq!( + /// date!(2023 - 06 - 28).next_occurrence(Weekday::Monday), + /// date!(2023 - 07 - 03) + /// ); + /// assert_eq!( + /// date!(2023 - 06 - 19).next_occurrence(Weekday::Monday), + /// date!(2023 - 06 - 26) + /// ); + /// ``` + pub const fn next_occurrence(self, weekday: Weekday) -> Self { + expect_opt!( + self.checked_next_occurrence(weekday), + "overflow calculating the next occurrence of a weekday" + ) + } + + /// Calculates the first occurrence of a weekday that is strictly earlier than a given `Date`. + /// + /// # Panics + /// Panics if an overflow occurred. + /// + /// # Examples + /// ``` + /// # use time::Weekday; + /// # use time_macros::date; + /// assert_eq!( + /// date!(2023 - 06 - 28).prev_occurrence(Weekday::Monday), + /// date!(2023 - 06 - 26) + /// ); + /// assert_eq!( + /// date!(2023 - 06 - 19).prev_occurrence(Weekday::Monday), + /// date!(2023 - 06 - 12) + /// ); + /// ``` + pub const fn prev_occurrence(self, weekday: Weekday) -> Self { + expect_opt!( + self.checked_prev_occurrence(weekday), + "overflow calculating the previous occurrence of a weekday" + ) + } + + /// Calculates the `n`th occurrence of a weekday that is strictly later than a given `Date`. + /// + /// # Panics + /// Panics if an overflow occurred or if `n == 0`. + /// + /// # Examples + /// ``` + /// # use time::Weekday; + /// # use time_macros::date; + /// assert_eq!( + /// date!(2023 - 06 - 25).nth_next_occurrence(Weekday::Monday, 5), + /// date!(2023 - 07 - 24) + /// ); + /// assert_eq!( + /// date!(2023 - 06 - 26).nth_next_occurrence(Weekday::Monday, 5), + /// date!(2023 - 07 - 31) + /// ); + /// ``` + pub const fn nth_next_occurrence(self, weekday: Weekday, n: u8) -> Self { + expect_opt!( + self.checked_nth_next_occurrence(weekday, n), + "overflow calculating the next occurrence of a weekday" + ) + } + + /// Calculates the `n`th occurrence of a weekday that is strictly earlier than a given `Date`. + /// + /// # Panics + /// Panics if an overflow occurred or if `n == 0`. + /// + /// # Examples + /// ``` + /// # use time::Weekday; + /// # use time_macros::date; + /// assert_eq!( + /// date!(2023 - 06 - 27).nth_prev_occurrence(Weekday::Monday, 3), + /// date!(2023 - 06 - 12) + /// ); + /// assert_eq!( + /// date!(2023 - 06 - 26).nth_prev_occurrence(Weekday::Monday, 3), + /// date!(2023 - 06 - 05) + /// ); + /// ``` + pub const fn nth_prev_occurrence(self, weekday: Weekday, n: u8) -> Self { + expect_opt!( + self.checked_nth_prev_occurrence(weekday, n), + "overflow calculating the previous occurrence of a weekday" + ) + } + /// Get the Julian day for the date. /// /// The algorithm to perform this conversion is derived from one provided by Peter Baum; it is @@ -641,6 +741,74 @@ impl Date { None } } + + /// Calculates the first occurrence of a weekday that is strictly later than a given `Date`. + /// Returns `None` if an overflow occurred. + pub(crate) const fn checked_next_occurrence(self, weekday: Weekday) -> Option { + let day_diff = match weekday as i8 - self.weekday() as i8 { + 1 | -6 => 1, + 2 | -5 => 2, + 3 | -4 => 3, + 4 | -3 => 4, + 5 | -2 => 5, + 6 | -1 => 6, + val => { + debug_assert!(val == 0); + 7 + } + }; + + self.checked_add(Duration::days(day_diff)) + } + + /// Calculates the first occurrence of a weekday that is strictly earlier than a given `Date`. + /// Returns `None` if an overflow occurred. + pub(crate) const fn checked_prev_occurrence(self, weekday: Weekday) -> Option { + let day_diff = match weekday as i8 - self.weekday() as i8 { + 1 | -6 => 6, + 2 | -5 => 5, + 3 | -4 => 4, + 4 | -3 => 3, + 5 | -2 => 2, + 6 | -1 => 1, + val => { + debug_assert!(val == 0); + 7 + } + }; + + self.checked_sub(Duration::days(day_diff)) + } + + /// Calculates the `n`th occurrence of a weekday that is strictly later than a given `Date`. + /// Returns `None` if an overflow occurred or if `n == 0`. + pub(crate) const fn checked_nth_next_occurrence(self, weekday: Weekday, n: u8) -> Option { + if n == 0 { + return None; + } + + let next_occ = self.checked_next_occurrence(weekday); + if let Some(val) = next_occ { + val.checked_add(Duration::weeks(n as i64 - 1)) + } else { + None + } + } + + /// Calculates the `n`th occurrence of a weekday that is strictly earlier than a given `Date`. + /// Returns `None` if an overflow occurred or if `n == 0`. + pub(crate) const fn checked_nth_prev_occurrence(self, weekday: Weekday, n: u8) -> Option { + if n == 0 { + return None; + } + + let next_occ = self.checked_prev_occurrence(weekday); + if let Some(val) = next_occ { + val.checked_sub(Duration::weeks(n as i64 - 1)) + } else { + None + } + } // endregion: checked arithmetic // region: saturating arithmetic