#[cfg(feature = "chrono")]
#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
mod chrono_impl;
#[cfg(feature = "jiff")]
#[cfg_attr(docsrs, doc(cfg(feature = "jiff")))]
mod jiff_impl;
mod cursor;
mod fold;
pub(crate) use cursor::Cursor;
pub(crate) use fold::{fold_of, other_fold_edge, Fold};
use core::cmp::Ordering;
use crate::errors::CronError;
const SECONDS_PER_DAY: i64 = 86_400;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Weekday {
Sunday = 0,
Monday = 1,
Tuesday = 2,
Wednesday = 3,
Thursday = 4,
Friday = 5,
Saturday = 6,
}
impl Weekday {
pub const fn num_days_from_sunday(self) -> u32 {
self as u32
}
pub const fn from_days_from_sunday(days: u32) -> Weekday {
match days % 7 {
0 => Weekday::Sunday,
1 => Weekday::Monday,
2 => Weekday::Tuesday,
3 => Weekday::Wednesday,
4 => Weekday::Thursday,
5 => Weekday::Friday,
_ => Weekday::Saturday,
}
}
pub const fn shift(self, days: i32) -> Weekday {
Weekday::from_days_from_sunday((self as i32 + days).rem_euclid(7) as u32)
}
}
pub const fn is_leap_year(year: i32) -> bool {
(year % 4 == 0 && year % 100 != 0) || year % 400 == 0
}
pub const fn days_in_month(year: i32, month: u32) -> u32 {
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 if is_leap_year(year) => 29,
2 => 28,
_ => unreachable!(),
}
}
pub const fn days_in_year(year: i32) -> u32 {
if is_leap_year(year) {
366
} else {
365
}
}
pub const fn day_of_year(year: i32, month: u32, day: u32) -> u32 {
const BEFORE_MONTH: [u32; 12] = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
let leap_day = if month > 2 && is_leap_year(year) {
1
} else {
0
};
BEFORE_MONTH[(month - 1) as usize] + day + leap_day
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct CivilDate {
year: i32,
month: u8,
day: u8,
}
impl CivilDate {
pub fn from_ymd_opt(year: i32, month: u32, day: u32) -> Option<CivilDate> {
if month == 0 || month > 12 {
return None;
}
if day == 0 || day > days_in_month(year, month) {
return None;
}
Some(CivilDate {
year,
month: month as u8,
day: day as u8,
})
}
pub fn from_ymd(year: i32, month: u32, day: u32) -> Result<CivilDate, CronError> {
CivilDate::from_ymd_opt(year, month, day).ok_or(CronError::InvalidDate)
}
pub const fn from_parts_unchecked(year: i32, month: u32, day: u32) -> CivilDate {
CivilDate {
year,
month: month as u8,
day: day as u8,
}
}
pub const fn year(self) -> i32 {
self.year
}
pub const fn month(self) -> u32 {
self.month as u32
}
pub const fn day(self) -> u32 {
self.day as u32
}
pub const fn days_in_month(self) -> u32 {
days_in_month(self.year, self.month())
}
pub const fn day_of_year(self) -> u32 {
day_of_year(self.year, self.month(), self.day())
}
pub(crate) fn checked_add_days(self, days: i64) -> Option<CivilDate> {
let mut year = self.year;
let mut month = self.month();
let mut day = i64::from(self.day) + days;
while day > i64::from(days_in_month(year, month)) {
day -= i64::from(days_in_month(year, month));
month += 1;
if month > 12 {
month = 1;
year = year.checked_add(1)?;
}
}
while day < 1 {
if month == 1 {
month = 12;
year = year.checked_sub(1)?;
} else {
month -= 1;
}
day += i64::from(days_in_month(year, month));
}
Some(CivilDate {
year,
month: month as u8,
day: day as u8,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct CivilTime {
hour: u8,
minute: u8,
second: u8,
}
impl CivilTime {
pub const MIDNIGHT: CivilTime = CivilTime::from_parts_unchecked(0, 0, 0);
pub const END_OF_DAY: CivilTime = CivilTime::from_parts_unchecked(23, 59, 59);
pub fn from_hms_opt(hour: u32, minute: u32, second: u32) -> Option<CivilTime> {
if hour > 23 || minute > 59 || second > 59 {
return None;
}
Some(CivilTime::from_parts_unchecked(hour, minute, second))
}
pub const fn from_parts_unchecked(hour: u32, minute: u32, second: u32) -> CivilTime {
CivilTime {
hour: hour as u8,
minute: minute as u8,
second: second as u8,
}
}
pub const fn hour(self) -> u32 {
self.hour as u32
}
pub const fn minute(self) -> u32 {
self.minute as u32
}
pub const fn second(self) -> u32 {
self.second as u32
}
const fn seconds_of_day(self) -> i64 {
self.hour as i64 * 3600 + self.minute as i64 * 60 + self.second as i64
}
const fn from_seconds_of_day(seconds: i64) -> CivilTime {
CivilTime {
hour: (seconds / 3600) as u8,
minute: (seconds % 3600 / 60) as u8,
second: (seconds % 60) as u8,
}
}
}
impl core::fmt::Display for CivilTime {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{:02}:{:02}:{:02}", self.hour, self.minute, self.second)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct CivilDateTime {
date: CivilDate,
time: CivilTime,
}
impl CivilDateTime {
pub const fn new(date: CivilDate, time: CivilTime) -> CivilDateTime {
CivilDateTime { date, time }
}
pub fn from_ymd_hms_opt(
year: i32,
month: u32,
day: u32,
hour: u32,
minute: u32,
second: u32,
) -> Option<CivilDateTime> {
Some(CivilDateTime::new(
CivilDate::from_ymd_opt(year, month, day)?,
CivilTime::from_hms_opt(hour, minute, second)?,
))
}
pub fn from_ymd_hms(
year: i32,
month: u32,
day: u32,
hour: u32,
minute: u32,
second: u32,
) -> Result<CivilDateTime, CronError> {
Ok(CivilDateTime::new(
CivilDate::from_ymd(year, month, day)?,
CivilTime::from_hms_opt(hour, minute, second).ok_or(CronError::InvalidTime)?,
))
}
pub const fn date(self) -> CivilDate {
self.date
}
pub const fn time(self) -> CivilTime {
self.time
}
pub const fn year(self) -> i32 {
self.date.year()
}
pub const fn month(self) -> u32 {
self.date.month()
}
pub const fn day(self) -> u32 {
self.date.day()
}
pub const fn hour(self) -> u32 {
self.time.hour()
}
pub const fn minute(self) -> u32 {
self.time.minute()
}
pub const fn second(self) -> u32 {
self.time.second()
}
pub(crate) const fn with_time(self, time: CivilTime) -> CivilDateTime {
CivilDateTime { time, ..self }
}
pub(crate) fn checked_add_seconds(self, seconds: i64) -> Option<(CivilDateTime, i64)> {
let total = self.time.seconds_of_day().checked_add(seconds)?;
let days = total.div_euclid(SECONDS_PER_DAY);
let date = if days == 0 {
self.date
} else {
self.date.checked_add_days(days)?
};
let moved = CivilDateTime {
date,
time: CivilTime::from_seconds_of_day(total.rem_euclid(SECONDS_PER_DAY)),
};
Some((moved, days))
}
}
impl core::fmt::Display for CivilDate {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
}
}
impl core::fmt::Display for CivilDateTime {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{}T{}", self.date, self.time)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Resolution<T> {
Single(T),
Ambiguous(T, T),
Gap,
}
pub trait CronDateTime: Sized + Clone {
fn to_civil(&self) -> CivilDateTime;
fn civil_weekday(&self) -> Weekday;
fn resolve_civil(&self, civil: CivilDateTime) -> Result<Resolution<Self>, CronError>;
fn checked_add_seconds(&self, seconds: i64) -> Option<Self>;
fn cmp_instant(&self, other: &Self) -> Ordering;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn shift_moves_in_both_directions_and_wraps() {
assert_eq!(Weekday::Wednesday.shift(0), Weekday::Wednesday);
assert_eq!(Weekday::Wednesday.shift(1), Weekday::Thursday);
assert_eq!(Weekday::Wednesday.shift(-1), Weekday::Tuesday);
assert_eq!(Weekday::Saturday.shift(1), Weekday::Sunday);
assert_eq!(Weekday::Sunday.shift(-1), Weekday::Saturday);
assert_eq!(Weekday::Monday.shift(70), Weekday::Monday);
assert_eq!(Weekday::Monday.shift(-70), Weekday::Monday);
assert_eq!(Weekday::Friday.shift(31), Weekday::Monday);
}
#[test]
fn shift_agrees_with_stepping_one_day_at_a_time() {
let mut stepped = Weekday::Thursday;
for days in 0..400 {
assert_eq!(Weekday::Thursday.shift(days), stepped, "after {days} days");
assert_eq!(
Weekday::Thursday.shift(-days).shift(days),
Weekday::Thursday
);
stepped = stepped.shift(1);
}
}
#[test]
fn days_in_month_handles_leap_years() {
assert_eq!(days_in_month(2023, 2), 28);
assert_eq!(days_in_month(2024, 2), 29);
assert_eq!(days_in_month(2000, 2), 29);
assert_eq!(days_in_month(1900, 2), 28);
assert_eq!(days_in_month(2024, 4), 30);
assert_eq!(days_in_month(2024, 12), 31);
}
#[test]
#[should_panic]
fn days_in_month_rejects_a_month_above_the_range() {
days_in_month(2023, 13);
}
#[test]
#[should_panic]
fn days_in_month_rejects_a_month_below_the_range() {
days_in_month(2023, 0);
}
}