use crate::{
civil::{Date, Weekday},
error::Error,
util::{
rangeint::RInto,
t::{self, ISOWeek, ISOYear, C},
},
};
#[derive(Clone, Copy, Hash)]
pub struct ISOWeekDate {
year: ISOYear,
week: ISOWeek,
weekday: Weekday,
}
impl ISOWeekDate {
pub const MIN: ISOWeekDate = ISOWeekDate {
year: ISOYear::new_unchecked(-9999),
week: ISOWeek::new_unchecked(1),
weekday: Weekday::Monday,
};
pub const MAX: ISOWeekDate = ISOWeekDate {
year: ISOYear::new_unchecked(9999),
week: ISOWeek::new_unchecked(52),
weekday: Weekday::Friday,
};
pub const ZERO: ISOWeekDate = ISOWeekDate {
year: ISOYear::new_unchecked(0),
week: ISOWeek::new_unchecked(1),
weekday: Weekday::Monday,
};
#[inline]
pub fn new(
year: i16,
week: i8,
weekday: Weekday,
) -> Result<ISOWeekDate, Error> {
let year = ISOYear::try_new("year", year)?;
let week = ISOWeek::try_new("week", week)?;
ISOWeekDate::new_ranged(year, week, weekday)
}
#[inline]
pub fn from_date(date: Date) -> ISOWeekDate {
date.to_iso_week_date()
}
#[inline]
pub fn year(self) -> i16 {
self.year_ranged().get()
}
#[inline]
pub fn week(self) -> i8 {
self.week_ranged().get()
}
#[inline]
pub fn weekday(self) -> Weekday {
self.weekday
}
#[inline]
pub fn in_long_year(self) -> bool {
is_long_year(self.year_ranged())
}
#[inline]
pub fn to_date(self) -> Date {
Date::from_iso_week_date(self)
}
}
impl ISOWeekDate {
#[inline]
pub(crate) fn new_ranged(
year: impl RInto<ISOYear>,
week: impl RInto<ISOWeek>,
weekday: Weekday,
) -> Result<ISOWeekDate, Error> {
let year = year.rinto();
let week = week.rinto();
debug_assert_eq!(t::Year::MIN, ISOYear::MIN);
debug_assert_eq!(t::Year::MAX, ISOYear::MAX);
if week == 53 && !is_long_year(year) {
return Err(Error::specific("ISO week number", week));
}
if year == ISOYear::MAX_SELF
&& week == 52
&& weekday.to_monday_zero_offset()
> Weekday::Friday.to_monday_zero_offset()
{
return Err(Error::signed(
"weekday",
weekday.to_monday_zero_offset(),
Weekday::Monday.to_monday_one_offset(),
Weekday::Friday.to_monday_one_offset(),
));
}
Ok(ISOWeekDate { year, week, weekday })
}
#[cfg(test)]
#[inline]
pub(crate) fn new_ranged_constrain(
year: impl RInto<ISOYear>,
week: impl RInto<ISOWeek>,
mut weekday: Weekday,
) -> ISOWeekDate {
let year = year.rinto();
let mut week = week.rinto();
debug_assert_eq!(t::Year::MIN, ISOYear::MIN);
debug_assert_eq!(t::Year::MAX, ISOYear::MAX);
if week == 53 && !is_long_year(year) {
week = ISOWeek::new(52).unwrap();
}
if year == ISOYear::MAX_SELF
&& week == 52
&& weekday.to_monday_zero_offset()
> Weekday::Friday.to_monday_zero_offset()
{
weekday = Weekday::Friday;
}
ISOWeekDate { year, week, weekday }
}
#[inline]
pub(crate) fn year_ranged(self) -> ISOYear {
self.year
}
#[inline]
pub(crate) fn week_ranged(self) -> ISOWeek {
self.week
}
}
impl Default for ISOWeekDate {
fn default() -> ISOWeekDate {
ISOWeekDate::ZERO
}
}
impl core::fmt::Debug for ISOWeekDate {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
f.debug_struct("ISOWeekDate")
.field("year", &self.year_ranged().debug())
.field("week", &self.week_ranged().debug())
.field("weekday", &self.weekday)
.finish()
}
}
impl Eq for ISOWeekDate {}
impl PartialEq for ISOWeekDate {
#[inline]
fn eq(&self, other: &ISOWeekDate) -> bool {
self.weekday == other.weekday
&& self.week.get() == other.week.get()
&& self.year.get() == other.year.get()
}
}
impl Ord for ISOWeekDate {
#[inline]
fn cmp(&self, other: &ISOWeekDate) -> core::cmp::Ordering {
(self.year.get(), self.week.get(), self.weekday.to_monday_one_offset())
.cmp(&(
other.year.get(),
other.week.get(),
other.weekday.to_monday_one_offset(),
))
}
}
impl PartialOrd for ISOWeekDate {
#[inline]
fn partial_cmp(&self, other: &ISOWeekDate) -> Option<core::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl From<Date> for ISOWeekDate {
#[inline]
fn from(date: Date) -> ISOWeekDate {
ISOWeekDate::from_date(date)
}
}
#[cfg(test)]
impl quickcheck::Arbitrary for ISOWeekDate {
fn arbitrary(g: &mut quickcheck::Gen) -> ISOWeekDate {
let year = ISOYear::arbitrary(g);
let week = ISOWeek::arbitrary(g);
let weekday = Weekday::arbitrary(g);
ISOWeekDate::new_ranged_constrain(year, week, weekday)
}
fn shrink(&self) -> alloc::boxed::Box<dyn Iterator<Item = ISOWeekDate>> {
alloc::boxed::Box::new(
(self.year_ranged(), self.week_ranged(), self.weekday())
.shrink()
.map(|(year, week, weekday)| {
ISOWeekDate::new_ranged_constrain(year, week, weekday)
}),
)
}
}
fn is_long_year(year: ISOYear) -> bool {
let last = Date::new_ranged(year, C(12), C(31))
.expect("last day of year is always valid");
let weekday = last.weekday();
weekday == Weekday::Thursday
|| (last.in_leap_year() && weekday == Weekday::Friday)
}
#[cfg(test)]
mod tests {
use super::*;
quickcheck::quickcheck! {
fn prop_all_long_years_have_53rd_week(year: ISOYear) -> bool {
!is_long_year(year)
|| ISOWeekDate::new(year.get(), 53, Weekday::Sunday).is_ok()
}
fn prop_prev_day_is_less(wd: ISOWeekDate) -> quickcheck::TestResult {
use crate::ToSpan;
if wd == ISOWeekDate::MIN {
return quickcheck::TestResult::discard();
}
let prev_date = wd.to_date().checked_add(-1.days()).unwrap();
quickcheck::TestResult::from_bool(prev_date.to_iso_week_date() < wd)
}
fn prop_next_day_is_greater(wd: ISOWeekDate) -> quickcheck::TestResult {
use crate::ToSpan;
if wd == ISOWeekDate::MAX {
return quickcheck::TestResult::discard();
}
let next_date = wd.to_date().checked_add(1.days()).unwrap();
quickcheck::TestResult::from_bool(wd < next_date.to_iso_week_date())
}
}
}