use crate::{
civil::{Date, DateTime, Weekday},
error::Error,
fmt::temporal::{DEFAULT_DATETIME_PARSER, DEFAULT_DATETIME_PRINTER},
util::b,
Zoned,
};
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct ISOWeekDate {
year: i16,
week: i8,
weekday: Weekday,
}
impl ISOWeekDate {
pub const MIN: ISOWeekDate = ISOWeekDate {
year: b::ISOYear::MIN,
week: b::ISOWeek::MIN,
weekday: Weekday::Monday,
};
pub const MAX: ISOWeekDate = ISOWeekDate {
year: b::ISOYear::MAX,
week: 52,
weekday: Weekday::Friday,
};
pub const ZERO: ISOWeekDate =
ISOWeekDate { year: 0, week: 1, weekday: Weekday::Monday };
#[inline]
pub fn new(
year: i16,
week: i8,
weekday: Weekday,
) -> Result<ISOWeekDate, Error> {
let year = b::ISOYear::check(year)?;
let week = b::ISOWeek::check(week)?;
debug_assert_eq!(b::Year::MIN, b::ISOYear::MIN);
debug_assert_eq!(b::Year::MAX, b::ISOYear::MAX);
if week == 53 && !is_long_year(year) {
return Err(b::ISOWeek::error().into());
}
if year == b::ISOYear::MAX
&& week == 52
&& weekday.to_monday_zero_offset()
> Weekday::Friday.to_monday_zero_offset()
{
return Err(b::WeekdayMondayOne::error().into());
}
Ok(ISOWeekDate { year, week, weekday })
}
#[cfg(test)]
#[inline]
fn new_constrain(
year: i16,
mut week: i8,
mut weekday: Weekday,
) -> ISOWeekDate {
debug_assert_eq!(b::Year::MIN, b::ISOYear::MIN);
debug_assert_eq!(b::Year::MAX, b::ISOYear::MAX);
if week == 53 && !is_long_year(year) {
week = 52;
}
if year == b::ISOYear::MAX
&& week == 52
&& weekday.to_monday_zero_offset()
> Weekday::Friday.to_monday_zero_offset()
{
weekday = Weekday::Friday;
}
ISOWeekDate { year, week, weekday }
}
#[inline]
pub fn from_date(date: Date) -> ISOWeekDate {
date.iso_week_date()
}
#[inline]
pub fn year(self) -> i16 {
self.year
}
#[inline]
pub fn week(self) -> i8 {
self.week
}
#[inline]
pub fn weekday(self) -> Weekday {
self.weekday
}
#[inline]
pub fn first_of_week(self) -> Result<ISOWeekDate, Error> {
ISOWeekDate::new(self.year(), self.week(), Weekday::Monday)
}
#[inline]
pub fn last_of_week(self) -> Result<ISOWeekDate, Error> {
ISOWeekDate::new(self.year(), self.week(), Weekday::Sunday)
}
#[inline]
pub fn first_of_year(self) -> Result<ISOWeekDate, Error> {
ISOWeekDate::new(self.year(), 1, Weekday::Monday)
}
#[inline]
pub fn last_of_year(self) -> Result<ISOWeekDate, Error> {
ISOWeekDate::new(self.year(), self.weeks_in_year(), Weekday::Sunday)
}
#[inline]
pub fn days_in_year(self) -> i16 {
if self.in_long_year() {
371
} else {
364
}
}
#[inline]
pub fn weeks_in_year(self) -> i8 {
if self.in_long_year() {
53
} else {
52
}
}
#[inline]
pub fn in_long_year(self) -> bool {
is_long_year(self.year())
}
#[inline]
pub fn tomorrow(self) -> Result<ISOWeekDate, Error> {
self.date().tomorrow().map(|d| d.iso_week_date())
}
#[inline]
pub fn yesterday(self) -> Result<ISOWeekDate, Error> {
self.date().yesterday().map(|d| d.iso_week_date())
}
#[inline]
pub fn date(self) -> Date {
Date::from_iso_week_date(self)
}
}
impl Default for ISOWeekDate {
fn default() -> ISOWeekDate {
ISOWeekDate::ZERO
}
}
impl core::fmt::Display for ISOWeekDate {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use crate::fmt::StdFmtWrite;
DEFAULT_DATETIME_PRINTER
.print_iso_week_date(self, StdFmtWrite(f))
.map_err(|_| core::fmt::Error)
}
}
impl core::str::FromStr for ISOWeekDate {
type Err = Error;
fn from_str(string: &str) -> Result<ISOWeekDate, Error> {
DEFAULT_DATETIME_PARSER.parse_iso_week_date(string)
}
}
impl Ord for ISOWeekDate {
#[inline]
fn cmp(&self, other: &ISOWeekDate) -> core::cmp::Ordering {
(self.year(), self.week(), self.weekday().to_monday_one_offset()).cmp(
&(
other.year(),
other.week(),
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)
}
}
impl From<DateTime> for ISOWeekDate {
#[inline]
fn from(dt: DateTime) -> ISOWeekDate {
ISOWeekDate::from(dt.date())
}
}
impl From<Zoned> for ISOWeekDate {
#[inline]
fn from(zdt: Zoned) -> ISOWeekDate {
ISOWeekDate::from(zdt.date())
}
}
impl<'a> From<&'a Zoned> for ISOWeekDate {
#[inline]
fn from(zdt: &'a Zoned) -> ISOWeekDate {
ISOWeekDate::from(zdt.date())
}
}
#[cfg(feature = "serde")]
impl serde_core::Serialize for ISOWeekDate {
#[inline]
fn serialize<S: serde_core::Serializer>(
&self,
serializer: S,
) -> Result<S::Ok, S::Error> {
serializer.collect_str(self)
}
}
#[cfg(feature = "serde")]
impl<'de> serde_core::Deserialize<'de> for ISOWeekDate {
#[inline]
fn deserialize<D: serde_core::Deserializer<'de>>(
deserializer: D,
) -> Result<ISOWeekDate, D::Error> {
use serde_core::de;
struct ISOWeekDateVisitor;
impl<'de> de::Visitor<'de> for ISOWeekDateVisitor {
type Value = ISOWeekDate;
fn expecting(
&self,
f: &mut core::fmt::Formatter,
) -> core::fmt::Result {
f.write_str("an ISO 8601 week date string")
}
#[inline]
fn visit_bytes<E: de::Error>(
self,
value: &[u8],
) -> Result<ISOWeekDate, E> {
DEFAULT_DATETIME_PARSER
.parse_iso_week_date(value)
.map_err(de::Error::custom)
}
#[inline]
fn visit_str<E: de::Error>(
self,
value: &str,
) -> Result<ISOWeekDate, E> {
self.visit_bytes(value.as_bytes())
}
}
deserializer.deserialize_str(ISOWeekDateVisitor)
}
}
#[cfg(test)]
impl quickcheck::Arbitrary for ISOWeekDate {
fn arbitrary(g: &mut quickcheck::Gen) -> ISOWeekDate {
let year = b::ISOYear::arbitrary(g);
let week = b::ISOWeek::arbitrary(g);
let weekday = Weekday::arbitrary(g);
ISOWeekDate::new_constrain(year, week, weekday)
}
fn shrink(&self) -> alloc::boxed::Box<dyn Iterator<Item = ISOWeekDate>> {
alloc::boxed::Box::new(
(self.year(), self.week(), self.weekday()).shrink().map(
|(year, week, weekday)| {
ISOWeekDate::new_constrain(year, week, weekday)
},
),
)
}
}
fn is_long_year(year: i16) -> bool {
let last =
Date::new(year, 12, 31).expect("last day of year is always valid");
let weekday = last.weekday();
weekday == Weekday::Thursday
|| (last.in_leap_year() && weekday == Weekday::Friday)
}
#[cfg(not(miri))]
#[cfg(test)]
mod tests {
use super::*;
quickcheck::quickcheck! {
fn prop_all_long_years_have_53rd_week(year: i16) -> quickcheck::TestResult {
if b::Year::check(year).is_err() {
return quickcheck::TestResult::discard();
}
quickcheck::TestResult::from_bool(!is_long_year(year)
|| ISOWeekDate::new(year, 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.date().checked_add(-1.days()).unwrap();
quickcheck::TestResult::from_bool(prev_date.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.date().checked_add(1.days()).unwrap();
quickcheck::TestResult::from_bool(wd < next_date.iso_week_date())
}
}
}