|
|
@ -1,9 +1,10 @@ |
|
|
|
//! Scheduling for events to run at certain intervals in the future
|
|
|
|
//! Scheduling for events to run at certain intervals in the future
|
|
|
|
|
|
|
|
|
|
|
|
use chrono::{ |
|
|
|
use chrono::{ |
|
|
|
Date, DateTime, Datelike, Duration as CDuration, Local, NaiveDateTime, NaiveTime, |
|
|
|
Date, DateTime, Datelike, Duration as CDuration, Local, NaiveDateTime, NaiveTime, TimeZone, |
|
|
|
TimeZone, Weekday, |
|
|
|
Weekday, |
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
use serde::{Deserialize, Serialize}; |
|
|
|
use std::cmp; |
|
|
|
use std::cmp; |
|
|
|
use std::iter::FromIterator; |
|
|
|
use std::iter::FromIterator; |
|
|
|
|
|
|
|
|
|
|
@ -32,7 +33,7 @@ pub fn every_day() -> WeekdaySet { |
|
|
|
|
|
|
|
|
|
|
|
/// Represents the different types of date-time bounds that can be on a schedule
|
|
|
|
/// Represents the different types of date-time bounds that can be on a schedule
|
|
|
|
#[allow(dead_code)] |
|
|
|
#[allow(dead_code)] |
|
|
|
#[derive(Debug, Clone)] |
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)] |
|
|
|
pub enum DateTimeBound { |
|
|
|
pub enum DateTimeBound { |
|
|
|
/// There is no bound (ie. the Schedule extends with no limit)
|
|
|
|
/// There is no bound (ie. the Schedule extends with no limit)
|
|
|
|
None, |
|
|
|
None, |
|
|
@ -73,14 +74,250 @@ impl DateTimeBound { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// A schedule that determines when an event will occur.
|
|
|
|
/// A schedule that determines when an event will occur.
|
|
|
|
#[derive(Default, Debug, Clone)] |
|
|
|
#[derive(Default, Debug, Clone, Serialize, Deserialize)] |
|
|
|
pub struct Schedule { |
|
|
|
pub struct Schedule { |
|
|
|
|
|
|
|
#[serde(
|
|
|
|
|
|
|
|
serialize_with = "ser::serialize_times", |
|
|
|
|
|
|
|
deserialize_with = "ser::deserialize_times" |
|
|
|
|
|
|
|
)] |
|
|
|
pub times: TimeSet, |
|
|
|
pub times: TimeSet, |
|
|
|
|
|
|
|
#[serde(
|
|
|
|
|
|
|
|
serialize_with = "ser::serialize_weekdays", |
|
|
|
|
|
|
|
deserialize_with = "ser::deserialize_weekdays" |
|
|
|
|
|
|
|
)] |
|
|
|
pub weekdays: WeekdaySet, |
|
|
|
pub weekdays: WeekdaySet, |
|
|
|
pub from: DateTimeBound, |
|
|
|
pub from: DateTimeBound, |
|
|
|
pub to: DateTimeBound, |
|
|
|
pub to: DateTimeBound, |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
mod ser { |
|
|
|
|
|
|
|
use super::{DateTimeBound, TimeSet, WeekdaySet}; |
|
|
|
|
|
|
|
use chrono::{NaiveDate, NaiveTime, Weekday}; |
|
|
|
|
|
|
|
use serde::{Deserialize, Deserializer, Serialize, Serializer}; |
|
|
|
|
|
|
|
use std::{convert::TryInto, fmt}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Default, Debug, Clone, Serialize, Deserialize)] |
|
|
|
|
|
|
|
struct TimeOfDay { |
|
|
|
|
|
|
|
hour: u32, |
|
|
|
|
|
|
|
minute: u32, |
|
|
|
|
|
|
|
second: u32, |
|
|
|
|
|
|
|
#[serde(default)] |
|
|
|
|
|
|
|
millisecond: u32, |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
impl From<&NaiveTime> for TimeOfDay { |
|
|
|
|
|
|
|
fn from(time: &NaiveTime) -> Self { |
|
|
|
|
|
|
|
use chrono::Timelike; |
|
|
|
|
|
|
|
Self { |
|
|
|
|
|
|
|
hour: time.hour(), |
|
|
|
|
|
|
|
minute: time.minute(), |
|
|
|
|
|
|
|
second: time.second(), |
|
|
|
|
|
|
|
millisecond: time.nanosecond() / 1_000_000_u32, |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Copy, Clone, Debug)] |
|
|
|
|
|
|
|
struct InvalidTimeOfDay; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
impl fmt::Display for InvalidTimeOfDay { |
|
|
|
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
|
|
|
|
|
|
|
"invalid time of day".fmt(f) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
impl TryInto<NaiveTime> for TimeOfDay { |
|
|
|
|
|
|
|
type Error = InvalidTimeOfDay; |
|
|
|
|
|
|
|
fn try_into(self) -> Result<NaiveTime, Self::Error> { |
|
|
|
|
|
|
|
NaiveTime::from_hms_milli_opt(self.hour, self.minute, self.second, self.millisecond) |
|
|
|
|
|
|
|
.ok_or(InvalidTimeOfDay) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#[allow(clippy::ptr_arg)] |
|
|
|
|
|
|
|
pub fn serialize_times<S>(times: &TimeSet, serializer: S) -> Result<S::Ok, S::Error> |
|
|
|
|
|
|
|
where |
|
|
|
|
|
|
|
S: Serializer, |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
serializer.collect_seq(times.iter().map(TimeOfDay::from)) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pub fn deserialize_times<'de, D>(deserializer: D) -> Result<TimeSet, D::Error> |
|
|
|
|
|
|
|
where |
|
|
|
|
|
|
|
D: Deserializer<'de>, |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
use serde::de::{Error, SeqAccess, Visitor}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
struct TimeSetVisitor; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
impl<'de> Visitor<'de> for TimeSetVisitor { |
|
|
|
|
|
|
|
type Value = TimeSet; |
|
|
|
|
|
|
|
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { |
|
|
|
|
|
|
|
formatter.write_str("a sequence of time of days") |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error> |
|
|
|
|
|
|
|
where |
|
|
|
|
|
|
|
A: SeqAccess<'de>, |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
let mut times = TimeSet::with_capacity(seq.size_hint().unwrap_or(0)); |
|
|
|
|
|
|
|
while let Some(value) = seq.next_element::<TimeOfDay>()? { |
|
|
|
|
|
|
|
let time: NaiveTime = value.try_into().map_err(A::Error::custom)?; |
|
|
|
|
|
|
|
times.push(time); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
Ok(times) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
deserializer.deserialize_seq(TimeSetVisitor) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#[allow(clippy::ptr_arg)] |
|
|
|
|
|
|
|
pub fn serialize_weekdays<S>(weekdays: &WeekdaySet, serializer: S) -> Result<S::Ok, S::Error> |
|
|
|
|
|
|
|
where |
|
|
|
|
|
|
|
S: Serializer, |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
let iter = weekdays |
|
|
|
|
|
|
|
.iter() |
|
|
|
|
|
|
|
.map(|weekday| weekday.num_days_from_sunday()); |
|
|
|
|
|
|
|
serializer.collect_seq(iter) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Copy, Clone, Debug)] |
|
|
|
|
|
|
|
struct InvalidWeekday; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
impl fmt::Display for InvalidWeekday { |
|
|
|
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
|
|
|
|
|
|
|
"invalid weekday".fmt(f) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fn weekday_from_days_from_sunday(days: u32) -> Result<Weekday, InvalidWeekday> { |
|
|
|
|
|
|
|
Ok(match days { |
|
|
|
|
|
|
|
0 => Weekday::Sun, |
|
|
|
|
|
|
|
1 => Weekday::Mon, |
|
|
|
|
|
|
|
2 => Weekday::Tue, |
|
|
|
|
|
|
|
3 => Weekday::Wed, |
|
|
|
|
|
|
|
4 => Weekday::Thu, |
|
|
|
|
|
|
|
5 => Weekday::Fri, |
|
|
|
|
|
|
|
6 => Weekday::Sat, |
|
|
|
|
|
|
|
_ => return Err(InvalidWeekday), |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pub fn deserialize_weekdays<'de, D>(deserializer: D) -> Result<WeekdaySet, D::Error> |
|
|
|
|
|
|
|
where |
|
|
|
|
|
|
|
D: Deserializer<'de>, |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
use serde::de::{Error, SeqAccess, Visitor}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
struct WeekdaySetVisitor; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
impl<'de> Visitor<'de> for WeekdaySetVisitor { |
|
|
|
|
|
|
|
type Value = WeekdaySet; |
|
|
|
|
|
|
|
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { |
|
|
|
|
|
|
|
formatter.write_str("a sequence of integers representing weekdays") |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error> |
|
|
|
|
|
|
|
where |
|
|
|
|
|
|
|
A: SeqAccess<'de>, |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
let mut weekdays = WeekdaySet::with_capacity(seq.size_hint().unwrap_or(0)); |
|
|
|
|
|
|
|
while let Some(value) = seq.next_element::<u32>()? { |
|
|
|
|
|
|
|
let weekday = weekday_from_days_from_sunday(value).map_err(A::Error::custom)?; |
|
|
|
|
|
|
|
weekdays.push(weekday); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
Ok(weekdays) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
deserializer.deserialize_seq(WeekdaySetVisitor) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize)] |
|
|
|
|
|
|
|
struct DateAndYear { |
|
|
|
|
|
|
|
year: i32, |
|
|
|
|
|
|
|
month: u32, |
|
|
|
|
|
|
|
day: u32, |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
impl From<NaiveDate> for DateAndYear { |
|
|
|
|
|
|
|
fn from(date: NaiveDate) -> Self { |
|
|
|
|
|
|
|
use chrono::Datelike; |
|
|
|
|
|
|
|
Self { |
|
|
|
|
|
|
|
year: date.year(), |
|
|
|
|
|
|
|
month: date.month(), |
|
|
|
|
|
|
|
day: date.day(), |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Copy, Clone, Debug)] |
|
|
|
|
|
|
|
struct InvalidDateAndYear; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
impl fmt::Display for InvalidDateAndYear { |
|
|
|
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
|
|
|
|
|
|
|
"invalid date or year".fmt(f) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
impl TryInto<NaiveDate> for DateAndYear { |
|
|
|
|
|
|
|
type Error = InvalidDateAndYear; |
|
|
|
|
|
|
|
fn try_into(self) -> Result<NaiveDate, Self::Error> { |
|
|
|
|
|
|
|
NaiveDate::from_ymd_opt(self.year, self.month, self.day).ok_or(InvalidDateAndYear) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
impl Serialize for DateTimeBound { |
|
|
|
|
|
|
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> |
|
|
|
|
|
|
|
where |
|
|
|
|
|
|
|
S: Serializer, |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
match self { |
|
|
|
|
|
|
|
DateTimeBound::None => serializer.serialize_none(), |
|
|
|
|
|
|
|
DateTimeBound::Yearly(date_time) => { |
|
|
|
|
|
|
|
// Discard time
|
|
|
|
|
|
|
|
let mut date_of_year: DateAndYear = date_time.date().into(); |
|
|
|
|
|
|
|
// Set year to 0 (since it is yearly)
|
|
|
|
|
|
|
|
date_of_year.year = 0; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
date_of_year.serialize(serializer) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
DateTimeBound::Definite(date_time) => { |
|
|
|
|
|
|
|
// Discard time
|
|
|
|
|
|
|
|
let date_of_year: DateAndYear = date_time.date().into(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
date_of_year.serialize(serializer) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
impl<'de> Deserialize<'de> for DateTimeBound { |
|
|
|
|
|
|
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> |
|
|
|
|
|
|
|
where |
|
|
|
|
|
|
|
D: Deserializer<'de>, |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
use serde::de::Error; |
|
|
|
|
|
|
|
let date_of_year: Option<DateAndYear> = Deserialize::deserialize(deserializer)?; |
|
|
|
|
|
|
|
Ok(match date_of_year { |
|
|
|
|
|
|
|
Some(date_of_year) => { |
|
|
|
|
|
|
|
let year = date_of_year.year; |
|
|
|
|
|
|
|
let date: NaiveDate = date_of_year.try_into().map_err(D::Error::custom)?; |
|
|
|
|
|
|
|
let date_time = date.and_hms(0, 0, 0); |
|
|
|
|
|
|
|
if year == 0 { |
|
|
|
|
|
|
|
DateTimeBound::Yearly(date_time) |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
DateTimeBound::Definite(date_time) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
None => DateTimeBound::None, |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// Gets the next date matching the `weekday` after `date`
|
|
|
|
/// Gets the next date matching the `weekday` after `date`
|
|
|
|
fn next_weekday<Tz: TimeZone>(mut date: Date<Tz>, weekday: Weekday) -> Date<Tz> { |
|
|
|
fn next_weekday<Tz: TimeZone>(mut date: Date<Tz>, weekday: Weekday) -> Date<Tz> { |
|
|
|
while date.weekday() != weekday { |
|
|
|
while date.weekday() != weekday { |
|
|
@ -174,6 +411,7 @@ impl Schedule { |
|
|
|
#[cfg(test)] |
|
|
|
#[cfg(test)] |
|
|
|
mod test { |
|
|
|
mod test { |
|
|
|
use super::*; |
|
|
|
use super::*; |
|
|
|
|
|
|
|
use assert_matches::assert_matches; |
|
|
|
use chrono::NaiveDate; |
|
|
|
use chrono::NaiveDate; |
|
|
|
|
|
|
|
|
|
|
|
#[test] |
|
|
|
#[test] |
|
|
@ -360,4 +598,50 @@ mod test { |
|
|
|
assert_eq!(result, case.expected_result, "case failed: {:?}", case); |
|
|
|
assert_eq!(result, case.expected_result, "case failed: {:?}", case); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#[test] |
|
|
|
|
|
|
|
fn test_serialize() { |
|
|
|
|
|
|
|
let sched = Schedule::new( |
|
|
|
|
|
|
|
vec![ |
|
|
|
|
|
|
|
NaiveTime::from_hms_milli(0, 0, 0, 0), |
|
|
|
|
|
|
|
NaiveTime::from_hms_milli(23, 59, 59, 999), |
|
|
|
|
|
|
|
], |
|
|
|
|
|
|
|
every_day(), |
|
|
|
|
|
|
|
DateTimeBound::Yearly(NaiveDate::from_ymd(2020, 1, 1).and_hms(0, 0, 0)), |
|
|
|
|
|
|
|
DateTimeBound::Definite(NaiveDate::from_ymd(9999, 12, 31).and_hms(23, 59, 59)), |
|
|
|
|
|
|
|
); |
|
|
|
|
|
|
|
let ser = serde_json::to_string(&sched).unwrap(); |
|
|
|
|
|
|
|
// Weekdays should match the order in `every_day()` but with sunday being 0
|
|
|
|
|
|
|
|
assert_eq!( |
|
|
|
|
|
|
|
&ser, |
|
|
|
|
|
|
|
"{\ |
|
|
|
|
|
|
|
\"times\":[\ |
|
|
|
|
|
|
|
{\"hour\":0,\"minute\":0,\"second\":0,\"millisecond\":0},\ |
|
|
|
|
|
|
|
{\"hour\":23,\"minute\":59,\"second\":59,\"millisecond\":999}\ |
|
|
|
|
|
|
|
],\ |
|
|
|
|
|
|
|
\"weekdays\":[1,2,3,4,5,6,0],\ |
|
|
|
|
|
|
|
\"from\":{\ |
|
|
|
|
|
|
|
\"year\":0,\"month\":1,\"day\":1\ |
|
|
|
|
|
|
|
},\ |
|
|
|
|
|
|
|
\"to\":{\ |
|
|
|
|
|
|
|
\"year\":9999,\"month\":12,\"day\":31\ |
|
|
|
|
|
|
|
}\ |
|
|
|
|
|
|
|
}" |
|
|
|
|
|
|
|
); |
|
|
|
|
|
|
|
let sched_de: Schedule = serde_json::from_str(&ser).unwrap(); |
|
|
|
|
|
|
|
assert_eq!(sched.times, sched_de.times); |
|
|
|
|
|
|
|
assert_eq!(sched.weekdays, sched_de.weekdays); |
|
|
|
|
|
|
|
// This serialization is lossy (year is discarded for yearly)
|
|
|
|
|
|
|
|
// assert_eq!(sched_de.from, sched.from);
|
|
|
|
|
|
|
|
assert_eq!( |
|
|
|
|
|
|
|
sched_de.from, |
|
|
|
|
|
|
|
DateTimeBound::Yearly(NaiveDate::from_ymd(0, 1, 1).and_hms(0, 0, 0)) |
|
|
|
|
|
|
|
); |
|
|
|
|
|
|
|
// This serialization is also lossy (time is discarded)
|
|
|
|
|
|
|
|
// assert_eq!(sched_de.to, sched.to);
|
|
|
|
|
|
|
|
assert_eq!( |
|
|
|
|
|
|
|
sched_de.to, |
|
|
|
|
|
|
|
DateTimeBound::Definite(NaiveDate::from_ymd(9999, 12, 31).and_hms(0, 0, 0)) |
|
|
|
|
|
|
|
); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|