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
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)) |
|
); |
|
} |
|
}
|
|
|