use chrono::offset::TimeZone;
use chrono::{DateTime, Datelike, Timelike, Utc};
use std::fmt::{Display, Formatter, Result as FmtResult};
use std::ops::Bound::{Included, Unbounded};
use crate::ordinal::*;
use crate::queries::*;
use crate::time_unit::*;
impl From<Schedule> for String {
fn from(schedule: Schedule) -> String {
schedule.source
}
}
#[derive(Clone, Debug, Eq)]
pub struct Schedule {
source: String,
fields: ScheduleFields,
}
impl Schedule {
pub(crate) fn new(source: String, fields: ScheduleFields) -> Schedule {
Schedule { source, fields }
}
pub fn next_after(&self, after: &u64) -> Option<u64> {
let mut query = NextAfterQuery::from(after);
for year in self
.fields
.years
.ordinals()
.range((Included(query.year_lower_bound()), Unbounded))
.cloned()
{
let month_start = query.month_lower_bound();
if !self.fields.months.ordinals().contains(&month_start) {
query.reset_month();
}
let month_range = (Included(month_start), Included(Months::inclusive_max()));
for month in self.fields.months.ordinals().range(month_range).cloned() {
let day_of_month_start = query.day_of_month_lower_bound();
if !self
.fields
.days_of_month
.ordinals()
.contains(&day_of_month_start)
{
query.reset_day_of_month();
}
let day_of_month_end = days_in_month(month, year);
let day_of_month_range = (Included(day_of_month_start), Included(day_of_month_end));
'day_loop: for day_of_month in self
.fields
.days_of_month
.ordinals()
.range(day_of_month_range)
.cloned()
{
let hour_start = query.hour_lower_bound();
if !self.fields.hours.ordinals().contains(&hour_start) {
query.reset_hour();
}
let hour_range = (Included(hour_start), Included(Hours::inclusive_max()));
for hour in self.fields.hours.ordinals().range(hour_range).cloned() {
let minute_start = query.minute_lower_bound();
if !self.fields.minutes.ordinals().contains(&minute_start) {
query.reset_minute();
}
let minute_range =
(Included(minute_start), Included(Minutes::inclusive_max()));
for minute in self.fields.minutes.ordinals().range(minute_range).cloned() {
let second_start = query.second_lower_bound();
if !self.fields.seconds.ordinals().contains(&second_start) {
query.reset_second();
}
let second_range =
(Included(second_start), Included(Seconds::inclusive_max()));
for second in
self.fields.seconds.ordinals().range(second_range).cloned()
{
let rem = *after % 1_000_000;
let secs = ((*after - rem) / 1_000_000_000) + 1;
let timezone =
Utc.timestamp_opt(secs as i64, 0).unwrap().timezone();
let candidate = if let Some(candidate) = timezone
.with_ymd_and_hms(
year as i32,
month,
day_of_month,
hour,
minute,
second,
)
.single()
{
candidate
} else {
continue;
};
if !self
.fields
.days_of_week
.ordinals()
.contains(&candidate.weekday().number_from_sunday())
{
continue 'day_loop;
}
return Some(candidate.timestamp_nanos_opt().unwrap() as u64);
}
query.reset_minute();
} query.reset_hour();
} query.reset_day_of_month();
} query.reset_month();
} }
None
}
pub fn upcoming(&self) -> ScheduleIterator {
self.after(&(Utc::now().naive_utc().timestamp_nanos_opt().unwrap() as u64))
}
pub fn after(&self, after: &u64) -> ScheduleIterator {
ScheduleIterator::new(self, after)
}
pub fn includes<Z>(&self, date_time: DateTime<Z>) -> bool
where
Z: TimeZone,
{
self.fields.years.includes(date_time.year() as Ordinal)
&& self.fields.months.includes(date_time.month() as Ordinal)
&& self
.fields
.days_of_week
.includes(date_time.weekday().number_from_sunday())
&& self
.fields
.days_of_month
.includes(date_time.day() as Ordinal)
&& self.fields.hours.includes(date_time.hour() as Ordinal)
&& self.fields.minutes.includes(date_time.minute() as Ordinal)
&& self.fields.seconds.includes(date_time.second() as Ordinal)
}
pub fn years(&self) -> &impl TimeUnitSpec {
&self.fields.years
}
pub fn months(&self) -> &impl TimeUnitSpec {
&self.fields.months
}
pub fn days_of_month(&self) -> &impl TimeUnitSpec {
&self.fields.days_of_month
}
pub fn days_of_week(&self) -> &impl TimeUnitSpec {
&self.fields.days_of_week
}
pub fn hours(&self) -> &impl TimeUnitSpec {
&self.fields.hours
}
pub fn minutes(&self) -> &impl TimeUnitSpec {
&self.fields.minutes
}
pub fn seconds(&self) -> &impl TimeUnitSpec {
&self.fields.seconds
}
pub fn timeunitspec_eq(&self, other: &Schedule) -> bool {
self.fields == other.fields
}
}
impl Display for Schedule {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
write!(f, "{}", self.source)
}
}
impl PartialEq for Schedule {
fn eq(&self, other: &Schedule) -> bool {
self.source == other.source
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ScheduleFields {
years: Years,
days_of_week: DaysOfWeek,
months: Months,
days_of_month: DaysOfMonth,
hours: Hours,
minutes: Minutes,
seconds: Seconds,
}
impl ScheduleFields {
pub(crate) fn new(
seconds: Seconds,
minutes: Minutes,
hours: Hours,
days_of_month: DaysOfMonth,
months: Months,
days_of_week: DaysOfWeek,
years: Years,
) -> ScheduleFields {
ScheduleFields {
years,
days_of_week,
months,
days_of_month,
hours,
minutes,
seconds,
}
}
}
pub struct ScheduleIterator<'a> {
is_done: bool,
schedule: &'a Schedule,
previous_datetime: u64,
}
impl<'a> ScheduleIterator<'a> {
fn new(schedule: &'a Schedule, starting_datetime: &u64) -> ScheduleIterator<'a> {
ScheduleIterator {
is_done: false,
schedule,
previous_datetime: *starting_datetime,
}
}
}
impl<'a> Iterator for ScheduleIterator<'a> {
type Item = u64;
fn next(&mut self) -> Option<u64> {
if self.is_done {
return None;
}
if let Some(next_datetime) = self.schedule.next_after(&self.previous_datetime) {
self.previous_datetime = next_datetime;
Some(next_datetime)
} else {
self.is_done = true;
None
}
}
}
fn is_leap_year(year: Ordinal) -> bool {
let by_four = year % 4 == 0;
let by_hundred = year % 100 == 0;
let by_four_hundred = year % 400 == 0;
by_four && ((!by_hundred) || by_four_hundred)
}
fn days_in_month(month: Ordinal, year: Ordinal) -> u32 {
let is_leap_year = is_leap_year(year);
match month {
9 | 4 | 6 | 11 => 30,
2 if is_leap_year => 29,
2 => 28,
_ => 31,
}
}
#[cfg(test)]
mod test {
use super::*;
use std::str::FromStr;
#[test]
fn test_next_duration() {
let expression = "0 5,13,40-42 17 1 Jan *";
let schedule = Schedule::from_str(expression).unwrap();
let next = schedule.next_after(&(Utc::now().timestamp_nanos_opt().unwrap() as u64));
println!("NEXT DURATION------- for {} {:?}", expression, next);
assert!(next.is_some());
}
#[test]
fn test_next_after() {
let expression = "0 5,13,40-42 17 1 Jan *";
let schedule = Schedule::from_str(expression).unwrap();
let next = schedule.next_after(&(Utc::now().timestamp_nanos_opt().unwrap() as u64));
println!("NEXT AFTER for {} {:?}", expression, next);
assert!(next.is_some());
}
#[test]
fn test_upcoming_utc() {
let expression = "0 0,30 0,6,12,18 1,15 Jan-March Thurs";
let schedule = Schedule::from_str(expression).unwrap();
let mut upcoming = schedule.upcoming();
let next1 = upcoming.next();
assert!(next1.is_some());
let next2 = upcoming.next();
assert!(next2.is_some());
let next3 = upcoming.next();
assert!(next3.is_some());
println!("Upcoming 1 for {} {:?}", expression, next1);
println!("Upcoming 2 for {} {:?}", expression, next2);
println!("Upcoming 3 for {} {:?}", expression, next3);
}
#[test]
fn test_schedule_to_string() {
let expression = "* 1,2,3 * * * *";
let schedule: Schedule = Schedule::from_str(expression).unwrap();
let result = String::from(schedule);
assert_eq!(expression, result);
}
#[test]
fn test_display_schedule() {
use std::fmt::Write;
let expression = "@monthly";
let schedule = Schedule::from_str(expression).unwrap();
let mut result = String::new();
write!(result, "{}", schedule).unwrap();
assert_eq!(expression, result);
}
#[test]
fn test_valid_from_str() {
let schedule = Schedule::from_str("0 0,30 0,6,12,18 1,15 Jan-March Thurs");
schedule.unwrap();
}
#[test]
fn test_invalid_from_str() {
let schedule = Schedule::from_str("cheesecake 0,30 0,6,12,18 1,15 Jan-March Thurs");
assert!(schedule.is_err());
}
#[test]
fn test_time_unit_spec_equality() {
let schedule_1 = Schedule::from_str("@weekly").unwrap();
let schedule_2 = Schedule::from_str("0 0 0 * * 1 *").unwrap();
let schedule_3 = Schedule::from_str("0 0 0 * * 1-7 *").unwrap();
let schedule_4 = Schedule::from_str("0 0 0 * * * *").unwrap();
assert_ne!(schedule_1, schedule_2);
assert!(schedule_1.timeunitspec_eq(&schedule_2));
println!("sc3:{schedule_3:?}\nsc4:{schedule_4:?}");
assert!(schedule_3.timeunitspec_eq(&schedule_4));
}
}