use crate::time::{days_in_month, days_in_year, CivilDateTime, CivilTime, CronDateTime, Weekday};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Cursor {
civil: CivilDateTime,
weekday: Weekday,
}
impl Cursor {
pub(crate) fn new<T: CronDateTime>(start: &T) -> Cursor {
Cursor {
civil: start.to_civil(),
weekday: start.civil_weekday(),
}
}
pub(crate) const fn civil(self) -> CivilDateTime {
self.civil
}
pub(crate) const fn weekday(self) -> Weekday {
self.weekday
}
pub(crate) const fn time(self) -> CivilTime {
self.civil.time()
}
pub(crate) const fn year(self) -> i32 {
self.civil.year()
}
pub(crate) const fn month(self) -> u32 {
self.civil.month()
}
pub(crate) const fn day(self) -> u32 {
self.civil.day()
}
pub(crate) const fn hour(self) -> u32 {
self.civil.hour()
}
pub(crate) const fn minute(self) -> u32 {
self.civil.minute()
}
pub(crate) const fn second(self) -> u32 {
self.civil.second()
}
pub(crate) const fn with_time(self, time: CivilTime) -> Cursor {
Cursor {
civil: self.civil.with_time(time),
weekday: self.weekday,
}
}
pub(crate) fn checked_add_seconds(self, seconds: i64) -> Option<Cursor> {
let (civil, days) = self.civil.checked_add_seconds(seconds)?;
Some(Cursor {
civil,
weekday: self.weekday.shift(i32::try_from(days).ok()?),
})
}
pub(crate) fn checked_add_days(self, days: i64) -> Option<Cursor> {
Some(Cursor {
civil: CivilDateTime::new(self.civil.date().checked_add_days(days)?, self.civil.time()),
weekday: self.weekday.shift(i32::try_from(days).ok()?),
})
}
pub(crate) fn start_of_next_year(self) -> Option<Cursor> {
let to_year_end = days_in_year(self.year()) - self.civil.date().day_of_year();
self.checked_add_days(i64::from(to_year_end) + 1)
.map(|cursor| cursor.with_time(CivilTime::MIDNIGHT))
}
pub(crate) fn end_of_previous_year(self) -> Option<Cursor> {
self.checked_add_days(-i64::from(self.civil.date().day_of_year()))
.map(|cursor| cursor.with_time(CivilTime::END_OF_DAY))
}
pub(crate) fn start_of_next_month(self) -> Option<Cursor> {
let to_month_end = days_in_month(self.year(), self.month()) - self.day();
self.checked_add_days(i64::from(to_month_end) + 1)
.map(|cursor| cursor.with_time(CivilTime::MIDNIGHT))
}
pub(crate) fn end_of_previous_month(self) -> Option<Cursor> {
self.checked_add_days(-i64::from(self.day()))
.map(|cursor| cursor.with_time(CivilTime::END_OF_DAY))
}
pub(crate) fn resolve_in<T: CronDateTime>(
self,
origin: &T,
) -> Result<super::Resolution<T>, crate::errors::CronError> {
origin.resolve_civil(self.civil)
}
}
impl core::fmt::Display for Cursor {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{}", self.civil)
}
}
#[cfg(test)]
mod tests {
use chrono::{Datelike, NaiveDate};
use super::*;
fn cursor(year: i32, month: u32, day: u32, hour: u32, minute: u32, second: u32) -> Cursor {
let date = NaiveDate::from_ymd_opt(year, month, day).expect("test date must exist");
Cursor {
civil: CivilDateTime::from_ymd_hms(year, month, day, hour, minute, second).unwrap(),
weekday: Weekday::from_days_from_sunday(date.weekday().num_days_from_sunday()),
}
}
#[track_caller]
fn assert_agrees_with_calendar(actual: Cursor) {
let expected = cursor(
actual.year(),
actual.month(),
actual.day(),
actual.hour(),
actual.minute(),
actual.second(),
);
assert_eq!(actual, expected, "weekday drifted at {actual}");
}
#[test]
fn seconds_carry_into_the_next_and_previous_day() {
let end_of_year = cursor(2023, 12, 31, 23, 59, 59);
let new_year = end_of_year.checked_add_seconds(1).unwrap();
assert_eq!(new_year.civil(), cursor(2024, 1, 1, 0, 0, 0).civil());
assert_agrees_with_calendar(new_year);
assert_agrees_with_calendar(new_year.checked_add_seconds(-1).unwrap());
assert_agrees_with_calendar(new_year.checked_add_seconds(-3600).unwrap());
}
#[test]
fn month_and_year_moves_keep_the_weekday_in_step() {
for year in [2024, 2025] {
let mut day = cursor(year, 1, 1, 12, 0, 0);
while day.year() == year {
assert_agrees_with_calendar(day);
assert_agrees_with_calendar(day.start_of_next_month().unwrap());
assert_agrees_with_calendar(day.end_of_previous_month().unwrap());
assert_agrees_with_calendar(day.start_of_next_year().unwrap());
assert_agrees_with_calendar(day.end_of_previous_year().unwrap());
day = day.checked_add_days(1).unwrap();
}
}
}
#[test]
fn moves_land_on_the_expected_dates() {
let mid_february = cursor(2024, 2, 15, 8, 30, 15);
assert_eq!(
mid_february.start_of_next_month().unwrap().civil(),
cursor(2024, 3, 1, 0, 0, 0).civil()
);
assert_eq!(
mid_february.end_of_previous_month().unwrap().civil(),
cursor(2024, 1, 31, 23, 59, 59).civil()
);
assert_eq!(
mid_february.start_of_next_year().unwrap().civil(),
cursor(2025, 1, 1, 0, 0, 0).civil()
);
assert_eq!(
mid_february.end_of_previous_year().unwrap().civil(),
cursor(2023, 12, 31, 23, 59, 59).civil()
);
assert_eq!(
cursor(2024, 2, 28, 0, 0, 0)
.checked_add_days(1)
.unwrap()
.civil(),
cursor(2024, 2, 29, 0, 0, 0).civil()
);
assert_eq!(
cursor(2023, 2, 28, 0, 0, 0)
.checked_add_days(1)
.unwrap()
.civil(),
cursor(2023, 3, 1, 0, 0, 0).civil()
);
}
#[test]
fn day_steps_cross_months_and_years() {
let mut walked = cursor(2023, 11, 1, 0, 0, 0);
let mut expected = NaiveDate::from_ymd_opt(2023, 11, 1).unwrap();
for _ in 0..120 {
assert_eq!(
(walked.year(), walked.month(), walked.day()),
(expected.year(), expected.month(), expected.day())
);
assert_agrees_with_calendar(walked);
walked = walked.checked_add_days(1).unwrap();
expected = expected.succ_opt().unwrap();
}
for _ in 0..120 {
walked = walked.checked_add_days(-1).unwrap();
expected = expected.pred_opt().unwrap();
assert_eq!(
(walked.year(), walked.month(), walked.day()),
(expected.year(), expected.month(), expected.day())
);
assert_agrees_with_calendar(walked);
}
}
}