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..3a111c2a71 100644 --- a/src/date.rs +++ b/src/date.rs @@ -16,11 +16,27 @@ 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::DateTime; -use crate::{Datelike, Weekday}; +use crate::{Datelike, Weekday, Weeklike}; + +/// 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 start_day(&self) -> Self::Day { + Date::from_utc(self.week.start_day(), self.offset.clone()) + } +} /// ISO 8601 calendar date with time zone. /// @@ -275,6 +291,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 +521,26 @@ where write!(f, "{}{}", self.naive_local(), self.offset) } } + +#[cfg(test)] +mod tests { + use crate::{Datelike, TimeZone, Utc, Weekday, Weeklike}; + + #[test] + fn test_week_start_day() { + let weekdays = [ + Weekday::Mon, + Weekday::Tue, + Weekday::Wed, + Weekday::Thu, + Weekday::Fri, + Weekday::Sat, + Weekday::Sun, + ]; + let date = Utc.ymd(2022, 4, 18); + assert_eq!(date.weekday(), Weekday::Mon); + for weekday in weekdays { + assert!(date.week(weekday).start_day() <= date); + } + } +} diff --git a/src/lib.rs b/src/lib.rs index d81cfe1e51..ea8aac9edd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -440,7 +440,7 @@ doctest!("../README.md"); /// A convenience module appropriate for glob imports (`use chrono::prelude::*;`). pub mod prelude { #[doc(no_inline)] - pub use crate::Date; + pub use crate::{Date, Week}; #[cfg(feature = "clock")] #[doc(no_inline)] pub use crate::Local; @@ -452,11 +452,11 @@ pub mod prelude { #[doc(no_inline)] pub use crate::{DateTime, SecondsFormat}; #[doc(no_inline)] - pub use crate::{Datelike, Month, Timelike, Weekday}; + pub use crate::{Datelike, Month, Timelike, Weekday, Weeklike}; #[doc(no_inline)] pub use crate::{FixedOffset, Utc}; #[doc(no_inline)] - pub use crate::{NaiveDate, NaiveDateTime, NaiveTime}; + pub use crate::{NaiveDate, NaiveDateTime, NaiveTime, NaiveWeek}; #[doc(no_inline)] pub use crate::{Offset, TimeZone}; } @@ -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..30e81bda50 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, + weekday: Weekday, +} + +impl Weeklike for NaiveWeek { + type Day = NaiveDate; + + #[inline] + fn start_day(&self) -> Self::Day { + let start = self.date.weekday().num_days_from_monday() as i64; + let end = self.weekday.num_days_from_monday() as i64; + let days = start - end; + self.date - Duration::days(if days >= 0 { days } else { 7 + days }) + } +} + /// 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, starting with the + /// [`Weekday`] specified. + #[inline] + pub fn week(&self, weekday: Weekday) -> NaiveWeek { + NaiveWeek { date: *self, weekday } + } } 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,22 @@ 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_start_day() { + let weekdays = [ + Weekday::Mon, + Weekday::Tue, + Weekday::Wed, + Weekday::Thu, + Weekday::Fri, + Weekday::Sat, + Weekday::Sun, + ]; + let date = NaiveDate::from_ymd(2022, 4, 18); + assert_eq!(date.weekday(), Weekday::Mon); + for weekday in weekdays { + assert!(date.week(weekday).start_day() <= date); + } + } } 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..270a6a5693 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 start 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.start_day() <= date); + /// ``` + fn start_day(&self) -> Self::Day; + + /// Returns a date representing the end 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.end_day() >= date); + /// ``` + #[inline] + fn end_day(&self) -> Self::Day { + self.start_day() + Duration::days(6) + } + + /// Returns a [`RangeInclusive`] representing the whole week bounded by + /// [start_day()][start_day] and [end_day()][end_day] functions. + /// + /// # Examples + /// + /// ``` + /// use chrono::{NaiveDate, Weekday, Weeklike}; + /// + /// let date = NaiveDate::from_ymd(2022, 4, 18); + /// let week = date.week(Weekday::Mon); + /// let range = week.range(); + /// assert_eq!(range.start(), &week.start_day()); + /// assert_eq!(range.end(), &week.end_day()); + /// ``` + /// + /// [start_day]: ./trait.Weeklike.html#tymethod.start_day + /// [end_day]: ./trait.Weeklike.html#method.end_day + #[inline] + fn range(&self) -> RangeInclusive { + self.start_day()..=self.end_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 start_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_end_day() { + let start_day = NaiveDate::from_ymd(2022, 4, 18); + let week = FakeWeek { date: start_day }; + let end_day = week.end_day(); + assert_eq!(start_day.weekday(), Weekday::Mon); + assert_eq!(end_day.weekday(), Weekday::Sun); + assert_eq!(end_day, NaiveDate::from_ymd(2022, 4, 24)); + } + + #[test] + fn test_weeklike_range() { + let start_day = NaiveDate::from_ymd(2022, 4, 18); + let week = FakeWeek { date: start_day }; + let range = week.range(); + assert_eq!(range.start(), &start_day); + assert_eq!(range.end(), &week.end_day()); + } }