use crate::{
Interval, NextTime,
intervals::{RunConfig, parse_time},
timeprovider::{DefaultTimeProvider, TimeProvider}
};
use std::{
fmt::{self, Debug, Formatter},
marker::PhantomData
};
use time::{OffsetDateTime, Time, UtcOffset};
#[doc(hidden)]
pub trait WithSchedule<TP> {
fn schedule_mut(&mut self) -> &mut JobSchedule<TP>;
fn schedule(&self) -> &JobSchedule<TP>;
}
pub struct Repeating<'a, T, TP> {
job: &'a mut T,
interval: Interval,
_tp: PhantomData<TP>
}
impl<'a, T, TP> Repeating<'a, T, TP>
where
T: WithSchedule<TP>,
TP: TimeProvider
{
pub(crate) fn new(job: &'a mut T, interval: Interval) -> Repeating<'a, T, TP> {
Self {
job,
interval,
_tp: PhantomData
}
}
pub fn times(self, n: usize) -> &'a mut T {
if n >= 1 {
self.job.schedule_mut().repeat_config = Some(RepeatConfig {
repeats: n,
repeat_interval: self.interval,
repeats_left: 0
});
}
self.job
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum RunCount {
Never,
Times(usize),
Forever
}
#[derive(Debug, Clone)]
pub(crate) struct RepeatConfig {
repeats: usize,
repeat_interval: Interval,
repeats_left: usize
}
pub struct JobSchedule<TP = DefaultTimeProvider> {
frequency: Vec<RunConfig>,
next_run: Option<OffsetDateTime>,
last_run: Option<OffsetDateTime>,
run_count: RunCount,
repeat_config: Option<RepeatConfig>,
utc_offset: UtcOffset,
_tp: PhantomData<TP>
}
impl<TP> Debug for JobSchedule<TP> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("JobSchedule")
.field("frequency", &self.frequency)
.field("next_run", &self.next_run)
.field("last_run", &self.last_run)
.field("run_count", &self.run_count)
.field("repeat_config", &self.repeat_config)
.finish()
}
}
impl<TP: TimeProvider> JobSchedule<TP> {
pub(crate) fn new(ival: Interval, utc_offset: UtcOffset) -> Self {
Self {
frequency: vec![RunConfig::from_interval(ival)],
next_run: None,
last_run: None,
run_count: RunCount::Forever,
repeat_config: None,
utc_offset,
_tp: PhantomData
}
}
fn last_frequency(&mut self) -> &mut RunConfig {
let last_idx = self.frequency.len() - 1;
&mut self.frequency[last_idx]
}
pub fn at(&mut self, time: &str) -> &mut Self {
self.try_at(time)
.expect("Could not convert value into a time")
}
pub fn try_at(&mut self, time: &str) -> Result<&mut Self, time::error::Parse> {
Ok(self.at_time(parse_time(time)?))
}
pub fn at_time(&mut self, time: Time) -> &mut Self {
let frequency = self.last_frequency();
*frequency = frequency.with_time(time);
self
}
pub fn plus(&mut self, ival: Interval) -> &mut Self {
let frequency = self.last_frequency();
*frequency = frequency.with_subinterval(ival);
self
}
pub fn and_every(&mut self, ival: Interval) -> &mut Self {
self.frequency.push(RunConfig::from_interval(ival));
self
}
pub fn once(&mut self) -> &mut Self {
self.run_count = RunCount::Times(1);
self
}
pub fn forever(&mut self) -> &mut Self {
self.run_count = RunCount::Forever;
self
}
pub fn count(&mut self, count: usize) -> &mut Self {
self.run_count = RunCount::Times(count);
self
}
fn next_run_time(&self, now: OffsetDateTime) -> Option<OffsetDateTime> {
match self.run_count {
RunCount::Never => None,
_ => self.frequency.iter().map(|freq| freq.next(now)).min()
}
}
pub fn can_run_again(&self) -> bool {
self.run_count != RunCount::Never
}
pub fn start_schedule(&mut self) -> &mut Self {
if self.next_run.is_none() {
let now = TP::now(self.utc_offset);
self.next_run = self.next_run_time(now);
if let Some(RepeatConfig {
repeats,
repeats_left,
..
}) = &mut self.repeat_config
{
*repeats_left = *repeats;
}
}
self
}
pub fn is_pending(&self, now: OffsetDateTime) -> bool {
self.next_run.is_some_and(|dt| dt <= now)
}
pub fn schedule_next(&mut self, now: OffsetDateTime) {
if self.run_count == RunCount::Never {
return;
}
let next_run_time = self.next_run_time(now);
match &mut self.repeat_config {
Some(RepeatConfig {
repeats,
repeats_left,
repeat_interval
}) => {
if *repeats_left > 0 {
*repeats_left -= 1;
let mut next = self.next_run.unwrap_or(now);
loop {
next = repeat_interval.next_from(next);
if next > now {
break;
}
}
self.next_run = Some(next);
} else {
self.next_run = next_run_time;
*repeats_left = *repeats;
}
},
None => self.next_run = next_run_time
}
self.last_run = Some(now);
self.run_count = match self.run_count {
RunCount::Never => RunCount::Never,
RunCount::Times(n) if n > 1 => RunCount::Times(n - 1),
RunCount::Times(_) => RunCount::Never,
RunCount::Forever => RunCount::Forever
};
}
}
#[cfg(test)]
mod test {
use super::JobSchedule;
use crate::{Job, SyncJob, intervals::*, timeprovider::TimeProvider};
use time::{Date, Month, OffsetDateTime, Time, UtcDateTime, UtcOffset};
fn utc_hms(h: u8, m: u8, s: u8) -> OffsetDateTime {
UtcDateTime::new(
Date::from_calendar_date(2020, Month::June, 16).unwrap(),
Time::from_hms(h, m, s).unwrap()
)
.into()
}
struct TestTimeProvider;
impl TimeProvider for TestTimeProvider {
fn now(utc_offset: UtcOffset) -> OffsetDateTime {
utc_hms(7, 58, 0).to_offset(utc_offset)
}
}
#[test]
fn test_repeating() {
let mut job = SyncJob::<TestTimeProvider>::new(1.hour(), UtcOffset::UTC);
job.repeating_every(45.minutes()).times(2);
job.run(|| {});
assert!(!job.is_pending(utc_hms(7, 59, 0)));
assert!(job.is_pending(utc_hms(8, 0, 0)));
job.execute(utc_hms(8, 0, 0));
assert!(!job.is_pending(utc_hms(8, 44, 0)));
assert!(job.is_pending(utc_hms(8, 45, 0)));
job.execute(utc_hms(8, 45, 0));
assert!(!job.is_pending(utc_hms(9, 0, 0)));
assert!(!job.is_pending(utc_hms(9, 29, 0)));
assert!(job.is_pending(utc_hms(9, 30, 0)));
job.execute(utc_hms(9, 30, 0));
assert!(!job.is_pending(utc_hms(9, 59, 0)));
assert!(job.is_pending(utc_hms(10, 0, 0)));
}
#[test]
fn test_time_coercion() {
let mut job = JobSchedule::<TestTimeProvider>::new(1.day(), UtcOffset::UTC);
job.try_at("12:32").unwrap();
job.try_at(&format!("{}:{}", 12, 32)).unwrap();
job.at_time(Time::from_hms(12, 32, 0).unwrap());
}
}