use crate::NaiveDate;
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub enum CalendarPeriod {
Day,
Week,
Month,
Quarter,
Year,
}
#[must_use]
pub const fn quarter_index0(month1: u32) -> u32 {
debug_assert!(month1 >= 1);
(month1 - 1) / 3
}
impl CalendarPeriod {
#[must_use]
pub fn start_of(self, day: NaiveDate) -> NaiveDate {
match self {
Self::Day => day,
Self::Week => day
.checked_sub(
jiff::Span::new().days(i64::from(day.weekday().to_monday_zero_offset())),
)
.unwrap_or(day),
Self::Month => NaiveDate::new(day.year(), day.month(), 1).unwrap_or(day),
Self::Quarter => {
let month1 = quarter_index0(u32::from(day.month().unsigned_abs())) * 3 + 1;
i8::try_from(month1)
.ok()
.and_then(|m| NaiveDate::new(day.year(), m, 1).ok())
.unwrap_or(day)
}
Self::Year => NaiveDate::new(day.year(), 1, 1).unwrap_or(day),
}
}
#[must_use]
pub fn next_start(self, start: NaiveDate) -> Option<NaiveDate> {
let span = match self {
Self::Day => jiff::Span::new().days(1),
Self::Week => jiff::Span::new().days(7),
Self::Month => jiff::Span::new().months(1),
Self::Quarter => jiff::Span::new().months(3),
Self::Year => jiff::Span::new().years(1),
};
start.checked_add(span).ok()
}
#[must_use]
pub fn period_days(self, start: NaiveDate) -> i64 {
let days_between = |a: NaiveDate, b: NaiveDate| {
i64::from(a.until((jiff::Unit::Day, b)).map_or(0, |s| s.get_days()))
};
if let Some(next) = self.next_start(start) {
return days_between(start, next);
}
match self {
Self::Day => 1,
Self::Week => 7,
Self::Month | Self::Quarter | Self::Year => days_between(start, NaiveDate::MAX) + 1,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::naive_date;
fn d(y: i32, m: u32, day: u32) -> NaiveDate {
naive_date(y, m, day).unwrap()
}
#[test]
fn quarters_anchor_to_jan_apr_jul_oct() {
for (month, want_month) in [
(1, 1),
(2, 1),
(3, 1),
(4, 4),
(5, 4),
(6, 4),
(7, 7),
(8, 7),
(9, 7),
(10, 10),
(11, 10),
(12, 10),
] {
assert_eq!(
CalendarPeriod::Quarter.start_of(d(2024, month, 15)),
d(2024, want_month, 1),
"month {month}"
);
}
}
#[test]
fn weeks_anchor_to_iso_monday() {
assert_eq!(CalendarPeriod::Week.start_of(d(2024, 3, 7)), d(2024, 3, 4));
assert_eq!(CalendarPeriod::Week.start_of(d(2024, 3, 4)), d(2024, 3, 4));
assert_eq!(
CalendarPeriod::Week.start_of(d(2025, 1, 1)),
d(2024, 12, 30)
);
}
#[test]
fn next_start_is_none_past_the_representable_range() {
let last = NaiveDate::MAX;
assert_eq!(CalendarPeriod::Year.next_start(last), None);
assert_eq!(CalendarPeriod::Month.next_start(last), None);
}
#[test]
fn month_and_year_truncate_and_advance() {
assert_eq!(
CalendarPeriod::Month.start_of(d(2024, 2, 29)),
d(2024, 2, 1)
);
assert_eq!(
CalendarPeriod::Year.start_of(d(2024, 12, 31)),
d(2024, 1, 1)
);
let feb = CalendarPeriod::Month.start_of(d(2024, 2, 10));
assert_eq!(CalendarPeriod::Month.next_start(feb), Some(d(2024, 3, 1)));
let y = CalendarPeriod::Year.start_of(d(2024, 6, 1));
assert_eq!(CalendarPeriod::Year.next_start(y), Some(d(2025, 1, 1)));
}
}