use std::time::Duration;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ScheduleType {
Once(Duration),
Interval(Duration),
Daily { hour: u8, minute: u8 },
Weekly { day: u8, hour: u8, minute: u8 },
Cron(String),
}
#[derive(Debug, Clone)]
enum CronField {
Any,
Value(u32),
Step(u32),
Range(u32, u32),
}
impl CronField {
fn parse(s: &str) -> Option<Self> {
if s == "*" {
return Some(CronField::Any);
}
if let Some(step) = s.strip_prefix("*/") {
return step.parse::<u32>().ok().map(CronField::Step);
}
if let Some((a, b)) = s.split_once('-') {
let start = a.parse::<u32>().ok()?;
let end = b.parse::<u32>().ok()?;
return Some(CronField::Range(start, end));
}
s.parse::<u32>().ok().map(CronField::Value)
}
fn matches(&self, current: u32) -> bool {
match self {
CronField::Any => true,
CronField::Value(v) => current == *v,
CronField::Step(step) => *step > 0 && current.is_multiple_of(*step),
CronField::Range(start, end) => current >= *start && current <= *end,
}
}
fn _next_match(&self, current: u32, wrap_at: u32) -> u32 {
for candidate in current..wrap_at {
if self.matches(candidate) {
return candidate;
}
}
for candidate in 0..wrap_at {
if self.matches(candidate) {
return candidate;
}
}
current
}
}
#[derive(Debug, Clone)]
struct CronExpr {
minute: CronField,
hour: CronField,
day: CronField,
month: CronField,
weekday: CronField,
}
impl CronExpr {
fn parse(expr: &str) -> Option<Self> {
let parts: Vec<&str> = expr.split_whitespace().collect();
if parts.len() != 5 {
return None;
}
Some(Self {
minute: CronField::parse(parts[0])?,
hour: CronField::parse(parts[1])?,
day: CronField::parse(parts[2])?,
month: CronField::parse(parts[3])?,
weekday: CronField::parse(parts[4])?,
})
}
fn next_run_after(&self, now_secs: u64) -> u64 {
let secs_per_min = 60u64;
let secs_per_hour = 3600u64;
let secs_per_day = 86400u64;
let start = now_secs + 60 - (now_secs % 60);
let max_iterations = 366 * 24 * 60;
let mut candidate = start;
for _ in 0..max_iterations {
let total_minutes = candidate / secs_per_min;
let minute = (total_minutes % 60) as u32;
let total_hours = candidate / secs_per_hour;
let hour = (total_hours % 24) as u32;
let days_since_epoch = (candidate / secs_per_day) as u32;
let weekday = (days_since_epoch + 4) % 7; let (_, month, day) = Self::days_to_ymd(days_since_epoch);
if self.minute.matches(minute)
&& self.hour.matches(hour)
&& self.day.matches(day)
&& self.month.matches(month)
&& self.weekday.matches(weekday)
{
return candidate;
}
candidate += secs_per_min; }
now_secs + secs_per_hour
}
fn days_to_ymd(days: u32) -> (u32, u32, u32) {
let mut y = 1970u32;
let mut remaining = days;
loop {
let days_in_year = if Self::is_leap(y) { 366 } else { 365 };
if remaining < days_in_year {
break;
}
remaining -= days_in_year;
y += 1;
}
let leap = Self::is_leap(y);
let month_days: [u32; 12] = [
31,
if leap { 29 } else { 28 },
31,
30,
31,
30,
31,
31,
30,
31,
30,
31,
];
let mut m = 1u32;
for &md in &month_days {
if remaining < md {
break;
}
remaining -= md;
m += 1;
}
(y, m, remaining + 1)
}
fn is_leap(y: u32) -> bool {
(y.is_multiple_of(4) && !y.is_multiple_of(100)) || y.is_multiple_of(400)
}
}
#[derive(Debug, Clone)]
pub struct Schedule {
pub schedule_type: ScheduleType,
pub next_run: u64,
}
impl Schedule {
pub fn new(schedule_type: ScheduleType) -> Self {
let next_run = Self::calculate_next_run(&schedule_type);
Self {
schedule_type,
next_run,
}
}
fn calculate_next_run(schedule_type: &ScheduleType) -> u64 {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
match schedule_type {
ScheduleType::Once(delay) => now + delay.as_secs(),
ScheduleType::Interval(interval) => now + interval.as_secs(),
ScheduleType::Daily { hour, minute } => {
let seconds_in_day = 24 * 60 * 60;
let target_seconds = (*hour as u64) * 3600 + (*minute as u64) * 60;
let current_seconds = now % seconds_in_day;
if current_seconds < target_seconds {
now + (target_seconds - current_seconds)
} else {
now + (seconds_in_day - current_seconds + target_seconds)
}
}
ScheduleType::Weekly {
day: _,
hour,
minute,
} => {
let seconds_in_week = 7 * 24 * 60 * 60;
let target_seconds = (*hour as u64) * 3600 + (*minute as u64) * 60;
now + seconds_in_week + target_seconds
}
ScheduleType::Cron(expr) => {
if let Some(cron) = CronExpr::parse(expr) {
cron.next_run_after(now)
} else {
now + 3600
}
}
}
}
pub fn update(&mut self) {
self.next_run = match &self.schedule_type {
ScheduleType::Once(_) => self.next_run,
ScheduleType::Interval(interval) => self.next_run + interval.as_secs(),
_ => Self::calculate_next_run(&self.schedule_type),
};
}
pub fn is_ready(&self) -> bool {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
now >= self.next_run
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_schedule_once() {
let schedule = Schedule::new(ScheduleType::Once(Duration::from_secs(10)));
assert!(!schedule.is_ready());
}
#[test]
fn test_schedule_interval() {
let mut schedule = Schedule::new(ScheduleType::Interval(Duration::from_secs(60)));
let first_run = schedule.next_run;
schedule.update();
assert_eq!(schedule.next_run, first_run + 60);
}
#[test]
fn test_schedule_daily() {
let schedule = Schedule::new(ScheduleType::Daily { hour: 9, minute: 0 });
assert!(schedule.next_run > 0);
}
#[test]
fn test_cron_field_parse() {
assert!(matches!(CronField::parse("*"), Some(CronField::Any)));
assert!(matches!(CronField::parse("*/5"), Some(CronField::Step(5))));
assert!(matches!(CronField::parse("30"), Some(CronField::Value(30))));
assert!(matches!(
CronField::parse("1-5"),
Some(CronField::Range(1, 5))
));
}
#[test]
fn test_cron_field_matches() {
assert!(CronField::Any.matches(42));
assert!(CronField::Value(5).matches(5));
assert!(!CronField::Value(5).matches(6));
assert!(CronField::Step(5).matches(0));
assert!(CronField::Step(5).matches(10));
assert!(!CronField::Step(5).matches(3));
assert!(CronField::Range(1, 5).matches(3));
assert!(!CronField::Range(1, 5).matches(6));
}
#[test]
fn test_cron_expr_parse() {
let cron = CronExpr::parse("*/5 * * * *");
assert!(cron.is_some());
let bad = CronExpr::parse("invalid");
assert!(bad.is_none());
}
#[test]
fn test_cron_next_run() {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
let cron = CronExpr::parse("* * * * *").unwrap();
let next = cron.next_run_after(now);
assert!(next > now);
assert!(next <= now + 60);
let cron = CronExpr::parse("0 * * * *").unwrap();
let next = cron.next_run_after(now);
assert!(next > now);
assert!(next <= now + 3600);
}
#[test]
fn test_cron_schedule_integration() {
let schedule = Schedule::new(ScheduleType::Cron("*/5 * * * *".to_string()));
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
assert!(schedule.next_run > now);
assert!(schedule.next_run <= now + 300);
}
#[test]
fn test_days_to_ymd() {
assert_eq!(CronExpr::days_to_ymd(0), (1970, 1, 1));
assert_eq!(CronExpr::days_to_ymd(10957), (2000, 1, 1));
}
}