use std::cmp::Ordering;
use std::error::Error;
use std::fmt::Display;
use std::ops::{Neg, RangeInclusive};
use jiff::civil::{Date, ISOWeekDate, Weekday};
use jiff::tz::TimeZone;
use jiff::{Error as JiffError, Span, Timestamp};
use crate::test_utils::MOCK_TODAY_DATE;
pub const DAYS_IN_WEEK: u8 = 7;
pub const ISO_WEEKS_IN_SHORT_YEAR: u8 = 52;
pub const ISO_WEEKS_IN_LONG_YEAR: u8 = 53;
pub const MONTHS_IN_YEAR: u8 = 12;
pub const DAYS_IN_COMMON_YEAR: u16 = 365;
pub const DAYS_IN_LEAP_YEAR: u16 = 366;
pub(crate) fn mockable_date_today(mock: bool, tz: TimeZone) -> Date {
if mock {
MOCK_TODAY_DATE
} else {
Timestamp::now().to_zoned(tz).date()
}
}
#[must_use]
pub fn date_today(tz: TimeZone) -> Date {
mockable_date_today(cfg!(test), tz)
}
pub fn iso_weeks_in_year(year: i16) -> Result<u8, JiffError> {
let start_of_year = Date::new(year, 1, 1)?;
let is_long_year = start_of_year.weekday() == Weekday::Thursday
|| (start_of_year.in_leap_year() && start_of_year.weekday() == Weekday::Wednesday);
if is_long_year {
Ok(ISO_WEEKS_IN_LONG_YEAR)
} else {
Ok(ISO_WEEKS_IN_SHORT_YEAR)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct OffsetIsoWeek {
year: i16,
week: u8,
week_start_offset: i8,
}
impl OffsetIsoWeek {
pub const ALLOWED_OFFSET_RANGE: RangeInclusive<i8> = -6..=6;
pub const ISO_OFFSET: i8 = 0;
#[must_use]
pub fn unchecked_new_with_offset(year: i16, week: u8, week_start_offset: i8) -> Self {
OffsetIsoWeek {
year,
week,
week_start_offset,
}
}
pub fn new(year: i16, week: u8) -> Result<Self, OffsetIsoWeekCreationError> {
Self::new_with_offset(year, week, Self::ISO_OFFSET)
}
pub fn new_with_offset(year: i16, week: u8, week_start_offset: i8) -> Result<Self, OffsetIsoWeekCreationError> {
let iso_weeks_in_year = iso_weeks_in_year(year).or(Err(OffsetIsoWeekCreationError::OutOfRangeYear))?;
if !(1..=iso_weeks_in_year).contains(&week) {
return Err(OffsetIsoWeekCreationError::OutOfRangeWeek);
}
if !Self::ALLOWED_OFFSET_RANGE.contains(&week_start_offset) {
return Err(OffsetIsoWeekCreationError::OutOfRangeOffset);
}
Ok(Self::unchecked_new_with_offset(year, week, week_start_offset))
}
pub fn from_date(date: Date) -> Result<Self, OffsetIsoWeekCreationError> {
Self::from_date_with_offset(date, Self::ISO_OFFSET)
}
pub fn from_date_with_offset(date: Date, week_start_offset: i8) -> Result<Self, OffsetIsoWeekCreationError> {
if !Self::ALLOWED_OFFSET_RANGE.contains(&week_start_offset) {
return Err(OffsetIsoWeekCreationError::OutOfRangeOffset);
}
let iso_week_date = date.iso_week_date();
let iso_week = iso_week_date.week().unsigned_abs();
let iso_week_year = iso_week_date.year();
let (resulting_year, resulting_week) = match week_start_offset.signum() {
0 => (iso_week_year, iso_week),
1 => {
let offset_since_monday = iso_week_date.date().weekday().since(Weekday::Monday);
if week_start_offset > offset_since_monday {
if iso_week == 1 {
let resulting_year = iso_week_year
.checked_sub(1)
.ok_or(OffsetIsoWeekCreationError::OutOfRangeYear)?;
let resulting_week =
iso_weeks_in_year(resulting_year).or(Err(OffsetIsoWeekCreationError::OutOfRangeYear))?;
(resulting_year, resulting_week)
} else {
(iso_week_year, iso_week.saturating_sub(1))
}
} else {
(iso_week_year, iso_week)
}
},
-1 => {
let offset_until_monday = iso_week_date.date().weekday().until(Weekday::Monday).neg();
if week_start_offset < offset_until_monday {
let iso_weeks_in_year =
iso_weeks_in_year(iso_week_year).or(Err(OffsetIsoWeekCreationError::OutOfRangeYear))?;
if iso_week == iso_weeks_in_year {
(
iso_week_year
.checked_add(1)
.ok_or(OffsetIsoWeekCreationError::OutOfRangeYear)?,
1,
)
} else {
(iso_week_year, iso_week + 1)
}
} else {
(iso_week_year, iso_week)
}
},
_ => unreachable!("core::num::signum is guaranteed to return only in the range -1..=1"),
};
Ok(Self::unchecked_new_with_offset(
resulting_year,
resulting_week,
week_start_offset,
))
}
#[must_use]
pub fn week(&self) -> u8 {
self.week
}
#[must_use]
pub fn year(&self) -> i16 {
self.year
}
#[must_use]
pub fn week_start_offset(&self) -> i8 {
self.week_start_offset
}
#[must_use]
pub fn start_weekday(&self) -> Weekday {
Weekday::Monday.wrapping_add(self.week_start_offset())
}
#[must_use]
pub fn end_weekday(&self) -> Weekday {
Weekday::Sunday.wrapping_add(self.week_start_offset())
}
pub fn zero_based_nth_day(&self, n: u8) -> Result<Date, OffsetIsoWeekDateError> {
if n > (DAYS_IN_WEEK - 1) {
return Err(OffsetIsoWeekDateError);
}
ISOWeekDate::new(
self.year(),
i8::try_from(self.week()).or(Err(OffsetIsoWeekDateError))?,
Weekday::from_monday_zero_offset(i8::try_from(n).or(Err(OffsetIsoWeekDateError))?)
.or(Err(OffsetIsoWeekDateError))?,
)
.or(Err(OffsetIsoWeekDateError))?
.date()
.checked_add(
Span::new()
.try_days(self.week_start_offset())
.or(Err(OffsetIsoWeekDateError))?,
)
.or(Err(OffsetIsoWeekDateError))
}
pub fn one_based_nth_day(&self, n: u8) -> Result<Date, OffsetIsoWeekDateError> {
self.zero_based_nth_day(n.checked_sub(1).ok_or(OffsetIsoWeekDateError)?)
}
pub fn first_day(&self) -> Result<Date, OffsetIsoWeekDateError> {
self.zero_based_nth_day(0)
}
pub fn last_day(&self) -> Result<Date, OffsetIsoWeekDateError> {
self.zero_based_nth_day(DAYS_IN_WEEK - 1)
}
pub fn weekday_date(&self, weekday: Weekday) -> Result<Date, OffsetIsoWeekDateError> {
self.zero_based_nth_day(u8::try_from(weekday.since(self.start_weekday())).or(Err(OffsetIsoWeekDateError))?)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum OffsetIsoWeekCreationError {
OutOfRangeYear,
OutOfRangeWeek,
OutOfRangeOffset,
Computation,
}
impl Display for OffsetIsoWeekCreationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::OutOfRangeYear => write!(f, "Out of range year"),
Self::OutOfRangeWeek => write!(f, "Out of range week number"),
Self::OutOfRangeOffset => write!(f, "Out of range week start offset"),
Self::Computation => write!(f, "An error occurred when computing the `OffsetIsoWeek`"),
}
}
}
impl Error for OffsetIsoWeekCreationError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct OffsetIsoWeekDateError;
impl Display for OffsetIsoWeekDateError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Conversion of the OffsetIsoWeek to a Date failed")
}
}
impl Error for OffsetIsoWeekDateError {}
impl TryFrom<Date> for OffsetIsoWeek {
type Error = OffsetIsoWeekCreationError;
fn try_from(value: Date) -> Result<Self, Self::Error> {
Self::from_date(value)
}
}
impl TryFrom<(Date, i8)> for OffsetIsoWeek {
type Error = OffsetIsoWeekCreationError;
fn try_from((date, week_start_offset): (Date, i8)) -> Result<Self, Self::Error> {
Self::from_date_with_offset(date, week_start_offset)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Month {
January,
February,
March,
April,
May,
June,
July,
August,
September,
October,
November,
December,
}
impl Month {
pub fn try_from_zero_offset(x: u8) -> Result<Self, MonthTryFromNumberError> {
match x {
0 => Ok(Self::January),
1 => Ok(Self::February),
2 => Ok(Self::March),
3 => Ok(Self::April),
4 => Ok(Self::May),
5 => Ok(Self::June),
6 => Ok(Self::July),
7 => Ok(Self::August),
8 => Ok(Self::September),
9 => Ok(Self::October),
10 => Ok(Self::November),
11 => Ok(Self::December),
_ => Err(MonthTryFromNumberError),
}
}
pub fn try_from_one_offset(x: u8) -> Result<Self, MonthTryFromNumberError> {
Self::try_from_zero_offset(x.checked_sub(1).ok_or(MonthTryFromNumberError)?)
}
#[must_use]
pub fn with_year(self, year: i16) -> MonthInYear {
MonthInYear::new(year, self)
}
#[must_use]
pub fn zero_offset_number(&self) -> u8 {
match self {
Self::January => 0,
Self::February => 1,
Self::March => 2,
Self::April => 3,
Self::May => 4,
Self::June => 5,
Self::July => 6,
Self::August => 7,
Self::September => 8,
Self::October => 9,
Self::November => 10,
Self::December => 11,
}
}
#[must_use]
pub fn one_offset_number(&self) -> u8 {
self.zero_offset_number().saturating_add(1)
}
}
impl From<MonthInYear> for Month {
fn from(value: MonthInYear) -> Self {
value.month()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct MonthTryFromDateError;
impl Display for MonthTryFromDateError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "An error occurred when trying to convert `Date` into `Month`")
}
}
impl Error for MonthTryFromDateError {}
impl TryFrom<Date> for Month {
type Error = MonthTryFromDateError;
fn try_from(value: Date) -> Result<Self, Self::Error> {
Self::try_from_one_offset(u8::try_from(value.month()).or(Err(MonthTryFromDateError))?)
.or(Err(MonthTryFromDateError))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct MonthTryFromNumberError;
impl Display for MonthTryFromNumberError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Conversion from a number to a Month failed")
}
}
impl Error for MonthTryFromNumberError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct MonthInYear {
year: i16,
month: Month,
}
impl MonthInYear {
#[must_use]
pub fn new(year: i16, month: Month) -> Self {
Self {
year,
month,
}
}
#[must_use]
pub fn year(&self) -> i16 {
self.year
}
#[must_use]
pub fn month(&self) -> Month {
self.month
}
pub fn first_day(&self) -> Result<Date, MonthInYearDateError> {
Date::new(
self.year(),
i8::try_from(self.month().one_offset_number()).or(Err(MonthInYearDateError::OutOfRangeMonth))?,
1,
)
.or(Err(MonthInYearDateError::OutOfRangeYear))
}
pub fn last_day(&self) -> Result<Date, MonthInYearDateError> {
self.first_day().map(Date::last_of_month)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum MonthInYearDateError {
OutOfRangeYear,
OutOfRangeMonth,
}
impl Display for MonthInYearDateError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::OutOfRangeYear => write!(f, "Out of range year"),
Self::OutOfRangeMonth => write!(f, "Out of range month"),
}
}
}
impl Error for MonthInYearDateError {}
impl PartialOrd for MonthInYear {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for MonthInYear {
fn cmp(&self, other: &Self) -> Ordering {
self.year()
.cmp(&other.year())
.then_with(|| self.month().cmp(&other.month()))
}
}
impl TryFrom<Date> for MonthInYear {
type Error = MonthTryFromNumberError;
fn try_from(value: Date) -> Result<Self, Self::Error> {
Ok(MonthInYear::new(
value.year(),
Month::try_from_one_offset(value.month().unsigned_abs())?,
))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CalendarAnchorOffset {
Days(i64),
Weeks(i64, Weekday),
IsoWeeks(i64),
Months(i64),
Years(i32),
}
impl CalendarAnchorOffset {
#[must_use]
pub fn is_zero(&self) -> bool {
match self {
Self::Days(x) | Self::Weeks(x, _) | Self::IsoWeeks(x) | Self::Months(x) => *x == 0,
Self::Years(x) => *x == 0,
}
}
#[must_use]
pub fn is_positive(&self) -> bool {
match self {
Self::Days(x) | Self::Weeks(x, _) | Self::IsoWeeks(x) | Self::Months(x) => x.is_positive(),
Self::Years(x) => x.is_positive(),
}
}
#[must_use]
pub fn is_negative(&self) -> bool {
match self {
Self::Days(x) | Self::Weeks(x, _) | Self::IsoWeeks(x) | Self::Months(x) => x.is_negative(),
Self::Years(x) => x.is_negative(),
}
}
}
impl PartialOrd for CalendarAnchorOffset {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
match (self, other) {
(Self::Days(x), Self::Days(y))
| (Self::Weeks(x, _), Self::Weeks(y, _))
| (Self::Months(x), Self::Months(y)) => Some(x.cmp(y)),
(Self::Years(x), Self::Years(y)) => Some(x.cmp(y)),
_ => None,
}
}
}
pub fn checked_add_calendar_week_offset_to_date(
weeks_offset: i64,
week_start: Weekday,
date: Date,
) -> Result<Date, CalendarAnchorOffsetDateError> {
date.checked_sub(Span::new().days(week_start.until(date.weekday())))
.or(Err(CalendarAnchorOffsetDateError::OutOfRangeResult))?
.checked_add(
Span::new()
.try_weeks(weeks_offset)
.or(Err(CalendarAnchorOffsetDateError::OffsetTooLarge))?,
)
.or(Err(CalendarAnchorOffsetDateError::OutOfRangeResult))
}
pub fn checked_sub_calendar_week_offset_to_date(
weeks_offset: i64,
week_start: Weekday,
date: Date,
) -> Result<Date, CalendarAnchorOffsetDateError> {
date.checked_sub(Span::new().days(week_start.until(date.weekday())))
.or(Err(CalendarAnchorOffsetDateError::OutOfRangeResult))?
.checked_sub(
Span::new()
.try_weeks(weeks_offset)
.or(Err(CalendarAnchorOffsetDateError::OffsetTooLarge))?,
)
.or(Err(CalendarAnchorOffsetDateError::OutOfRangeResult))
}
pub fn checked_add_calendar_anchor_offset_to_date(
calendar_anchor_offset: CalendarAnchorOffset,
date: Date,
) -> Result<Date, CalendarAnchorOffsetDateError> {
match calendar_anchor_offset {
CalendarAnchorOffset::Days(days_offset) => date
.checked_add(
Span::new()
.try_days(days_offset)
.or(Err(CalendarAnchorOffsetDateError::OffsetTooLarge))?,
)
.or(Err(CalendarAnchorOffsetDateError::OutOfRangeResult)),
CalendarAnchorOffset::Weeks(weeks_offset, week_start) => {
checked_add_calendar_week_offset_to_date(weeks_offset, week_start, date)
},
CalendarAnchorOffset::IsoWeeks(weeks_offset) => {
checked_add_calendar_week_offset_to_date(weeks_offset, Weekday::Monday, date)
},
CalendarAnchorOffset::Months(months_offset) => Ok(date
.checked_add(
Span::new()
.try_months(months_offset)
.or(Err(CalendarAnchorOffsetDateError::OffsetTooLarge))?,
)
.or(Err(CalendarAnchorOffsetDateError::OutOfRangeResult))?
.first_of_month()),
CalendarAnchorOffset::Years(years_offset) => Ok(date
.checked_add(
Span::new()
.try_years(years_offset)
.or(Err(CalendarAnchorOffsetDateError::OffsetTooLarge))?,
)
.or(Err(CalendarAnchorOffsetDateError::OutOfRangeResult))?
.first_of_year()),
}
}
pub fn checked_sub_calendar_anchor_offset_to_date(
calendar_anchor_offset: CalendarAnchorOffset,
date: Date,
) -> Result<Date, CalendarAnchorOffsetDateError> {
match calendar_anchor_offset {
CalendarAnchorOffset::Days(days_offset) => date
.checked_sub(
Span::new()
.try_days(days_offset)
.or(Err(CalendarAnchorOffsetDateError::OffsetTooLarge))?,
)
.or(Err(CalendarAnchorOffsetDateError::OutOfRangeResult)),
CalendarAnchorOffset::Weeks(weeks_offset, week_start) => {
checked_sub_calendar_week_offset_to_date(weeks_offset, week_start, date)
},
CalendarAnchorOffset::IsoWeeks(weeks_offset) => {
checked_sub_calendar_week_offset_to_date(weeks_offset, Weekday::Monday, date)
},
CalendarAnchorOffset::Months(months_offset) => Ok(date
.checked_sub(
Span::new()
.try_months(months_offset)
.or(Err(CalendarAnchorOffsetDateError::OffsetTooLarge))?,
)
.or(Err(CalendarAnchorOffsetDateError::OutOfRangeResult))?
.first_of_month()),
CalendarAnchorOffset::Years(years_offset) => Ok(date
.checked_sub(
Span::new()
.try_years(years_offset)
.or(Err(CalendarAnchorOffsetDateError::OffsetTooLarge))?,
)
.or(Err(CalendarAnchorOffsetDateError::OutOfRangeResult))?
.first_of_year()),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CalendarAnchorOffsetDateError {
OffsetTooLarge,
OutOfRangeResult,
}
impl Display for CalendarAnchorOffsetDateError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::OffsetTooLarge => write!(f, "Calendar anchor offset was too large to be applied"),
Self::OutOfRangeResult => write!(f, "Out of range result"),
}
}
}
impl Error for CalendarAnchorOffsetDateError {}