use crate::Interval::*;
use time::{
Date, Duration, OffsetDateTime, Time, Weekday,
format_description::BorrowedFormatItem, macros::format_description
};
#[derive(Eq, PartialEq, Debug, Copy, Clone)]
pub enum Interval {
Seconds(u32),
Minutes(u32),
Hours(u32),
Days(u32),
Weeks(u32),
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
Sunday,
Workday
}
impl From<time::Weekday> for Interval {
fn from(weekday: time::Weekday) -> Self {
match weekday {
time::Weekday::Monday => Interval::Monday,
time::Weekday::Tuesday => Interval::Tuesday,
time::Weekday::Wednesday => Interval::Wednesday,
time::Weekday::Thursday => Interval::Thursday,
time::Weekday::Friday => Interval::Friday,
time::Weekday::Saturday => Interval::Saturday,
time::Weekday::Sunday => Interval::Sunday
}
}
}
pub trait NextTime {
fn next(&self, from: OffsetDateTime) -> OffsetDateTime;
fn prev(&self, from: OffsetDateTime) -> OffsetDateTime;
}
pub(crate) fn parse_time(s: &str) -> Result<Time, time::error::Parse> {
static TIME_FORMATS: &[&[BorrowedFormatItem<'static>]] = &[
format_description!(
"[hour padding:none]:[minute padding:none]:[second padding:none]"
),
format_description!(
"[hour padding:none repr:12]:[minute padding:none]:[second padding:none] [period case_sensitive:false]"
),
format_description!("[hour padding:none]:[minute padding:none]"),
format_description!(
"[hour padding:none repr:12]:[minute padding:none] [period case_sensitive:false]"
)
];
let mut err = None;
for format in TIME_FORMATS {
match Time::parse(s, format) {
Ok(time) => return Ok(time),
Err(parse_err) => {
if cfg!(test) {
eprintln!("Time parse error using {format:?}: {parse_err}");
}
if err.is_none() {
err = Some(parse_err);
}
}
}
}
Err(err.unwrap())
}
#[derive(Debug)]
enum Adjustment {
Intervals(Vec<Interval>),
Time(Time)
}
#[derive(Debug)]
pub(crate) struct RunConfig {
base: Interval,
adjustment: Option<Adjustment>
}
impl RunConfig {
pub(crate) fn from_interval(base: Interval) -> Self {
RunConfig {
base,
adjustment: None
}
}
pub(crate) fn with_time(&self, t: Time) -> Self {
RunConfig {
adjustment: Some(Adjustment::Time(t)),
..*self
}
}
pub(crate) fn with_subinterval(&self, ival: Interval) -> Self {
let mut ival_queue = match self.adjustment {
None => vec![],
Some(Adjustment::Time(_)) => vec![],
Some(Adjustment::Intervals(ref ivals)) => ivals.clone()
};
ival_queue.push(ival);
RunConfig {
adjustment: Some(Adjustment::Intervals(ival_queue)),
..*self
}
}
fn apply_adjustment(&self, from: OffsetDateTime) -> OffsetDateTime {
match self.adjustment {
None => from,
Some(Adjustment::Time(t)) => {
let from_time = from.time();
if t >= from_time {
from.replace_time(t)
} else {
(from + Duration::days(1)).replace_time(t)
}
},
Some(Adjustment::Intervals(ref ivals)) => {
let mut rv = from;
for ival in ivals {
rv = ival.next(rv);
}
rv
}
}
}
}
impl NextTime for RunConfig {
fn next(&self, from: OffsetDateTime) -> OffsetDateTime {
let candidate = self.apply_adjustment(self.base.prev(from));
if candidate > from {
candidate
} else {
self.apply_adjustment(self.base.next(from))
}
}
fn prev(&self, _from: OffsetDateTime) -> OffsetDateTime {
unimplemented!()
}
}
static DAYS_TO_SHIFT: [u8; 14] = [7, 6, 5, 4, 3, 2, 1, 7, 6, 5, 4, 3, 2, 1];
fn day_of_week(i: Interval) -> usize {
match i {
Monday => 0,
Tuesday => 1,
Wednesday => 2,
Thursday => 3,
Friday => 4,
Saturday => 5,
Sunday => 6,
_ => 7
}
}
#[inline]
fn num_seconds_from_midnight(dt: OffsetDateTime) -> u32 {
dt.hour() as u32 * 3600 + dt.minute() as u32 * 60 + dt.second() as u32
}
#[inline]
fn num_days_from_ce(dt: OffsetDateTime) -> u32 {
let days: u32 = (dt.date() - Date::from_ordinal_date(1, 1).unwrap())
.whole_days()
.try_into()
.expect("Congratulations, you just invented time travel");
days + 1
}
impl NextTime for Interval {
fn next(&self, from: OffsetDateTime) -> OffsetDateTime {
match *self {
Seconds(0) | Minutes(0) | Hours(0) | Days(0) | Weeks(0) => from,
Seconds(s) => {
let modulus = from.unix_timestamp().checked_rem(s.into()).unwrap_or(0);
let next = s - (modulus as u32);
from.truncate_to_second() + Duration::seconds(next.into())
},
Minutes(m) => {
let s = num_seconds_from_midnight(from);
let modulus = s.checked_rem(m * 60).unwrap_or(0);
from.truncate_to_second() + Duration::seconds((m * 60 - modulus).into())
},
Hours(h) => {
let s = num_seconds_from_midnight(from);
let modulus = s.checked_rem(h * 3600).unwrap_or(0);
from.truncate_to_second() + Duration::seconds((h * 3600 - modulus).into())
},
Days(d) => {
let day_of_era = num_days_from_ce(from);
let modulus = day_of_era.checked_rem(d).unwrap_or(0);
from.truncate_to_day() + Duration::days((d - modulus).into())
},
Weeks(w) => {
let dow = from.weekday().number_days_from_monday();
let start_of_week = from.truncate_to_day() - Duration::days(dow.into());
let days_since_ever = num_days_from_ce(from);
let week_num = days_since_ever / 7;
let modulus = week_num.checked_rem(w).unwrap_or(0);
start_of_week + Duration::weeks(i64::from(w - modulus))
},
Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | Sunday => {
let dow = from.weekday().number_days_from_monday() as usize;
let i_dow = day_of_week(*self);
let to_shift = DAYS_TO_SHIFT[7 - i_dow + dow];
from.truncate_to_day() + Duration::days(i64::from(to_shift))
},
Workday => {
let days = match from.weekday() {
Weekday::Friday => 3,
Weekday::Saturday => 2,
_ => 1
};
from.truncate_to_day() + Duration::days(days)
}
}
}
fn prev(&self, from: OffsetDateTime) -> OffsetDateTime {
match *self {
Seconds(0) | Minutes(0) | Hours(0) | Days(0) | Weeks(0) => from,
Seconds(s) => {
let modulus = from.unix_timestamp().checked_rem(s.into()).unwrap_or(0);
let modulus = if modulus == 0 { s.into() } else { modulus };
from.truncate_to_second() - Duration::seconds(modulus)
},
Minutes(m) => {
let s = num_seconds_from_midnight(from);
let modulus = s.checked_rem(m * 60).unwrap_or(0);
let modulus = if modulus == 0 { m * 60 } else { modulus };
from.truncate_to_second() - Duration::seconds(i64::from(modulus))
},
Hours(h) => {
let s = num_seconds_from_midnight(from);
let modulus = s.checked_rem(h * 3600).unwrap_or(0);
let modulus = if modulus == 0 { h * 3600 } else { modulus };
from.truncate_to_second() - Duration::seconds(i64::from(modulus))
},
Days(d) => {
let day_of_era = num_days_from_ce(from);
let modulus = day_of_era.checked_rem(d).unwrap_or(0);
let modulus = if modulus == 0 && num_seconds_from_midnight(from) == 0 {
d
} else {
modulus
};
from.truncate_to_day() - Duration::days(i64::from(modulus))
},
Weeks(w) => {
let dow = from.weekday().number_days_from_monday();
let start_of_week = from.truncate_to_day() - Duration::days(dow.into());
let days_since_ever = num_days_from_ce(from);
let week_num = days_since_ever / 7;
let mut modulus = week_num.checked_rem(w).unwrap_or(0);
if modulus == 0 && num_seconds_from_midnight(from) == 0 {
modulus = w;
}
start_of_week - Duration::weeks(i64::from(modulus))
},
Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | Sunday => {
let dow = from.weekday().number_days_from_monday() as i32;
let i_dow = day_of_week(*self) as i32;
let mut to_shift = if dow >= i_dow {
dow - i_dow
} else {
7 + dow - i_dow
};
if to_shift == 0 && num_seconds_from_midnight(from) == 0 {
to_shift = 7;
}
from.truncate_to_day() - Duration::days(i64::from(to_shift))
},
Workday => {
let days = match from.weekday() {
Weekday::Saturday => 1,
Weekday::Sunday => 2,
_ if num_seconds_from_midnight(from) == 0 => 1,
_ => 0
};
from.truncate_to_day() - Duration::days(days)
}
}
}
}
impl Interval {
pub(crate) fn next_from(&self, from: OffsetDateTime) -> OffsetDateTime {
match *self {
Seconds(0) | Minutes(0) | Hours(0) | Days(0) | Weeks(0) => from,
Seconds(s) => from + Duration::seconds(s as i64),
Minutes(m) => from + Duration::seconds(m as i64 * 60),
Hours(h) => from + Duration::seconds(h as i64 * 3600),
Days(d) => from + Duration::days(d as i64),
Weeks(w) => from + Duration::days(w as i64 * 7),
Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | Sunday => {
self.next(from)
},
Workday => {
let d = from.date();
let dow = d.weekday();
let days = match dow {
Weekday::Friday => 3,
Weekday::Saturday => 2,
_ => 1
};
from + Duration::days(days)
}
}
}
}
pub trait TimeUnits: Sized {
fn seconds(self) -> Interval;
fn minutes(self) -> Interval;
fn hours(self) -> Interval;
fn days(self) -> Interval;
fn weeks(self) -> Interval;
fn second(self) -> Interval {
self.seconds()
}
fn minute(self) -> Interval {
self.minutes()
}
fn hour(self) -> Interval {
self.hours()
}
fn day(self) -> Interval {
self.days()
}
fn week(self) -> Interval {
self.weeks()
}
}
impl TimeUnits for u32 {
fn seconds(self) -> Interval {
Seconds(self)
}
fn minutes(self) -> Interval {
Minutes(self)
}
fn hours(self) -> Interval {
Hours(self)
}
fn days(self) -> Interval {
Days(self)
}
fn weeks(self) -> Interval {
Weeks(self)
}
}
#[cfg(test)]
mod tests {
use super::parse_time;
use crate::{
Interval::*,
TimeUnits,
intervals::{NextTime, RunConfig}
};
use time::{OffsetDateTime, Time, format_description::well_known::Rfc3339};
fn parse_from_rfc3339(s: &str) -> OffsetDateTime {
OffsetDateTime::parse(s, &Rfc3339).unwrap()
}
#[test]
fn basic_units() {
assert_eq!(Seconds(5), 5.seconds());
assert_eq!(Minutes(5), 5.minutes());
assert_eq!(Hours(5), 5.hours());
assert_eq!(Days(5), 5.days());
assert_eq!(Weeks(5), 5.weeks());
assert_eq!(Seconds(0), 0.seconds());
assert_eq!(Minutes(0), 0.minutes());
assert_eq!(Hours(0), 0.hours());
assert_eq!(Days(0), 0.days());
assert_eq!(Weeks(0), 0.weeks());
}
#[test]
fn test_next_start() {
let dt = parse_from_rfc3339("2018-09-04T14:22:13.999-00:00");
let next_dt = 5.seconds().next(dt);
let expected = parse_from_rfc3339("2018-09-04T14:22:15-00:00");
assert_eq!(next_dt, expected);
let next_dt = 5.seconds().next(expected);
let expected = parse_from_rfc3339("2018-09-04T14:22:20-00:00");
assert_eq!(next_dt, expected);
let next_dt = 15.minutes().next(dt);
let expected = parse_from_rfc3339("2018-09-04T14:30:00-00:00");
assert_eq!(next_dt, expected);
let next_dt = 15.minutes().next(expected);
let expected = parse_from_rfc3339("2018-09-04T14:45:00-00:00");
assert_eq!(next_dt, expected);
let next_dt = 2.hours().next(dt);
let expected = parse_from_rfc3339("2018-09-04T16:00:00-00:00");
assert_eq!(next_dt, expected);
let next_dt = 2.hours().next(expected);
let expected = parse_from_rfc3339("2018-09-04T18:00:00-00:00");
assert_eq!(next_dt, expected);
let next_dt = 2.days().next(dt);
let expected = parse_from_rfc3339("2018-09-05T00:00:00-00:00");
assert_eq!(next_dt, expected);
let next_dt = 2.days().next(expected);
let expected = parse_from_rfc3339("2018-09-07T00:00:00-00:00");
assert_eq!(next_dt, expected);
let next_dt = 2.weeks().next(dt);
let expected = parse_from_rfc3339("2018-09-10T00:00:00-00:00");
assert_eq!(next_dt, expected);
let next_dt = 2.weeks().next(expected);
let expected = parse_from_rfc3339("2018-09-24T00:00:00-00:00");
assert_eq!(next_dt, expected);
let next_dt = Monday.next(dt);
let expected = parse_from_rfc3339("2018-09-10T00:00:00-00:00");
assert_eq!(next_dt, expected);
let next_dt = Monday.next(expected);
let expected = parse_from_rfc3339("2018-09-17T00:00:00-00:00");
assert_eq!(next_dt, expected);
let next_dt = Wednesday.next(dt);
let expected = parse_from_rfc3339("2018-09-05T00:00:00-00:00");
assert_eq!(next_dt, expected);
let next_dt = Wednesday.next(expected);
let expected = parse_from_rfc3339("2018-09-12T00:00:00-00:00");
assert_eq!(next_dt, expected);
let friday = Friday.next(dt);
let next_dt = Workday.next(friday);
let expected = parse_from_rfc3339("2018-09-10T00:00:00-00:00");
assert_eq!(next_dt, expected);
let next_dt = Workday.next(expected);
let expected = parse_from_rfc3339("2018-09-11T00:00:00-00:00");
assert_eq!(next_dt, expected);
}
#[test]
fn test_prev() {
let dt = parse_from_rfc3339("2018-09-04T14:22:13.999-00:00");
let prev_dt = 5.seconds().prev(dt);
let expected = parse_from_rfc3339("2018-09-04T14:22:10-00:00");
assert_eq!(prev_dt, expected);
let prev_dt = 5.seconds().prev(expected);
let expected = parse_from_rfc3339("2018-09-04T14:22:05-00:00");
assert_eq!(prev_dt, expected);
let prev_dt = 1.second().prev(dt);
let expected = parse_from_rfc3339("2018-09-04T14:22:12-00:00");
assert_eq!(prev_dt, expected);
let prev_dt = 15.minutes().prev(dt);
let expected = parse_from_rfc3339("2018-09-04T14:15:00-00:00");
assert_eq!(prev_dt, expected);
let prev_dt = 15.minutes().prev(expected);
let expected = parse_from_rfc3339("2018-09-04T14:00:00-00:00");
assert_eq!(prev_dt, expected);
let prev_dt = 1.minute().prev(dt);
let expected = parse_from_rfc3339("2018-09-04T14:22:00-00:00");
assert_eq!(prev_dt, expected);
let prev_dt = 2.hours().prev(dt);
let expected = parse_from_rfc3339("2018-09-04T14:00:00-00:00");
assert_eq!(prev_dt, expected);
let prev_dt = 2.hours().prev(expected);
let expected = parse_from_rfc3339("2018-09-04T12:00:00-00:00");
assert_eq!(prev_dt, expected);
let prev_dt = 1.hour().prev(dt);
let expected = parse_from_rfc3339("2018-09-04T14:00:00-00:00");
assert_eq!(prev_dt, expected);
let prev_dt = 2.days().prev(dt);
let expected = parse_from_rfc3339("2018-09-03T00:00:00-00:00");
assert_eq!(prev_dt, expected);
let prev_dt = 2.days().prev(expected);
let expected = parse_from_rfc3339("2018-09-01T00:00:00-00:00");
assert_eq!(prev_dt, expected);
let prev_dt = 1.day().prev(dt);
let expected = parse_from_rfc3339("2018-09-04T00:00:00-00:00");
assert_eq!(prev_dt, expected);
let prev_dt = 2.weeks().prev(dt);
let expected = parse_from_rfc3339("2018-08-27T00:00:00-00:00");
assert_eq!(prev_dt, expected);
let prev_dt = 2.weeks().prev(expected);
let expected = parse_from_rfc3339("2018-08-13T00:00:00-00:00");
assert_eq!(prev_dt, expected);
let prev_dt = 1.week().prev(dt);
let expected = parse_from_rfc3339("2018-09-03T00:00:00-00:00");
assert_eq!(prev_dt, expected);
let prev_dt = Monday.prev(dt);
let expected = parse_from_rfc3339("2018-09-03T00:00:00-00:00");
assert_eq!(prev_dt, expected);
let prev_dt = Monday.prev(expected);
let expected = parse_from_rfc3339("2018-08-27T00:00:00-00:00");
assert_eq!(prev_dt, expected);
let prev_dt = Wednesday.prev(dt);
let expected = parse_from_rfc3339("2018-08-29T00:00:00-00:00");
assert_eq!(prev_dt, expected);
let prev_dt = Wednesday.prev(expected);
let expected = parse_from_rfc3339("2018-08-22T00:00:00-00:00");
assert_eq!(prev_dt, expected);
let saturday = Saturday.prev(dt);
let prev_dt = Workday.prev(saturday);
let expected = parse_from_rfc3339("2018-08-31T00:00:00-00:00");
assert_eq!(prev_dt, expected);
let prev_dt = Workday.prev(expected);
let expected = parse_from_rfc3339("2018-08-30T00:00:00-00:00");
assert_eq!(prev_dt, expected);
}
#[test]
fn test_parse_time() {
assert_eq!(
parse_time("14:52:13"),
Ok(Time::from_hms(14, 52, 13).unwrap())
);
assert_eq!(
parse_time("2:52:13 pm"),
Ok(Time::from_hms(14, 52, 13).unwrap())
);
assert_eq!(parse_time("14:52"), Ok(Time::from_hms(14, 52, 0).unwrap()));
assert_eq!(
parse_time("2:52 PM"),
Ok(Time::from_hms(14, 52, 0).unwrap())
);
}
#[test]
fn test_run_config() {
let rc = RunConfig::from_interval(1.day())
.with_time(Time::from_hms(15, 0, 0).unwrap());
let dt = parse_from_rfc3339("2018-09-04T14:22:13-00:00");
let next_dt = rc.next(dt);
let expected = parse_from_rfc3339("2018-09-04T15:00:00-00:00");
assert_eq!(next_dt, expected);
let rc = RunConfig::from_interval(Tuesday)
.with_time(Time::from_hms(15, 0, 0).unwrap());
let dt = parse_from_rfc3339("2018-09-04T14:22:13-00:00");
let next_dt = rc.next(dt);
let expected = parse_from_rfc3339("2018-09-04T15:00:00-00:00");
assert_eq!(next_dt, expected);
let rc = RunConfig::from_interval(Tuesday)
.with_time(Time::from_hms(14, 0, 0).unwrap());
let next_dt = rc.next(dt);
let expected = parse_from_rfc3339("2018-09-11T14:00:00-00:00");
assert_eq!(next_dt, expected);
let rc = RunConfig::from_interval(Tuesday)
.with_subinterval(6.hours())
.with_subinterval(5.minutes());
let next_dt = rc.next(dt);
let expected = parse_from_rfc3339("2018-09-11T06:05:00-00:00");
assert_eq!(next_dt, expected);
}
#[test]
fn test_division_by_zero() {
let dt = parse_from_rfc3339("2018-09-04T14:22:13-00:00");
assert_eq!(0.seconds().next(dt), dt, "next 0 seconds");
assert_eq!(0.seconds().prev(dt), dt, "previous 0 seconds");
assert_eq!(0.minutes().next(dt), dt, "next 0 minutes");
assert_eq!(0.minutes().prev(dt), dt, "prev 0 minutes");
assert_eq!(0.hours().next(dt), dt, "next 0 hours");
assert_eq!(0.hours().prev(dt), dt, "prev 0 hours");
assert_eq!(0.days().next(dt), dt, "next 0 days");
assert_eq!(0.days().prev(dt), dt, "prev 0 days");
assert_eq!(0.weeks().next(dt), dt, "next 0 weeks");
assert_eq!(0.weeks().prev(dt), dt, "prev 0 weeks");
}
#[test]
fn test_daily_interval_plus_time_of_midnight() {
let dt = parse_from_rfc3339("2018-09-04T14:22:13-00:00");
let rc =
RunConfig::from_interval(Tuesday).with_time(Time::from_hms(0, 0, 0).unwrap());
let next_dt = rc.next(dt);
let expected = parse_from_rfc3339("2018-09-11T00:00:00-00:00");
assert_eq!(next_dt, expected);
}
}