Implementation of firmware for a sprinklers system using async Rust, Tokio, Actix, MQTT and more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

646 lines
22 KiB

//! Scheduling for events to run at certain intervals in the future
use chrono::{
Date, DateTime, Datelike, Duration as CDuration, Local, NaiveDateTime, NaiveTime, TimeZone,
Weekday,
};
use serde::{Deserialize, Serialize};
use std::cmp;
use std::iter::FromIterator;
/// A set of times of day (for [Schedule](struct.Schedule.html))
pub type TimeSet = Vec<NaiveTime>;
/// A set of days of week (for [Schedule](struct.Schedule.html))
pub type WeekdaySet = Vec<Weekday>;
/// Returns a [`WeekdaySet`](type.WeekdaySet.html) of every day of the week
#[allow(dead_code)]
pub fn every_day() -> WeekdaySet {
WeekdaySet::from_iter(
[
Weekday::Mon,
Weekday::Tue,
Weekday::Wed,
Weekday::Thu,
Weekday::Fri,
Weekday::Sat,
Weekday::Sun,
]
.iter()
.cloned(),
)
}
/// Represents the different types of date-time bounds that can be on a schedule
#[allow(dead_code)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DateTimeBound {
/// There is no bound (ie. the Schedule extends with no limit)
None,
/// There is a bound that repeats every year (ie. the year is set to the current year)
Yearly(NaiveDateTime),
/// There is a definite bound on the schedule
Definite(NaiveDateTime),
}
impl Default for DateTimeBound {
fn default() -> DateTimeBound {
DateTimeBound::None
}
}
impl DateTimeBound {
/// Resolves this bound into an optional `DateTime`. If there is no bound or the bound could
/// not be resolved, None is returned.
///
/// `reference` is the reference that is used to resolve a `Yearly` bound.
pub fn resolve_from<Tz: TimeZone>(&self, reference: &DateTime<Tz>) -> Option<DateTime<Tz>> {
match *self {
DateTimeBound::None => None,
DateTimeBound::Yearly(date_time) => {
date_time.with_year(reference.year()).and_then(|date_time| {
reference
.timezone()
.from_local_datetime(&date_time)
.single()
})
}
DateTimeBound::Definite(date_time) => reference
.timezone()
.from_local_datetime(&date_time)
.single(),
}
}
}
/// A schedule that determines when an event will occur.
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
pub struct Schedule {
#[serde(
serialize_with = "ser::serialize_times",
deserialize_with = "ser::deserialize_times"
)]
pub times: TimeSet,
#[serde(
serialize_with = "ser::serialize_weekdays",
deserialize_with = "ser::deserialize_weekdays"
)]
pub weekdays: WeekdaySet,
pub from: 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 {
"weekday out of range 0 to 6".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`
fn next_weekday<Tz: TimeZone>(mut date: Date<Tz>, weekday: Weekday) -> Date<Tz> {
while date.weekday() != weekday {
date = date.succ();
}
date
}
#[allow(dead_code)]
impl Schedule {
/// Creates a new Schedule.
///
/// `times` is the times of day the event will be run. `weekdays` is the set of days of week
/// the event will be run. `from` and `to` are restrictions on the end and beginning of event
/// runs, respectively.
pub fn new<T, W>(times: T, weekdays: W, from: DateTimeBound, to: DateTimeBound) -> Schedule
where
T: IntoIterator<Item = NaiveTime>,
W: IntoIterator<Item = Weekday>,
{
Schedule {
times: TimeSet::from_iter(times),
weekdays: WeekdaySet::from_iter(weekdays),
from,
to,
}
}
/// Gets the next `DateTime` the event should run after `reference`
///
/// Returns `None` if the event will never run after `reference` (ie. must be a `from` bound)
pub fn next_run_after<Tz: TimeZone>(&self, reference: &DateTime<Tz>) -> Option<DateTime<Tz>> {
let mut to = self.to.resolve_from(reference);
let mut from = self.from.resolve_from(reference);
if let (Some(from), Some(to)) = (&mut from, &mut to) {
// We must handle the case where yearly bounds cross a year boundary
if to < from {
if reference < to {
// Still in the bounds overlapping the previous year boundary
*from = from.with_year(from.year() - 1).unwrap();
} else {
// Awaiting (or in) next years bounds
*to = to.with_year(to.year() + 1).unwrap();
}
}
}
let from = match (from, &to) {
(Some(from), Some(to)) if &from > to => from.with_year(from.year() + 1),
(from, _) => from,
};
let reference = match &from {
Some(from) if from > reference => from,
_ => reference,
}
.clone();
let mut next_run: Option<DateTime<Tz>> = None;
for weekday in &self.weekdays {
for time in &self.times {
let candidate = next_weekday(reference.date(), *weekday)
.and_time(*time)
.map(|date| {
if date < reference {
date + CDuration::weeks(1)
} else {
date
}
});
let candidate = match (candidate, &to) {
(Some(date), Some(to)) if &date > to => None,
(date, _) => date,
};
next_run = match (next_run, candidate) {
// return whichever is first if there are 2 candidates
(Some(d1), Some(d2)) => Some(cmp::min(d1, d2)),
// otherwise return whichever isn't None (or None if both are)
(o1, o2) => o1.or(o2),
}
}
}
next_run
}
/// Gets the next run after the current (local) time
///
/// See [next_run_after](#method.next_run_after)
pub fn next_run_local(&self) -> Option<DateTime<Local>> {
self.next_run_after(&Local::now())
}
}
#[cfg(test)]
mod test {
use super::*;
use chrono::NaiveDate;
#[test]
fn test_date_time_bound() {
use super::DateTimeBound::*;
let cases: Vec<(DateTimeBound, Option<DateTime<Local>>)> = vec![
(None, Option::None),
(
Definite(NaiveDate::from_ymd(2016, 11, 16).and_hms(10, 30, 0)),
Some(Local.ymd(2016, 11, 16).and_hms(10, 30, 0)),
),
(
Yearly(NaiveDate::from_ymd(2016, 11, 16).and_hms(10, 30, 0)),
Some(Local.ymd(2018, 11, 16).and_hms(10, 30, 0)),
),
(
Yearly(NaiveDate::from_ymd(2016, 1, 1).and_hms(0, 0, 0)),
Some(Local.ymd(2018, 1, 1).and_hms(0, 0, 0)),
),
(
Yearly(NaiveDate::from_ymd(2016, 12, 31).and_hms(23, 59, 59)),
Some(Local.ymd(2018, 12, 31).and_hms(23, 59, 59)),
),
(
Yearly(NaiveDate::from_ymd(2012, 2, 29).and_hms(0, 0, 0)),
Option::None,
), /* leap day */
];
let from = Local.ymd(2018, 1, 1).and_hms(0, 0, 0);
for (bound, expected_result) in cases {
let result = bound.resolve_from(&from);
assert_eq!(result, expected_result);
}
}
#[test]
fn test_next_weekday() {
use super::next_weekday;
use chrono::Weekday;
// (date, weekday, result)
let cases: Vec<(Date<Local>, Weekday, Date<Local>)> = vec![
(
Local.ymd(2016, 11, 16),
Weekday::Wed,
Local.ymd(2016, 11, 16),
),
(
Local.ymd(2016, 11, 16),
Weekday::Fri,
Local.ymd(2016, 11, 18),
),
(
Local.ymd(2016, 11, 16),
Weekday::Tue,
Local.ymd(2016, 11, 22),
),
(Local.ymd(2016, 12, 30), Weekday::Tue, Local.ymd(2017, 1, 3)),
(
Local.ymd(2016, 11, 16),
Weekday::Tue,
Local.ymd(2016, 11, 22),
),
];
for (date, weekday, expected_result) in cases {
let result = next_weekday(date, weekday);
assert_eq!(result, expected_result);
}
}
#[test]
fn test_next_run_after() {
use super::{DateTimeBound, Schedule};
use chrono::{DateTime, Local, NaiveTime, TimeZone, Weekday};
let schedule = Schedule::new(
vec![NaiveTime::from_hms(10, 30, 0)],
vec![Weekday::Wed],
DateTimeBound::None,
DateTimeBound::None,
);
let cases: Vec<(DateTime<Local>, Option<DateTime<Local>>)> = vec![
(
Local.ymd(2016, 11, 14).and_hms(10, 30, 0),
Some(Local.ymd(2016, 11, 16).and_hms(10, 30, 0)),
),
(
Local.ymd(2016, 11, 16).and_hms(10, 20, 0),
Some(Local.ymd(2016, 11, 16).and_hms(10, 30, 0)),
),
(
Local.ymd(2016, 11, 16).and_hms(10, 40, 0),
Some(Local.ymd(2016, 11, 23).and_hms(10, 30, 0)),
),
];
for (reference, expected_result) in cases {
let result = schedule.next_run_after(&reference);
assert_eq!(result, expected_result);
}
}
#[test]
fn test_next_run_after2() {
use super::{DateTimeBound, Schedule};
use chrono::{DateTime, Local, NaiveTime, TimeZone, Weekday};
#[derive(Debug)]
struct Case {
schedule: Schedule,
ref_time: DateTime<Local>,
expected_result: Option<DateTime<Local>>,
}
impl Case {
fn new(
schedule: Schedule,
ref_time: DateTime<Local>,
expected_result: Option<DateTime<Local>>,
) -> Self {
Self {
schedule,
ref_time,
expected_result,
}
}
}
let sched1 = Schedule::new(
vec![NaiveTime::from_hms(8, 30, 0), NaiveTime::from_hms(20, 0, 0)],
vec![Weekday::Thu, Weekday::Fri],
DateTimeBound::None,
DateTimeBound::None,
);
let sched2 = Schedule::new(
vec![NaiveTime::from_hms(8, 30, 0), NaiveTime::from_hms(20, 0, 0)],
vec![Weekday::Thu, Weekday::Fri],
DateTimeBound::Definite(NaiveDate::from_ymd(2016, 5, 30).and_hms(0, 0, 0)),
DateTimeBound::Definite(NaiveDate::from_ymd(2016, 6, 30).and_hms(0, 0, 0)),
);
let sched3 = Schedule::new(
vec![NaiveTime::from_hms(8, 30, 0), NaiveTime::from_hms(20, 0, 0)],
every_day(),
DateTimeBound::Yearly(NaiveDate::from_ymd(0, 12, 15).and_hms(0, 0, 0)),
DateTimeBound::Yearly(NaiveDate::from_ymd(0, 1, 15).and_hms(0, 0, 0)),
);
let cases: Vec<Case> = vec![
Case::new(
sched1.clone(),
Local.ymd(2016, 5, 16).and_hms(0, 0, 0),
Some(Local.ymd(2016, 5, 19).and_hms(8, 30, 0)),
),
Case::new(
sched1,
Local.ymd(2016, 5, 20).and_hms(9, 0, 0),
Some(Local.ymd(2016, 5, 20).and_hms(20, 0, 0)),
),
Case::new(
sched2.clone(),
Local.ymd(2016, 6, 1).and_hms(0, 0, 0),
Some(Local.ymd(2016, 6, 2).and_hms(8, 30, 0)),
),
Case::new(
sched2.clone(),
Local.ymd(2016, 5, 1).and_hms(0, 0, 0),
Some(Local.ymd(2016, 6, 2).and_hms(8, 30, 0)),
),
Case::new(sched2, Local.ymd(2016, 7, 1).and_hms(0, 0, 0), None),
Case::new(
sched3.clone(),
Local.ymd(2016, 11, 1).and_hms(0, 0, 0),
Some(Local.ymd(2016, 12, 15).and_hms(8, 30, 0)),
),
Case::new(
sched3.clone(),
Local.ymd(2017, 1, 1).and_hms(9, 0, 0),
Some(Local.ymd(2017, 1, 1).and_hms(20, 0, 0)),
),
Case::new(
sched3,
Local.ymd(2016, 1, 30).and_hms(0, 0, 0),
Some(Local.ymd(2016, 12, 15).and_hms(8, 30, 0)),
),
];
for case in cases {
let result = case.schedule.next_run_after(&case.ref_time);
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))
);
}
}