use crate::calendar::weekday_from_days;
use crate::duration::Days;
use crate::naive::date::NaiveDate;
use crate::weekday::Weekday;
use core::hash::{Hash, Hasher};
use core::ops::RangeInclusive;
const fn add_signed_days(date: NaiveDate, delta: i32) -> Option<NaiveDate> {
if delta >= 0 {
date.checked_add_days(Days::new(delta as u64))
} else {
date.checked_sub_days(Days::new((-(delta as i64)) as u64))
}
}
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct NaiveWeek {
date: NaiveDate,
start: Weekday,
}
impl NaiveWeek {
pub(crate) const fn new(date: NaiveDate, start: Weekday) -> Self {
Self { date, start }
}
pub const fn first_day(&self) -> NaiveDate {
match self.checked_first_day() {
Some(d) => d,
None => panic!("first weekday out of range for NaiveDate"),
}
}
pub const fn checked_first_day(&self) -> Option<NaiveDate> {
let start = self.start.num_days_from_monday() as i32;
let ref_day = weekday_from_days(self.date.days_since_epoch()).num_days_from_monday() as i32;
let delta = start - ref_day - if start > ref_day { 7 } else { 0 };
add_signed_days(self.date, delta)
}
pub const fn last_day(&self) -> NaiveDate {
match self.checked_last_day() {
Some(d) => d,
None => panic!("last weekday out of range for NaiveDate"),
}
}
pub const fn checked_last_day(&self) -> Option<NaiveDate> {
let end = self.start.pred().num_days_from_monday() as i32;
let ref_day = weekday_from_days(self.date.days_since_epoch()).num_days_from_monday() as i32;
let delta = end - ref_day + if end < ref_day { 7 } else { 0 };
add_signed_days(self.date, delta)
}
pub fn days(&self) -> RangeInclusive<NaiveDate> {
match self.checked_days() {
Some(val) => val,
None => panic!("first or last weekday is out of range for NaiveDate"),
}
}
pub fn checked_days(&self) -> Option<RangeInclusive<NaiveDate>> {
match (self.checked_first_day(), self.checked_last_day()) {
(Some(first), Some(last)) => Some(first..=last),
_ => None,
}
}
}
impl PartialEq for NaiveWeek {
fn eq(&self, other: &Self) -> bool {
self.first_day() == other.first_day()
}
}
impl Eq for NaiveWeek {}
impl Hash for NaiveWeek {
fn hash<H: Hasher>(&self, state: &mut H) {
self.first_day().hash(state);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::Datelike;
const ALL_WEEKDAYS: [Weekday; 7] = [
Weekday::Mon,
Weekday::Tue,
Weekday::Wed,
Weekday::Thu,
Weekday::Fri,
Weekday::Sat,
Weekday::Sun,
];
fn thursday() -> NaiveDate {
NaiveDate::from_ymd_opt(2023, 6, 15).unwrap()
}
#[test]
fn week_starting_monday_bounds_a_thursday_correctly() {
let week = thursday().week(Weekday::Mon);
assert_eq!(week.first_day(), NaiveDate::from_ymd_opt(2023, 6, 12).unwrap());
assert_eq!(week.last_day(), NaiveDate::from_ymd_opt(2023, 6, 18).unwrap());
}
#[test]
fn week_starting_sunday_bounds_a_thursday_correctly() {
let week = thursday().week(Weekday::Sun);
assert_eq!(week.first_day(), NaiveDate::from_ymd_opt(2023, 6, 11).unwrap());
assert_eq!(week.last_day(), NaiveDate::from_ymd_opt(2023, 6, 17).unwrap());
}
#[test]
fn week_always_spans_exactly_seven_days_and_starts_on_the_right_weekday() {
for &start in &ALL_WEEKDAYS {
let week = thursday().week(start);
assert_eq!(week.last_day() - week.first_day(), crate::duration::Duration::days(6));
assert_eq!(week.first_day().weekday(), start);
}
}
#[test]
fn the_original_date_always_falls_within_its_own_week() {
let d = thursday();
for &start in &ALL_WEEKDAYS {
let week = d.week(start);
assert!(week.first_day() <= d && d <= week.last_day(), "start={start:?}");
}
}
#[test]
fn days_returns_the_inclusive_seven_day_range() {
let week = thursday().week(Weekday::Mon);
let days = week.days();
assert_eq!(*days.start(), week.first_day());
assert_eq!(*days.end(), week.last_day());
assert_eq!(*days.end() - *days.start(), crate::duration::Duration::days(6));
assert!(days.contains(&thursday()));
}
#[test]
fn checked_first_and_last_day_agree_with_the_panicking_variants() {
let week = thursday().week(Weekday::Mon);
assert_eq!(week.checked_first_day(), Some(week.first_day()));
assert_eq!(week.checked_last_day(), Some(week.last_day()));
assert_eq!(week.checked_days(), Some(week.first_day()..=week.last_day()));
}
#[test]
fn checked_first_day_is_none_when_the_week_would_start_before_min() {
let mut some_count = 0;
let mut none_count = 0;
for &start in &ALL_WEEKDAYS {
match NaiveDate::MIN.week(start).checked_first_day() {
Some(d) => {
assert_eq!(d, NaiveDate::MIN);
some_count += 1;
}
None => none_count += 1,
}
}
assert_eq!(some_count, 1);
assert_eq!(none_count, 6);
}
#[test]
fn checked_last_day_is_none_when_the_week_would_end_after_max() {
let mut some_count = 0;
let mut none_count = 0;
for &start in &ALL_WEEKDAYS {
match NaiveDate::MAX.week(start).checked_last_day() {
Some(d) => {
assert_eq!(d, NaiveDate::MAX);
some_count += 1;
}
None => none_count += 1,
}
}
assert_eq!(some_count, 1);
assert_eq!(none_count, 6);
}
#[test]
fn equality_and_ordering_are_based_on_first_day_not_the_originating_date() {
let a = thursday().week(Weekday::Mon);
let b = NaiveDate::from_ymd_opt(2023, 6, 13).unwrap().week(Weekday::Mon);
assert_eq!(a, b);
assert_eq!(a.first_day(), b.first_day());
}
}