139 lines
4.9 KiB
Rust
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 }
|
|
}
|
|
}
|