Files
Stalwart/crates/utils/src/cron.rs
mdecimus c6495f6fed RIP TOML
2026-03-11 17:00:39 +01:00

139 lines
4.9 KiB
Rust

/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use chrono::{Datelike, Local, TimeDelta, TimeZone, Timelike};
use std::{str::FromStr, time::Duration};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SimpleCron {
Day { hour: u32, minute: u32 },
Week { day: u32, hour: u32, minute: u32 },
Hour { minute: u32 },
}
impl SimpleCron {
pub fn time_to_next(&self) -> Duration {
let now = Local::now();
let next = match self {
SimpleCron::Day { hour, minute } => {
let next = Local
.with_ymd_and_hms(now.year(), now.month(), now.day(), *hour, *minute, 0)
.earliest()
.unwrap_or_else(|| now - TimeDelta::try_seconds(1).unwrap_or_default());
if next <= now {
next + TimeDelta::try_days(1).unwrap_or_default()
} else {
next
}
}
SimpleCron::Week { day, hour, minute } => {
let next = Local
.with_ymd_and_hms(now.year(), now.month(), now.day(), *hour, *minute, 0)
.earliest()
.unwrap_or_else(|| now - TimeDelta::try_seconds(1).unwrap_or_default());
if next <= now {
next + TimeDelta::try_days(
(7 - now.weekday().number_from_monday() + *day).into(),
)
.unwrap_or_default()
} else {
next
}
}
SimpleCron::Hour { minute } => {
let next = Local
.with_ymd_and_hms(now.year(), now.month(), now.day(), now.hour(), *minute, 0)
.earliest()
.unwrap_or_else(|| now - TimeDelta::try_seconds(1).unwrap_or_default());
if next <= now {
next + TimeDelta::try_hours(1).unwrap_or_default()
} else {
next
}
}
};
(next - now).to_std().unwrap_or_else(|_| self.as_duration())
}
pub fn as_duration(&self) -> Duration {
match self {
SimpleCron::Day { .. } => Duration::from_secs(24 * 60 * 60),
SimpleCron::Week { .. } => Duration::from_secs(7 * 24 * 60 * 60),
SimpleCron::Hour { .. } => Duration::from_secs(60 * 60),
}
}
}
impl FromStr for SimpleCron {
type Err = String;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let mut hour = 0;
let mut minute = 0;
for (pos, value) in value.split(' ').enumerate() {
if pos == 0 {
minute = value
.parse::<u32>()
.map_err(|_| "Invalid cron key: failed to parse cron minute".to_string())?;
if !(0..=59).contains(&minute) {
return Err(format!(
"Invalid cron key: failed to parse minute, invalid value: {minute}"
));
}
} else if pos == 1 {
if value
.as_bytes()
.first()
.ok_or_else(|| "Invalid cron key: failed to parse cron hour".to_string())?
== &b'*'
{
return Ok(SimpleCron::Hour { minute });
} else {
hour = value
.parse::<u32>()
.map_err(|_| "Invalid cron key: failed to parse cron hour".to_string())?;
if !(0..=23).contains(&hour) {
return Err(format!(
"Invalid cron key: failed to parse hour, invalid value: {hour}"
));
}
}
} else if pos == 2 {
if value
.as_bytes()
.first()
.ok_or_else(|| "Invalid cron key: failed to parse cron weekday".to_string())?
== &b'*'
{
return Ok(SimpleCron::Day { hour, minute });
} else {
let day = value.parse::<u32>().map_err(|_| {
"Invalid cron key: failed to parse cron weekday".to_string()
})?;
if !(1..=7).contains(&hour) {
return Err(format!(
"Invalid cron key: failed to parse weekday, invalid value: {}, range is 1 (Monday) to 7 (Sunday).",
hour,
));
}
return Ok(SimpleCron::Week { day, hour, minute });
}
}
}
Err("Invalid cron key: parse cron expression.".to_string())
}
}
impl Default for SimpleCron {
fn default() -> Self {
SimpleCron::Hour { minute: 0 }
}
}