From 94bc259daa3f81d8692853fc038d5aad472b3a21 Mon Sep 17 00:00:00 2001 From: Caleb Adepitan Date: Sat, 2 Nov 2024 10:02:27 +0100 Subject: [PATCH] updates: rename folder, improve code, add tests --- .../scheduler/src/{scheduler => core}/cron.rs | 2 +- .../src/{scheduler => core}/frequency.rs | 347 ++++++++++++++---- .../scheduler/src/{scheduler => core}/mod.rs | 0 .../src/{scheduler => core}/priority.rs | 0 .../src/{scheduler => core}/schedule.rs | 176 +++++++-- .../src/{scheduler => core}/scheduler.rs | 57 ++- .../scheduler/src/{scheduler => core}/time.rs | 22 +- packages/scheduler/src/lib.rs | 52 ++- packages/scheduler/tests/schedule.rs | 99 +++++ packages/scheduler/tests/web.rs | 45 +++ 10 files changed, 688 insertions(+), 112 deletions(-) rename packages/scheduler/src/{scheduler => core}/cron.rs (92%) rename packages/scheduler/src/{scheduler => core}/frequency.rs (65%) rename packages/scheduler/src/{scheduler => core}/mod.rs (100%) rename packages/scheduler/src/{scheduler => core}/priority.rs (100%) rename packages/scheduler/src/{scheduler => core}/schedule.rs (61%) rename packages/scheduler/src/{scheduler => core}/scheduler.rs (70%) rename packages/scheduler/src/{scheduler => core}/time.rs (91%) create mode 100644 packages/scheduler/tests/schedule.rs diff --git a/packages/scheduler/src/scheduler/cron.rs b/packages/scheduler/src/core/cron.rs similarity index 92% rename from packages/scheduler/src/scheduler/cron.rs rename to packages/scheduler/src/core/cron.rs index 739f1c5..79cadaf 100644 --- a/packages/scheduler/src/scheduler/cron.rs +++ b/packages/scheduler/src/core/cron.rs @@ -1,6 +1,6 @@ use wasm_bindgen::prelude::*; -use crate::scheduler::frequency::StFrequencyType; +use crate::core::frequency::StFrequencyType; #[wasm_bindgen] #[derive(Debug, Clone, PartialEq, Eq)] diff --git a/packages/scheduler/src/scheduler/frequency.rs b/packages/scheduler/src/core/frequency.rs similarity index 65% rename from packages/scheduler/src/scheduler/frequency.rs rename to packages/scheduler/src/core/frequency.rs index a17a44f..0088eba 100644 --- a/packages/scheduler/src/scheduler/frequency.rs +++ b/packages/scheduler/src/core/frequency.rs @@ -1,4 +1,5 @@ -use std::{cmp::Ordering, f32::INFINITY}; +use core::fmt; +use std::cmp::Ordering; use wasm_bindgen::prelude::*; @@ -31,7 +32,7 @@ pub enum StOrdinals { } #[wasm_bindgen] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum StMonth { Jan, Feb, @@ -108,14 +109,14 @@ pub struct StDailyExpression { #[wasm_bindgen] #[derive(Debug, Clone, PartialEq, Eq)] pub struct StWeeklySubExpression { - weekdays: Vec, + pub(crate) weekdays: Vec, } #[wasm_bindgen] #[derive(Debug, Clone, PartialEq, Eq)] pub struct StWeeklyExpression { pub every: u8, - subexpr: StWeeklySubExpression, + pub(crate) subexpr: StWeeklySubExpression, } #[wasm_bindgen] @@ -170,7 +171,7 @@ pub struct StYearlyExpression { pub struct StRegularFrequency { pub ftype: StFrequencyType, pub until: Option, - expr: StFrequencyExpression, + pub(crate) expr: StFrequencyExpression, } #[wasm_bindgen] @@ -183,26 +184,65 @@ pub struct StCustomFrequency { cron_expressions: Vec, } -pub trait HasEvery { - fn every(&self) -> u32; +pub trait Repeating { + fn every(&self) -> u64; +} + +impl StOrdinals { + /// Gets the value, from 0-4 and 255, of the given enum variant + #[rustfmt::skip] + pub fn to_value(variant: &Self) -> u8 { + match variant { + Self::First => 0x00, + Self::Second => 0x01, + Self::Third => 0x02, + Self::Fourth => 0x03, + Self::Fifth => 0x04, + Self::Last => 0xFF, + } + } + + /// Gets the enum variant from a value between 0-4 and 255 + /// + /// # Panics + /// + /// When the supplied value is out of bounds, that is, greater than 4 and less than 255 + pub fn from_value(value: &u8) -> Self { + match value { + 0x00 => Self::First, + 0x01 => Self::Second, + 0x02 => Self::Third, + 0x03 => Self::Fourth, + 0x04 => Self::Fifth, + 0xFF => Self::Last, + _ => panic!( + "Unknown value \"{}\". Allowed values are 0x00...0x04 and 0xFF", + value + ), + } + } +} + +impl fmt::Display for StOrdinals { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + StOrdinals::First => "First", + StOrdinals::Second => "Second", + StOrdinals::Third => "Thitd", + StOrdinals::Fourth => "Fourth", + StOrdinals::Fifth => "Fifth", + StOrdinals::Last => "Last", + }) + } } impl Ord for StOrdinals { fn cmp(&self, other: &Self) -> Ordering { - let value_of = |v: &Self| match v { - Self::First => 1f32, - Self::Second => 2f32, - Self::Third => 3f32, - Self::Fourth => 4f32, - Self::Fifth => 5f32, - Self::Last => INFINITY, - }; - - let self_value = value_of(&self); - let other_value = value_of(&other); + let self_value = Self::to_value(self); + let other_value = Self::to_value(other); // place other before self for an inverse ordering - other_value.total_cmp(&self_value) + other_value.cmp(&self_value) } } @@ -212,9 +252,10 @@ impl PartialOrd for StOrdinals { } } -impl Ord for StConstWeekday { - fn cmp(&self, other: &Self) -> Ordering { - let value_of = |v: &Self| match v { +impl StConstWeekday { + /// Gets the value, from 0-6, for the given enum variant + pub fn to_value(variant: &Self) -> u8 { + match &variant { Self::Sun => 0, Self::Mon => 1, Self::Tue => 2, @@ -222,10 +263,73 @@ impl Ord for StConstWeekday { Self::Thu => 4, Self::Fri => 5, Self::Sat => 6, - }; + } + } + + /// Gets the enum variant from a value between 0-6 + /// + /// # Panics + /// + /// When the supplied value is out of bounds, that is, greater than 6 + pub fn from_value(value: &u8) -> Self { + match value { + 0 => StConstWeekday::Sun, + 1 => StConstWeekday::Mon, + 2 => StConstWeekday::Tue, + 3 => StConstWeekday::Wed, + 4 => StConstWeekday::Thu, + 5 => StConstWeekday::Fri, + 6 => StConstWeekday::Sat, + _ => panic!( + "Weekday \"{}\" out of bounds. Allowed values are 0...6", + value + ), + } + } - let self_value = value_of(&self); - let other_value = value_of(&other); + pub fn to_chrono_weekday(weekday: &Self) -> chrono::Weekday { + match weekday { + StConstWeekday::Sun => chrono::Weekday::Sun, + StConstWeekday::Mon => chrono::Weekday::Mon, + StConstWeekday::Tue => chrono::Weekday::Tue, + StConstWeekday::Wed => chrono::Weekday::Wed, + StConstWeekday::Thu => chrono::Weekday::Thu, + StConstWeekday::Fri => chrono::Weekday::Fri, + StConstWeekday::Sat => chrono::Weekday::Sat, + } + } + + pub fn from_chrono_weekday(chrono_weekday: &chrono::Weekday) -> Self { + match chrono_weekday { + chrono::Weekday::Sun => StConstWeekday::Sun, + chrono::Weekday::Mon => StConstWeekday::Mon, + chrono::Weekday::Tue => StConstWeekday::Tue, + chrono::Weekday::Wed => StConstWeekday::Wed, + chrono::Weekday::Thu => StConstWeekday::Thu, + chrono::Weekday::Fri => StConstWeekday::Fri, + chrono::Weekday::Sat => StConstWeekday::Sat, + } + } +} + +impl fmt::Display for StConstWeekday { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + StConstWeekday::Sun => "Sun", + StConstWeekday::Mon => "Mon", + StConstWeekday::Tue => "Tue", + StConstWeekday::Wed => "Wed", + StConstWeekday::Thu => "Thu", + StConstWeekday::Fri => "Fri", + StConstWeekday::Sat => "Sat", + }) + } +} + +impl Ord for StConstWeekday { + fn cmp(&self, other: &Self) -> Ordering { + let self_value = Self::to_value(self); + let other_value = Self::to_value(other); self_value.cmp(&other_value) } @@ -237,9 +341,10 @@ impl PartialOrd for StConstWeekday { } } -impl Ord for StMonth { - fn cmp(&self, other: &Self) -> Ordering { - let value_of = |m: &Self| match m { +impl StMonth { + /// Gets the value, from 0-11, for the given enum variant + pub fn to_value(variant: &Self) -> u8 { + match variant { Self::Jan => 0, Self::Feb => 1, Self::Mar => 2, @@ -252,10 +357,83 @@ impl Ord for StMonth { Self::Oct => 9, Self::Nov => 10, Self::Dec => 11, - }; + } + } + + /// Gets the enum variant from a value between 0-11 + /// + /// # Panics + /// + /// When the supplied value is out of bounds, that is, greater than 11 + pub fn from_value(value: &u8) -> Self { + match value { + 0 => Self::Jan, + 1 => Self::Feb, + 2 => Self::Mar, + 3 => Self::Apr, + 4 => Self::May, + 5 => Self::Jun, + 6 => Self::Jul, + 7 => Self::Aug, + 8 => Self::Sep, + 9 => Self::Oct, + 10 => Self::Nov, + 11 => Self::Dec, + _ => panic!( + "Month \"{}\" out of bounds. Allowed values are 0...11", + value + ), + } + } + + pub fn number_of_days(&self) -> u8 { + match self { + Self::Jan => 31, + Self::Feb => 28, + Self::Mar => 31, + Self::Apr => 30, + Self::May => 31, + Self::Jun => 30, + Self::Jul => 31, + Self::Aug => 31, + Self::Sep => 30, + Self::Oct => 31, + Self::Nov => 30, + Self::Dec => 31, + } + } + + pub fn number_of_leap_days(&self) -> u8 { + match self { + Self::Feb => 29, + _ => self.number_of_days(), + } + } +} + +impl fmt::Display for StMonth { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + StMonth::Jan => "Jan", + StMonth::Feb => "Feb", + StMonth::Mar => "Mar", + StMonth::Apr => "Apr", + StMonth::May => "May", + StMonth::Jun => "Jun", + StMonth::Jul => "Jul", + StMonth::Aug => "Aug", + StMonth::Sep => "Sep", + StMonth::Oct => "Oct", + StMonth::Nov => "Nov", + StMonth::Dec => "Dec", + }) + } +} - let self_value = value_of(&self); - let other_value = value_of(&other); +impl Ord for StMonth { + fn cmp(&self, other: &Self) -> Ordering { + let self_value = Self::to_value(self); + let other_value = Self::to_value(other); self_value.cmp(&other_value) } @@ -267,15 +445,31 @@ impl PartialOrd for StMonth { } } -impl HasEvery for StFrequencyExpression { - fn every(&self) -> u32 { - match &self { - Self::Daily(v) => v.every as u32, - Self::Hourly(v) => v.every as u32, - Self::Monthly(v) => v.every as u32, - Self::Weekly(v) => v.every as u32, - Self::Yearly(v) => v.every as u32, - } +impl Repeating for StFrequencyExpression { + fn every(&self) -> u64 { + (match &self { + Self::Daily(v) => v.every, + Self::Hourly(v) => v.every, + Self::Monthly(v) => v.every, + Self::Weekly(v) => v.every, + Self::Yearly(v) => v.every, + }) as u64 + } +} + +#[wasm_bindgen] +impl StHourlyExpression { + #[wasm_bindgen(constructor)] + pub fn new(every: u8) -> Self { + StHourlyExpression { every } + } +} + +#[wasm_bindgen] +impl StDailyExpression { + #[wasm_bindgen(constructor)] + pub fn new(every: u8) -> Self { + StDailyExpression { every } } } @@ -292,20 +486,15 @@ impl StWeeklySubExpression { let weekdays = weekdays .iter() .take(7) - .map(move |w| match w { - 0 => StConstWeekday::Sun, - 1 => StConstWeekday::Mon, - 2 => StConstWeekday::Tue, - 3 => StConstWeekday::Wed, - 4 => StConstWeekday::Thu, - 5 => StConstWeekday::Fri, - 6 => StConstWeekday::Sat, - _ => panic!("Weekday \"{}\" out of bounds. Allowed values are 0...6", w), - }) + .map(|w| StConstWeekday::from_value(w)) .collect::>(); StWeeklySubExpression { weekdays } } + + pub(crate) fn get_weekdays(&self) -> &Vec { + &self.weekdays + } } #[wasm_bindgen] @@ -323,6 +512,11 @@ impl StWeeklyExpression { subexpr: StWeeklySubExpression::new(weekdays), } } + + #[inline] + pub(crate) fn get_subexpr(&self) -> &StWeeklySubExpression { + &self.subexpr + } } #[wasm_bindgen] @@ -333,7 +527,7 @@ impl StMonthlyOnDaysSubExpression { /// /// # Panics /// - /// If the size of days exceeds the maximum, `31` different possible days in a month + /// If the size of days exceeds the maximum, `31`, different, possible days in a month pub fn new(days: Vec) -> Self { assert!(days.len() <= 31, "Size of `days` cannot be more than 31"); StMonthlyOnDaysSubExpression { days } @@ -417,21 +611,7 @@ impl StYearlyInSubExpression { let months = months .iter() .take(12) - .map(|m| match m { - 0 => StMonth::Jan, - 1 => StMonth::Feb, - 2 => StMonth::Mar, - 3 => StMonth::Apr, - 4 => StMonth::May, - 5 => StMonth::Jun, - 6 => StMonth::Jul, - 7 => StMonth::Aug, - 8 => StMonth::Sep, - 9 => StMonth::Oct, - 10 => StMonth::Nov, - 11 => StMonth::Dec, - _ => panic!("Month \"{}\" out of bounds. Allowed values are 0...11", m), - }) + .map(|m| StMonth::from_value(m)) .collect::>(); StYearlyInSubExpression { months } @@ -547,6 +727,41 @@ impl StYearlyExpression { pub fn new(every: u8, subexpr: StYearlySubExpression) -> Self { StYearlyExpression { every, subexpr } } + + pub fn with_months(every: u8, months: Vec) -> Self { + StYearlyExpression { + every, + subexpr: StYearlySubExpression::with_months(months), + } + } + + pub fn with_months_ordinal_const_weekday( + every: u8, + months: Vec, + ordinal: StOrdinals, + weekday: StConstWeekday, + ) -> Self { + StYearlyExpression { + every, + subexpr: StYearlySubExpression::with_months_ordinal_const_weekday( + months, ordinal, weekday, + ), + } + } + + pub fn with_months_ordinal_var_weekday( + every: u8, + months: Vec, + ordinal: StOrdinals, + weekday: StVarWeekday, + ) -> Self { + StYearlyExpression { + every, + subexpr: StYearlySubExpression::with_months_ordinal_var_weekday( + months, ordinal, weekday, + ), + } + } } #[wasm_bindgen] diff --git a/packages/scheduler/src/scheduler/mod.rs b/packages/scheduler/src/core/mod.rs similarity index 100% rename from packages/scheduler/src/scheduler/mod.rs rename to packages/scheduler/src/core/mod.rs diff --git a/packages/scheduler/src/scheduler/priority.rs b/packages/scheduler/src/core/priority.rs similarity index 100% rename from packages/scheduler/src/scheduler/priority.rs rename to packages/scheduler/src/core/priority.rs diff --git a/packages/scheduler/src/scheduler/schedule.rs b/packages/scheduler/src/core/schedule.rs similarity index 61% rename from packages/scheduler/src/scheduler/schedule.rs rename to packages/scheduler/src/core/schedule.rs index 8a48935..cf78cf2 100644 --- a/packages/scheduler/src/scheduler/schedule.rs +++ b/packages/scheduler/src/core/schedule.rs @@ -1,18 +1,22 @@ +use core::fmt; use std::cmp::{max, Ordering}; +use chrono::prelude::*; use wasm_bindgen::prelude::*; -use crate::scheduler::frequency::{HasEvery, StCustomFrequency}; -use crate::scheduler::frequency::{StFrequency, StFrequencyType, StRegularFrequency}; -use crate::scheduler::priority::StPriority; -use crate::scheduler::time::{parse_cron_expr, utc_timestamp, Timestamp}; -use crate::scheduler::time::{DAY_MILLIS, HOUR_MILLIS, WEEK_MILLIS}; +use crate::core::frequency::StConstWeekday; +use crate::core::frequency::StFrequencyExpression; +use crate::core::frequency::{Repeating, StCustomFrequency}; +use crate::core::frequency::{StFrequency, StRegularFrequency}; +use crate::core::priority::StPriority; +use crate::core::time::{parse_cron_expr, utc_timestamp, Timestamp}; +use crate::core::time::{DAY_MILLIS, HOUR_MILLIS, WEEK_MILLIS}; #[wasm_bindgen] #[derive(Debug, Clone, PartialEq, Eq)] pub struct StSchedule { id: String, - timestamp: u64, + timestamp: Timestamp, priority: Option, frequency: Option, } @@ -35,7 +39,7 @@ impl StSchedule { id: String::from(id), frequency: None, priority, - timestamp, + timestamp: Timestamp::Millis(timestamp), } } @@ -60,7 +64,7 @@ impl StSchedule { id: String::from(id), frequency: Some(StFrequency::Regular(freq)), priority, - timestamp, + timestamp: Timestamp::Millis(timestamp), } } @@ -85,12 +89,12 @@ impl StSchedule { id: String::from(id), frequency: Some(StFrequency::Custom(freq)), priority, - timestamp, + timestamp: Timestamp::Millis(timestamp), } } - fn next_hourly_timestamp(timestamp: Timestamp, hours: u32) -> Timestamp { - let hour_ms = (HOUR_MILLIS * hours) as f64; + fn next_hourly_timestamp(timestamp: Timestamp, hours: u64) -> Timestamp { + let hour_ms = (HOUR_MILLIS * hours as u64) as f64; let current_timestamp = utc_timestamp(); if timestamp >= current_timestamp { @@ -105,7 +109,7 @@ impl StSchedule { timestamp + Timestamp::Millis(next_hour_millis as u64) } - fn next_daily_timestamp(timestamp: Timestamp, days: u32) -> Timestamp { + fn next_daily_timestamp(timestamp: Timestamp, days: u64) -> Timestamp { let day_ms = (DAY_MILLIS * days) as f64; let current_timestamp = utc_timestamp(); @@ -121,7 +125,7 @@ impl StSchedule { timestamp + Timestamp::Millis(next_day_millis as u64) } - fn next_weekly_timestamp(timestamp: Timestamp, weeks: u32) -> Timestamp { + fn next_weekly_timestamp(timestamp: Timestamp, weeks: u64) -> Timestamp { let week_ms = (WEEK_MILLIS * weeks) as f64; let current_timestamp = utc_timestamp(); @@ -137,6 +141,43 @@ impl StSchedule { timestamp + Timestamp::Millis(next_week_millis as u64) } + /// Compute the correction factor that places a weekly `timestamp` at exactly the weekday, + /// in the same week as the `timestamp`, specified in its schedule frequency. + /// + /// It calculates the time in milliseconds that takes the `timestamp` either ahead or behind to + /// exactly the weekday, from `weekdays`, that outputs the least time in milliseconds. + /// + /// This method should be used after [`StSchedule::next_weekly_timestamp`](#method.next_weekly_timestamp) + /// when `weekdays` is provided. + /// + /// Returns a tuple of `(neg: bool, correction: Timestamp)` + fn next_weekly_correction_factor( + timestamp: &Timestamp, + weekdays: &Vec, + ) -> (bool, Timestamp) { + let minimum = weekdays + .iter() + .map(|weekday| { + let datetime = timestamp.to_datetime(); + let ts_weekday = StConstWeekday::from_chrono_weekday(&datetime.weekday()); + + // This computes how many days behind or ahead, in the same week, is the + // timestamp from the day of the specified weekday + let weekday_offset = StConstWeekday::to_value(&ts_weekday) as i64 + - StConstWeekday::to_value(weekday) as i64; + + (DAY_MILLIS as i64) * weekday_offset * -1 + }) + .min() + .unwrap_or(0); + + if minimum.signum() == -1 { + return (true, Timestamp::Millis(minimum as u64)); + } + + return (false, Timestamp::Millis(minimum as u64)); + } + // pub(crate) fn get_id_as_str(&self) -> &str { // self.id.as_str() // } @@ -146,7 +187,7 @@ impl StSchedule { } pub fn get_timestamp(&self) -> u64 { - self.timestamp + self.timestamp.as_ms() } pub fn get_priority(&self) -> Option { @@ -178,7 +219,7 @@ impl StSchedule { } pub fn is_passed(&self) -> bool { - Timestamp::Millis(self.timestamp) < utc_timestamp() + self.timestamp < utc_timestamp() } /// Calculate the upcoming schedule for the given schedule @@ -197,12 +238,10 @@ impl StSchedule { .iter() .take(3) .map(|c| { - let ref_timestamp = Timestamp::Millis(self.timestamp); - let cur_timestamp = utc_timestamp(); let result = parse_cron_expr( c.as_str(), cstm_freq.tz_offset, - Some(max(ref_timestamp, cur_timestamp)), + Some(max(&self.timestamp, &utc_timestamp())), ); result @@ -211,7 +250,7 @@ impl StSchedule { }) .min(); - timestamp.map(move |t| { + timestamp.map(|t| { StSchedule::with_custom( &self.id, t.as_ms(), @@ -223,10 +262,10 @@ impl StSchedule { // For regular frequencies, match the repetition frequency type and calculate // the timestamp in milliseconds for the upcoming schedule - StFrequency::Regular(reg_freq) => match reg_freq.ftype { - StFrequencyType::Hour => { + StFrequency::Regular(reg_freq) => match reg_freq.get_expr() { + StFrequencyExpression::Hourly(_expr) => { let next_timestamp = Self::next_hourly_timestamp( - Timestamp::Millis(self.timestamp), + self.timestamp, reg_freq.get_expr().every(), ); @@ -238,11 +277,9 @@ impl StSchedule { )) } - StFrequencyType::Day => { - let next_timestamp = Self::next_daily_timestamp( - Timestamp::Millis(self.timestamp), - reg_freq.get_expr().every(), - ); + StFrequencyExpression::Daily(_expr) => { + let next_timestamp = + Self::next_daily_timestamp(self.timestamp, reg_freq.get_expr().every()); Some(StSchedule::with_regular( &self.id, @@ -252,12 +289,25 @@ impl StSchedule { )) } - StFrequencyType::Week => { - let next_timestamp = Self::next_weekly_timestamp( - Timestamp::Millis(self.timestamp), + StFrequencyExpression::Weekly(expr) => { + let mut next_timestamp = Self::next_weekly_timestamp( + self.timestamp, reg_freq.get_expr().every(), ); + let subexpr = expr.get_subexpr(); + + let (neg, correction) = Self::next_weekly_correction_factor( + &next_timestamp, + subexpr.get_weekdays(), + ); + + if neg { + next_timestamp -= correction; + } else { + next_timestamp += correction + } + // TODO: Check for specific days of the week and apply correction to either move // the timestamp forward or backward depending on if the specifies weekday(s) is // after or before the weekday of the timestamp. @@ -272,7 +322,7 @@ impl StSchedule { _ => Some(StSchedule::with_regular( &self.id, - self.timestamp, + self.timestamp.as_ms(), reg_freq.to_owned(), self.priority, )), @@ -283,6 +333,12 @@ impl StSchedule { } } +impl fmt::Display for StSchedule { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:#?}", &self) + } +} + impl Ord for StSchedule { fn cmp(&self, other: &Self) -> std::cmp::Ordering { let order = self.timestamp.cmp(&other.timestamp); @@ -299,3 +355,61 @@ impl PartialOrd for StSchedule { Some(self.cmp(other)) } } + +#[cfg(test)] +mod tests { + use super::*; + use wasm_bindgen_test::*; + + wasm_bindgen_test_configure!(run_in_browser); + + static TIMESTAMP: Timestamp = Timestamp::Millis(1729520340000); + + #[allow(dead_code)] + #[wasm_bindgen_test] + pub fn test_next_hourly_schedule() { + { + let hours = 1; + let next_timestamp = StSchedule::next_hourly_timestamp(TIMESTAMP, hours); + + assert_eq!( + ((next_timestamp.as_ms() - TIMESTAMP.as_ms()) / HOUR_MILLIS) % hours, + 0, + ) + } + + { + let hours = 4; + let next_timestamp = StSchedule::next_hourly_timestamp(TIMESTAMP, hours); + + assert_eq!( + ((next_timestamp.as_ms() - TIMESTAMP.as_ms()) / HOUR_MILLIS) % hours, + 0, + ) + } + } + + #[allow(dead_code)] + #[wasm_bindgen_test] + pub fn test_next_weekly_schedule() { + { + let weeks = 1; + let next_timestamp = StSchedule::next_weekly_timestamp(TIMESTAMP, weeks); + + assert_eq!( + ((next_timestamp.as_ms() - TIMESTAMP.as_ms()) / WEEK_MILLIS) % weeks, + 0, + ) + } + + { + let weeks = 3; + let next_timestamp = StSchedule::next_weekly_timestamp(TIMESTAMP, weeks); + + assert_eq!( + ((next_timestamp.as_ms() - TIMESTAMP.as_ms()) / WEEK_MILLIS) % weeks, + 0, + ) + } + } +} diff --git a/packages/scheduler/src/scheduler/scheduler.rs b/packages/scheduler/src/core/scheduler.rs similarity index 70% rename from packages/scheduler/src/scheduler/scheduler.rs rename to packages/scheduler/src/core/scheduler.rs index f2df971..7ede130 100644 --- a/packages/scheduler/src/scheduler/scheduler.rs +++ b/packages/scheduler/src/core/scheduler.rs @@ -5,9 +5,9 @@ use wasm_bindgen::prelude::*; use crate::console_log; use crate::queue::priority_queue::{Comparator, PriorityQueue}; -use crate::scheduler::schedule::StSchedule; -use crate::scheduler::time::utc_timestamp; -use crate::scheduler::time::Timestamp; +use crate::core::schedule::StSchedule; +use crate::core::time::utc_timestamp; +use crate::core::time::Timestamp; pub struct ClosureComparator where @@ -157,11 +157,60 @@ impl StSchedulerRunner { StSchedulerRunner { scheduler } } + async fn idle(&self, till: Option) { + let sleep_ts = till.unwrap_or_else(|| Timestamp::Millis(1000)); + + console_log!("scheduler idling for {} milliseconds", sleep_ts); + + task::sleep(sleep_ts.to_std_duration()).await + } + pub fn stop(&mut self) { self.scheduler.abort(); } - pub fn run(&mut self) { + pub async fn run(&mut self) { self.scheduler.unabort(); + let sleep_ts = Timestamp::Millis(1000); + + console_log!("scheduler running"); + + loop { + if self.scheduler.isaborted() { + break; + } + + if self.scheduler.pq.is_empty() { + self.idle(None).await; + continue; + } + + let peeked = self.scheduler.pq.peek().unwrap(); + let peeked_id = peeked.get_id(); + let timestamp = Timestamp::Millis(peeked.get_timestamp()); + let now = utc_timestamp(); + let difference = timestamp - now; + + if timestamp > now { + console_log!("there are no due schedules yet"); + console_log!("timestamp: {}; current time: {}", timestamp, now); + + if difference > sleep_ts { + self.idle(Some(sleep_ts)).await; + continue; + } + continue; + } else if timestamp >= now { + console_log!("dispatching one schedule found to be due"); + + let item = self.scheduler.pq.dequeue().unwrap(); + let item_id = item.get_id(); + assert_eq!(item_id, peeked_id); + self.scheduler + .receiver + .as_ref() + .map(|r| r.call1(&JsValue::NULL, &JsValue::from_str(item.get_id().as_str()))); + } + } } } diff --git a/packages/scheduler/src/scheduler/time.rs b/packages/scheduler/src/core/time.rs similarity index 91% rename from packages/scheduler/src/scheduler/time.rs rename to packages/scheduler/src/core/time.rs index 0c9fe50..cd5e39e 100644 --- a/packages/scheduler/src/scheduler/time.rs +++ b/packages/scheduler/src/core/time.rs @@ -1,12 +1,12 @@ use chrono::prelude::*; use cron_parser::{parse, ParseError}; use std::cmp::Ordering; -use std::ops::{Add, Div, Sub}; +use std::ops::{Add, AddAssign, Div, Sub, SubAssign}; use std::time::{SystemTime, UNIX_EPOCH}; -pub const HOUR_MILLIS: u32 = 3_600_000; -pub const DAY_MILLIS: u32 = HOUR_MILLIS * 24; -pub const WEEK_MILLIS: u32 = DAY_MILLIS * 7; +pub const HOUR_MILLIS: u64 = 3_600_000; +pub const DAY_MILLIS: u64 = HOUR_MILLIS * 24; +pub const WEEK_MILLIS: u64 = DAY_MILLIS * 7; #[derive(Debug, Clone, Copy)] pub enum Timestamp { @@ -90,6 +90,18 @@ impl Sub for Timestamp { } } +impl AddAssign for Timestamp { + fn add_assign(&mut self, rhs: Self) { + *self = self.add(rhs); + } +} + +impl SubAssign for Timestamp { + fn sub_assign(&mut self, rhs: Self) { + *self = self.sub(rhs); + } +} + impl PartialEq for Timestamp { fn eq(&self, other: &Self) -> bool { self.as_ms() == other.as_ms() @@ -148,7 +160,7 @@ pub fn utc_timestamp() -> Timestamp { pub fn parse_cron_expr( expression: &str, tz_offset: i32, - start_timestamp: Option, + start_timestamp: Option<&Timestamp>, ) -> Result, ParseError> { let offset = FixedOffset::east_opt(tz_offset.div(1_000)) .unwrap_or_else(|| FixedOffset::east_opt(0).unwrap()); diff --git a/packages/scheduler/src/lib.rs b/packages/scheduler/src/lib.rs index d83c55f..382771a 100644 --- a/packages/scheduler/src/lib.rs +++ b/packages/scheduler/src/lib.rs @@ -1,11 +1,14 @@ mod utils; -mod queue; -mod scheduler; +pub mod core; +pub mod queue; use wasm_bindgen::prelude::*; -use scheduler::scheduler::StScheduler; +use core::{ + frequency::{StConstWeekday, StOrdinals}, + scheduler::StScheduler, +}; #[wasm_bindgen] extern "C" { @@ -19,7 +22,7 @@ extern "C" { #[macro_export] macro_rules! console_log { ($($t:tt)*) => { - let time = crate::scheduler::time::utc_timestamp().to_datetime().format("%Y-%m-%dT%H:%M:%S%.3f%z"); + let time = crate::core::time::utc_timestamp().to_datetime().format("%Y-%m-%dT%H:%M:%S%.3f%z"); $crate::log(&format!("[Scheduler] - {} INFO {}", time, &format!($($t)*))); }; } @@ -27,7 +30,7 @@ macro_rules! console_log { #[macro_export] macro_rules! console_error { ($($t:tt)*) => { - let time = crate::scheduler::time::utc_timestamp().to_datetime().format("%Y-%m-%dT%H:%M:%S%.3f%z"); + let time = crate::core::time::utc_timestamp().to_datetime().format("%Y-%m-%dT%H:%M:%S%.3f%z"); $crate::error(&format!("[Scheduler] - {} ERROR {}", time, &format!($($t)*))); }; } @@ -38,3 +41,42 @@ pub fn get_scheduler() -> StScheduler { scheduler } + +/// Gets the enum variant from a value between 0-6 +/// +/// # Panics +/// +/// When the supplied value is out of bounds, that is, greater than 6 +#[wasm_bindgen] +pub fn st_const_weekday_from_value(value: u8) -> StConstWeekday { + StConstWeekday::from_value(&value) +} + +/// Gets the enum variant from a value between 0-4 and 255 +/// +/// 0-4 are First to Fifth, respectively, consecutively, and 255 is Last +/// +/// # Panics +/// +/// When the supplied value is out of bounds, that is, greater than 4 and less than 255 +#[wasm_bindgen] +pub fn st_ordinals_from_value(value: u8) -> StOrdinals { + StOrdinals::from_value(&value) +} + +// macro_rules! doc_inherited { +// ($name:ident) => { +// #[doc = "This function performs some operation."] +// pub fn $name() { +// println!("{} called", stringify!($name)); +// } +// }; +// } + +// doc_inherited!(function_a); +// doc_inherited!(function_b); + +// fn main() { +// function_a(); // Output: function_a called +// function_b(); // Output: function_b called +// } diff --git a/packages/scheduler/tests/schedule.rs b/packages/scheduler/tests/schedule.rs new file mode 100644 index 0000000..c017714 --- /dev/null +++ b/packages/scheduler/tests/schedule.rs @@ -0,0 +1,99 @@ +//! Test suite for the Web and headless browsers. + +use std::{cmp::max, vec}; + +use chrono::{DateTime, Timelike, Utc}; +// extern crate wasm_bindgen_test; +use wasm_bindgen_test::*; + +extern crate scheduler; +use scheduler::core::{ + frequency::{StCustomFrequency, StFrequencyType, StHourlyExpression, StRegularFrequency}, + priority::StPriority, + schedule::*, +}; + +wasm_bindgen_test_configure!(run_in_browser); + +// static ISO_DATE_STRING: &str = "2024-10-28T21:05:55.025Z"; +static TIMESTAMP_MILLIS: u64 = 1730149555025; + +/// Should sucessfilly create a bare minimum schedule with the given data +#[wasm_bindgen_test] +pub fn pass_create_bare_minimum_schedule() { + let schedule = StSchedule::new("id", TIMESTAMP_MILLIS, None); + + assert_eq!(schedule.get_id(), "id"); + assert_eq!(schedule.get_priority(), None); + assert_eq!(schedule.get_timestamp(), TIMESTAMP_MILLIS); + assert_eq!(schedule.get_custom_frequency(), None); + assert_eq!(schedule.get_regular_frequency(), None); +} + +/// Should successfully create a more robust schedule +#[wasm_bindgen_test] +pub fn pass_create_robust_schedule() { + { + let cstm_freq = StCustomFrequency::new(-3_600_000, vec!["*/10 * * * *".to_string()], None); + let schedule = StSchedule::with_custom( + "id", + TIMESTAMP_MILLIS, + cstm_freq.clone(), + Some(StPriority::High), + ); + + assert_eq!(schedule.get_id(), "id"); + assert_eq!(schedule.get_priority(), Some(StPriority::High)); + assert_eq!(schedule.get_timestamp(), TIMESTAMP_MILLIS); + assert_eq!(schedule.get_custom_frequency(), Some(cstm_freq)); + assert_eq!(schedule.get_regular_frequency(), None); + } + + { + let reg_freq = + StRegularFrequency::new(StFrequencyType::Hour, StHourlyExpression::new(1), None); + let schedule = StSchedule::with_regular( + "id", + TIMESTAMP_MILLIS, + reg_freq.clone(), + Some(StPriority::High), + ); + + assert_eq!(schedule.get_id(), "id"); + assert_eq!(schedule.get_priority(), Some(StPriority::High)); + assert_eq!(schedule.get_timestamp(), TIMESTAMP_MILLIS); + assert_eq!(schedule.get_regular_frequency(), Some(reg_freq)); + assert_eq!(schedule.get_custom_frequency(), None); + } +} + +#[wasm_bindgen_test] +pub fn pass_get_upcoming_cron_schedule() { + let cstm_freq = StCustomFrequency::new(-3_600_000, vec!["*/10 * * * *".to_string()], None); + + let schedule = StSchedule::with_custom( + "id", + TIMESTAMP_MILLIS, + cstm_freq.clone(), + Some(StPriority::High), + ); + + let time_t = DateTime::from_timestamp_millis(schedule.get_timestamp() as i64).unwrap(); + + let upcoming_schedule = schedule.get_upcoming_schedule().unwrap(); + + assert_ne!(schedule, upcoming_schedule); + + // Ensure the greatest of `time_t` and `now` is used + let after_time_t = max(Utc::now(), time_t); + let upcoming_time_t = + DateTime::from_timestamp_millis(upcoming_schedule.get_timestamp() as i64).unwrap(); + + if after_time_t.minute() <= 50 { + assert_eq!(after_time_t.hour(), upcoming_time_t.hour()); + assert_eq!(upcoming_time_t.minute() % 10, 0); + } else { + assert_eq!(upcoming_time_t.hour(), (after_time_t.hour() + 1) % 24); + assert_eq!(upcoming_time_t.minute(), 0); + } +} diff --git a/packages/scheduler/tests/web.rs b/packages/scheduler/tests/web.rs index de5c1da..e9cbfd0 100644 --- a/packages/scheduler/tests/web.rs +++ b/packages/scheduler/tests/web.rs @@ -11,3 +11,48 @@ wasm_bindgen_test_configure!(run_in_browser); fn pass() { assert_eq!(1 + 1, 2); } + + +// &.collapsed { +// & .s-subtask-listitem { +// margin-block-end: 0; + +// &:nth-last-child(1) { +// --color: var(--p-green-300); +// --color: var(--p-slate-200); +// } + +// &:nth-last-child(2) { +// --color: var(--p-red-300); +// --color: var(--p-slate-400); +// } + +// &:nth-last-child(n + 2) { +// /* max-height: 1rem; */ +// overflow: hidden; +// } + +// &:nth-last-child(n + 3) { +// --color: var(--p-amber-300); +// --color: var(--p-slate-600); +// } + +// --deviation: calc(var(--s-index) - var(--mid)); +// --factor: max(var(--deviation), 0); + +// transform: translate3d( +// 0, +// calc(1rem * var(--factor) - 100% * var(--s-index)), +// /* calc((-100% * var(--s-index)) + 1rem * var(--s-index)), */ calc(-1px * var(--n-index)) +// ) +// scale(calc(1 - (0.05 * var(--n-index)))); + +// & .s-subtask-element { +// background-color: rgba( +// from var(--s-surface-ground) r g b / calc(var(--s-index) / var(--size)) +// ); /* rgb(from var(--color) r g b / calc(0.5 - (var(--n-index) / 10)));*/ +// backdrop-filter: blur(15px); +// /* filter: saturate(calc(10% * var(--s-index) + 30%)); */ +// } +// } +// } \ No newline at end of file