use chrono::{Datelike, Duration, NaiveDate, NaiveDateTime, NaiveTime, Timelike}; use karlcommon::{Condition, Property}; use std::{ cmp::{max, min}, ops::Range, }; use Direction::*; use Edge::*; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Edge { Start, End, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Direction { Forward, Backward, } pub trait ConditionFind { fn find(&self, edge: Edge, dir: Direction, from: NaiveDateTime) -> Option; fn find_inverse_inclusive( &self, edge: Edge, dir: Direction, from: NaiveDateTime, ) -> Option { let s = self.find(edge, dir, from); let e = self.find(edge.invert(), dir, from); // we are "inside" match (s, e) { (Some(s), Some(e)) => { if s > e { self.find(edge, dir.invert(), from) } else { Some(s) } } (None, Some(_)) => self.find(edge, dir.invert(), from), (Some(s), None) => Some(s), (None, None) => None, } } fn find_instance(&self, dir: Direction, from: NaiveDateTime) -> Range> { let start = self.find(Edge::Start, dir, from); let end = self.find(Edge::End, dir, start.unwrap_or(from)); start..end } } impl ConditionFind for Condition { fn find(&self, edge: Edge, dir: Direction, mut from: NaiveDateTime) -> Option { match self { Condition::Never => None, Condition::And(cs) => loop { // TODO improve efficiency for backward search // TODO fix endless search let last_start = cs .iter() .map(|c| c.find_inverse_inclusive(Start, dir, from)) .reduce(|a, b| Some(max(a?, b?)))?; let first_end = cs .iter() .map(|c| c.find(End, dir, from)) .reduce(|a, b| Some(min(a?, b?)))?; match last_start { Some(start) => match first_end { Some(end) => { if end > start { break Some(match edge { Start => start, End => end, }); } else { from = start - Duration::seconds(1); // TODO proper fix } } None => break Some(start), }, None => break None, } }, Condition::Or(cs) => { cs.iter() .filter_map(|c| c.find(edge, dir, from)) .reduce(match dir { Forward => min, Backward => max, }) } Condition::From(_c) => todo!(), Condition::Invert(c) => c.find(edge.invert(), dir, from), Condition::Equal { prop, value, modulus: _, } => { let value = *value; let off: i64 = match edge { Start => 0, End => 1, }; let dir_off = match dir { Forward => 0, Backward => -1, }; match prop { Property::Year => { let geq = match dir { Forward => |a, b| a >= b, Backward => |a, b| a < b, }; if geq(from.year(), (value + off) as i32) { None } else { Some(NaiveDateTime::new( NaiveDate::from_ymd_opt((value + off) as i32, 1, 1).unwrap(), NaiveTime::from_hms_opt(0, 0, 0).unwrap(), )) } } Property::Monthofyear => { let rollover = (value + off) / 12; let value = (value + off) % 12; // month still coming for this year if from.month0() < value as u32 { Some(NaiveDateTime::new( NaiveDate::from_ymd_opt( from.year() + (rollover + dir_off) as i32, value as u32 + 1, 1, ) .unwrap(), NaiveTime::from_hms_opt(0, 0, 0).unwrap(), )) } else { Some(NaiveDateTime::new( NaiveDate::from_ymd_opt( from.year() + (rollover + dir_off + 1) as i32, value as u32 + 1, 1, ) .unwrap(), NaiveTime::from_hms_opt(0, 0, 0).unwrap(), )) } } Property::Weekofmonth => todo!(), Property::Dayofyear => { todo!() } Property::Dayofmonth => { // let mut target = NaiveDateTime::new( // NaiveDate::from_ymd(from.year(), from.month(), value as u32), // NaiveTime::from_hms(0, 0, 0), // ); // if edge == End { // target += Duration::days(1) // } // fn increment_month(d: NaiveDateTime) -> NaiveDateTime { // NaiveDateTime::new( // NaiveDate::from_ymd( // d.year() + (d.month() as i32 / 12), // (d.month() + 1) % 12, // d.day(), // ), // NaiveTime::from_hms(d.hour(), d.minute(), d.second()), // ) // } // let dir_off = match dir { // Forward => |d| d, // Backward => increment_month, // }; // if target > from { // Some(dir_off(target)) // } else { // Some(increment_month(dir_off(target))) // } todo!() } Property::Dayofweek => todo!(), Property::Hour => { let mut target = NaiveDateTime::new( NaiveDate::from_ymd_opt(from.year(), from.month(), from.day()).unwrap(), NaiveTime::from_hms_opt(value as u32, 0, 0).unwrap(), ); if edge == End { target += Duration::hours(1) } let dir_off = match dir { Forward => Duration::zero(), Backward => Duration::days(-1), }; if target > from { Some(target + dir_off) } else { Some(target + dir_off + Duration::days(1)) } } Property::Minute => { let mut target = NaiveDateTime::new( NaiveDate::from_ymd_opt(from.year(), from.month(), from.day()).unwrap(), NaiveTime::from_hms_opt(from.hour(), value as u32, 0).unwrap(), ); if edge == End { target += Duration::minutes(1) } let dir_off = match dir { Forward => Duration::zero(), Backward => Duration::hours(-1), }; if target > from { Some(target + dir_off) } else { Some(target + dir_off + Duration::hours(1)) } } Property::Second => { let mut target = NaiveDateTime::new( NaiveDate::from_ymd_opt(from.year(), from.month(), from.day()).unwrap(), NaiveTime::from_hms_opt(from.hour(), from.minute(), value as u32) .unwrap(), ); if edge == End { target += Duration::seconds(1) } let dir_off = match dir { Forward => Duration::zero(), Backward => Duration::minutes(-1), }; if target > from { Some(target + dir_off) } else { Some(target + dir_off + Duration::minutes(1)) } } Property::Unix => { let geq = match dir { Forward => |a, b| a >= b, Backward => |a, b| a < b, }; if geq(from.timestamp(), (value + off) as i64) { None } else { Some(NaiveDateTime::from_timestamp_opt(value, 0).unwrap()) } } } } Condition::Range { prop, min, max, modulus, } => { // TODO assert_eq!(*modulus, None); assert_eq!(*prop, Property::Unix); assert_eq!(dir, Direction::Forward); let min = NaiveDateTime::from_timestamp_opt(*min, 0).unwrap(); let max = NaiveDateTime::from_timestamp_opt(*max, 0).unwrap(); match edge { Start => { if min < from { None } else { Some(min) } } End => { if max < from { None } else { Some(max) } } } } } } } impl Edge { pub fn invert(self) -> Self { match self { Edge::Start => Edge::End, Edge::End => Edge::Start, } } } impl Direction { pub fn invert(self) -> Self { match self { Direction::Forward => Direction::Backward, Direction::Backward => Direction::Forward, } } } #[cfg(test)] mod test { use super::{Condition, ConditionFind, Direction, Edge, Property}; use chrono::{NaiveDateTime, Utc}; use std::str::FromStr; use Direction::*; use Edge::*; #[test] fn blub() { let cond = Condition::And(vec![ Condition::Equal { modulus: None, prop: Property::Monthofyear, value: 1, }, Condition::Equal { modulus: None, prop: Property::Hour, value: 12, }, ]); let dt = Utc::now().naive_utc(); println!("START FORWARD => {:?}", cond.find(Start, Forward, dt)); println!("END FORWARD => {:?}", cond.find(End, Forward, dt)); println!("START BACKWARD => {:?}", cond.find(Start, Backward, dt)); println!("END BACKWARD => {:?}", cond.find(End, Backward, dt)); } #[test] fn year_equal() { let cond = Condition::Equal { modulus: None, prop: Property::Year, value: 2023, }; let dt = NaiveDateTime::from_str("2022-06-07T13:37:48").unwrap(); assert_eq!( cond.find(Edge::Start, Direction::Forward, dt).unwrap(), NaiveDateTime::from_str("2023-01-01T00:00:00").unwrap(), ); assert_eq!( cond.find(Edge::End, Direction::Forward, dt).unwrap(), NaiveDateTime::from_str("2024-01-01T00:00:00").unwrap(), ); assert_eq!(cond.find(Edge::End, Direction::Backward, dt), None); assert_eq!(cond.find(Edge::Start, Direction::Backward, dt), None); } #[test] fn month_equal() { let cond = Condition::Equal { modulus: None, prop: Property::Monthofyear, value: 3, }; let dt = NaiveDateTime::from_str("2022-06-07T13:37:48").unwrap(); assert_eq!( cond.find(Edge::Start, Direction::Forward, dt), Some(NaiveDateTime::from_str("2023-04-01T00:00:00").unwrap()) ); assert_eq!( cond.find(Edge::End, Direction::Forward, dt), Some(NaiveDateTime::from_str("2023-05-01T00:00:00").unwrap()) ); assert_eq!( cond.find(Edge::Start, Direction::Backward, dt), Some(NaiveDateTime::from_str("2022-04-01T00:00:00").unwrap()) ); assert_eq!( cond.find(Edge::End, Direction::Backward, dt), Some(NaiveDateTime::from_str("2022-05-01T00:00:00").unwrap()) ); } }