diff --git a/.gitignore b/.gitignore index a9d37c560c..9fac28caba 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ target Cargo.lock +.tool-versions diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c07750da2..e5922283bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ Versions with only mechanical changes will be omitted from the following list. * Add support for microseconds timestamps serde serialization for `NaiveDateTime`. * Add support for optional timestamps serde serialization for `NaiveDateTime`. * Fix build for wasm32-unknown-emscripten (@yu-re-ka #593) +* Add support for getting week bounds based on a specific date and a `Weekday` for `NaiveDate` and `Date` (#666) ## 0.4.19 diff --git a/src/date.rs b/src/date.rs index 0758cc844f..0ef2def82f 100644 --- a/src/date.rs +++ b/src/date.rs @@ -16,12 +16,29 @@ use rkyv::{Archive, Deserialize, Serialize}; use crate::format::Locale; #[cfg(any(feature = "alloc", feature = "std", test))] use crate::format::{DelayedFormat, Item, StrftimeItems}; -use crate::naive::{self, IsoWeek, NaiveDate, NaiveTime}; +use crate::naive::{self, IsoWeek, NaiveDate, NaiveTime, NaiveWeek}; use crate::offset::{TimeZone, Utc}; use crate::oldtime::Duration as OldDuration; +use crate::traits::Weeklike; use crate::DateTime; use crate::{Datelike, Weekday}; +/// A week represented by a [`NaiveWeek`] + [Offset](crate::offset::Offset). +#[derive(Debug)] +pub struct Week { + week: NaiveWeek, + offset: Tz::Offset, +} + +impl Weeklike for Week { + type Day = Date; + + #[inline] + fn first_day(&self) -> Self::Day { + Date::from_utc(self.week.first_day(), self.offset.clone()) + } +} + /// ISO 8601 calendar date with time zone. /// /// You almost certainly want to be using a [`NaiveDate`] instead of this type. @@ -275,6 +292,13 @@ impl Date { pub fn naive_local(&self) -> NaiveDate { self.date } + + /// Returns the [`Week`] that the date belongs, starting with the + /// [`Weekday`] specified. + #[inline] + pub fn week(&self, weekday: Weekday) -> Week { + Week { week: self.date.week(weekday), offset: self.offset.clone() } + } } /// Maps the local date to other date with given conversion function. @@ -498,3 +522,29 @@ where write!(f, "{}{}", self.naive_local(), self.offset) } } + +#[cfg(test)] +mod tests { + use crate::{Local, Weekday, Weeklike}; + + #[test] + fn test_week() { + let weekdays = [ + Weekday::Mon, + Weekday::Tue, + Weekday::Wed, + Weekday::Thu, + Weekday::Fri, + Weekday::Sat, + Weekday::Sun, + ]; + let date = Local::today(); + for start in weekdays { + let week = date.week(start); + let days = week.days(); + assert!(week.first_day() <= date); + assert!(week.last_day() >= date); + assert!(days.contains(&date)); + } + } +} diff --git a/src/lib.rs b/src/lib.rs index d81cfe1e51..1b587b988e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -472,7 +472,7 @@ macro_rules! try_opt { } mod date; -pub use date::{Date, MAX_DATE, MIN_DATE}; +pub use date::{Date, Week, MAX_DATE, MIN_DATE}; mod datetime; #[cfg(feature = "rustc-serialize")] @@ -487,7 +487,7 @@ pub use format::{ParseError, ParseResult}; pub mod naive; #[doc(no_inline)] -pub use naive::{IsoWeek, NaiveDate, NaiveDateTime, NaiveTime}; +pub use naive::{IsoWeek, NaiveDate, NaiveDateTime, NaiveTime, NaiveWeek}; pub mod offset; #[cfg(feature = "clock")] @@ -506,7 +506,7 @@ mod month; pub use month::{Month, ParseMonthError}; mod traits; -pub use traits::{Datelike, Timelike}; +pub use traits::{Datelike, Timelike, Weeklike}; #[cfg(feature = "__internal_bench")] #[doc(hidden)] diff --git a/src/naive/date.rs b/src/naive/date.rs index 0bc880f24b..3cbdab9852 100644 --- a/src/naive/date.rs +++ b/src/naive/date.rs @@ -19,7 +19,7 @@ use crate::format::{parse, ParseError, ParseResult, Parsed, StrftimeItems}; use crate::format::{Item, Numeric, Pad}; use crate::naive::{IsoWeek, NaiveDateTime, NaiveTime}; use crate::oldtime::Duration as OldDuration; -use crate::{Datelike, Weekday}; +use crate::{Datelike, Duration, Weekday, Weeklike}; use super::internals::{self, DateImpl, Mdf, Of, YearFlags}; use super::isoweek; @@ -50,6 +50,26 @@ const MIN_DAYS_FROM_YEAR_0: i32 = (MIN_YEAR + 400_000) * 365 + (MIN_YEAR + 400_0 #[cfg(test)] // only used for testing, but duplicated in naive::datetime const MAX_BITS: usize = 44; +/// A week represented by a [`NaiveDate`] and a [`Weekday`] which is the first +/// day of the week. +#[derive(Debug)] +pub struct NaiveWeek { + date: NaiveDate, + start: Weekday, +} + +impl Weeklike for NaiveWeek { + type Day = NaiveDate; + + #[inline] + fn first_day(&self) -> Self::Day { + let start = self.start.num_days_from_monday(); + let end = self.date.weekday().num_days_from_monday(); + let days = if start > end { 7 - end } else { end - start }; + self.date - Duration::days(days.into()) + } +} + /// ISO 8601 calendar date without timezone. /// Allows for every [proleptic Gregorian date](#calendar-date) /// from Jan 1, 262145 BCE to Dec 31, 262143 CE. @@ -1095,6 +1115,13 @@ impl NaiveDate { pub fn iter_weeks(&self) -> NaiveDateWeeksIterator { NaiveDateWeeksIterator { value: *self } } + + /// Returns the [`NaiveWeek`] that the date belongs to, starting with the [`Weekday`] + /// specified. + #[inline] + pub fn week(&self, start: Weekday) -> NaiveWeek { + NaiveWeek { date: *self, start } + } } impl Datelike for NaiveDate { @@ -1894,7 +1921,7 @@ mod tests { use super::{MAX_DATE, MAX_DAYS_FROM_YEAR_0, MAX_YEAR}; use super::{MIN_DATE, MIN_DAYS_FROM_YEAR_0, MIN_YEAR}; use crate::oldtime::Duration; - use crate::{Datelike, Weekday}; + use crate::{Datelike, Weekday, Weeklike}; use std::{i32, u32}; #[test] @@ -2411,4 +2438,29 @@ mod tests { fn test_week_iterator_limit() { assert_eq!(NaiveDate::from_ymd(262143, 12, 12).iter_weeks().take(4).count(), 2); } + + #[test] + fn test_naiveweek_less_than_weekday() { + let date = NaiveDate::from_ymd(2022, 5, 18); + let week = date.week(Weekday::Mon); + assert_eq!(week.first_day(), NaiveDate::from_ymd(2022, 5, 16)); + assert_eq!(week.last_day(), NaiveDate::from_ymd(2022, 5, 22)); + } + + #[test] + fn test_naiveweek_equal_to_weekday() { + let date = NaiveDate::from_ymd(2022, 5, 18); + let week = date.week(Weekday::Wed); + assert_eq!(week.first_day(), date); + assert_eq!(week.last_day(), NaiveDate::from_ymd(2022, 5, 24)); + } + + /// TODO: Fix this test + #[test] + fn test_naiveweek_greater_than_weekday() { + let date = NaiveDate::from_ymd(2022, 5, 18); + let week = date.week(Weekday::Fri); + assert_eq!(week.first_day(), NaiveDate::from_ymd(2022, 5, 13)); + assert_eq!(week.last_day(), NaiveDate::from_ymd(2022, 5, 19)); + } } diff --git a/src/naive/mod.rs b/src/naive/mod.rs index f23a731a76..171cac36bb 100644 --- a/src/naive/mod.rs +++ b/src/naive/mod.rs @@ -10,7 +10,7 @@ mod internals; mod isoweek; mod time; -pub use self::date::{NaiveDate, MAX_DATE, MIN_DATE}; +pub use self::date::{NaiveDate, NaiveWeek, MAX_DATE, MIN_DATE}; #[cfg(feature = "rustc-serialize")] #[allow(deprecated)] pub use self::datetime::rustc_serialize::TsSeconds; diff --git a/src/traits.rs b/src/traits.rs index b3905e670d..c8b2520ba2 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -1,4 +1,63 @@ -use crate::{IsoWeek, Weekday}; +use core::ops::{Add, RangeInclusive}; + +use crate::{Duration, IsoWeek, Weekday}; + +/// The common set of methods for a week component. +pub trait Weeklike: Sized { + /// An associated type, representing a day within the context of a week. + type Day: Add; + + /// Returns a date representing the first day of the week. + /// + /// # Examples + /// + /// ``` + /// use chrono::{NaiveDate, Weekday, Weeklike}; + /// + /// let date = NaiveDate::from_ymd(2022, 4, 18); + /// let week = date.week(Weekday::Mon); + /// assert!(week.first_day() <= date); + /// ``` + fn first_day(&self) -> Self::Day; + + /// Returns a date representing the last day of the week. + /// + /// # Examples + /// + /// ``` + /// use chrono::{NaiveDate, Weekday, Weeklike}; + /// + /// let date = NaiveDate::from_ymd(2022, 4, 18); + /// let week = date.week(Weekday::Mon); + /// assert!(week.last_day() >= date); + /// ``` + #[inline] + fn last_day(&self) -> Self::Day { + self.first_day() + Duration::days(6) + } + + /// Returns a [`RangeInclusive`] representing the whole week bounded by + /// [first_day()][first_day] and [last_day()][last_day] functions. + /// + /// # Examples + /// + /// ``` + /// use chrono::{NaiveDate, Weekday, Weeklike}; + /// + /// let date = NaiveDate::from_ymd(2022, 4, 18); + /// let week = date.week(Weekday::Mon); + /// let days = week.days(); + /// assert_eq!(days.start(), &week.first_day()); + /// assert_eq!(days.end(), &week.last_day()); + /// ``` + /// + /// [first_day]: ./trait.Weeklike.html#tymethod.first_day + /// [last_day]: ./trait.Weeklike.html#method.last_day + #[inline] + fn days(&self) -> RangeInclusive { + self.first_day()..=self.last_day() + } +} /// The common set of methods for date component. pub trait Datelike: Sized { @@ -179,9 +238,21 @@ pub trait Timelike: Sized { #[cfg(test)] mod tests { - use super::Datelike; + use super::{Datelike, Weeklike}; use crate::naive::{MAX_DATE, MIN_DATE}; - use crate::{Duration, NaiveDate}; + use crate::{Duration, NaiveDate, Weekday}; + + struct FakeWeek { + date: NaiveDate, + } + + impl Weeklike for FakeWeek { + type Day = NaiveDate; + + fn first_day(&self) -> Self::Day { + self.date + } + } /// Tests `Datelike::num_days_from_ce` against an alternative implementation. /// @@ -239,4 +310,23 @@ mod tests { ); } } + + #[test] + fn test_weeklike_last_day() { + let date = NaiveDate::from_ymd(2022, 4, 18); + let week = FakeWeek { date }; + let last_day = week.last_day(); + assert_eq!(date.weekday(), Weekday::Mon); + assert_eq!(last_day.weekday(), Weekday::Sun); + assert_eq!(last_day, NaiveDate::from_ymd(2022, 4, 24)); + } + + #[test] + fn test_weeklike_days() { + let date = NaiveDate::from_ymd(2022, 4, 18); + let week = FakeWeek { date }; + let days = week.days(); + assert_eq!(days.start(), &week.first_day()); + assert_eq!(days.end(), &week.last_day()); + } }