diff --git a/Cargo.toml b/Cargo.toml index cae0d90..30fbd12 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ resolver = "2" members = [ - "leptos_webpage", + "leptos_webpage", "session_iter", ] [profile] diff --git a/leptos_webpage/Cargo.toml b/leptos_webpage/Cargo.toml index 3e7bfff..255443c 100644 --- a/leptos_webpage/Cargo.toml +++ b/leptos_webpage/Cargo.toml @@ -13,4 +13,5 @@ leptos = { version = "0.7", features = ["csr"] } console_error_panic_hook = "0.1.7" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -clap = { version = "4.5", features = ["derive"] } \ No newline at end of file +clap = { version = "4.5", features = ["derive"] } +session_iter = { path = "../session_iter"} \ No newline at end of file diff --git a/leptos_webpage/src/session.rs b/leptos_webpage/src/session.rs index 8351433..1f86678 100644 --- a/leptos_webpage/src/session.rs +++ b/leptos_webpage/src/session.rs @@ -3,22 +3,6 @@ use chrono::NaiveDate; use serde::{Deserialize, Serialize}; use std::collections::HashMap; -#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] -pub struct OldDatedSession { - pub day: Day, - pub session: Session, - pub applying_exception: Option, -} - -impl Noted for OldDatedSession { - fn get_note(&self) -> Option<&String> { - match &self.applying_exception { - Some(e) => e.get_note(), - None => self.session.get_note(), - } - } -} - #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] pub enum DatedSession { Regular { diff --git a/session_iter/Cargo.toml b/session_iter/Cargo.toml new file mode 100644 index 0000000..59e1ac1 --- /dev/null +++ b/session_iter/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "session_iter" +version = "0.1.0" +edition = "2021" + +[dependencies] +chrono = { version = "0.4", features = ["serde"] } +serde = { version = "1.0", features = ["derive"] } +clap = { version = "4.5", features = ["derive"] } \ No newline at end of file diff --git a/session_iter/src/day.rs b/session_iter/src/day.rs new file mode 100644 index 0000000..4db6ced --- /dev/null +++ b/session_iter/src/day.rs @@ -0,0 +1,49 @@ +use chrono::{Datelike, Local, NaiveDate, Weekday}; +use serde::{Deserialize, Serialize}; +use std::ops::Deref; + +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)] +#[repr(transparent)] +pub struct Day { + date: NaiveDate, +} + +impl From for Day { + fn from(date: NaiveDate) -> Self { + Self { date } + } +} + +impl Default for Day { + fn default() -> Self { + Self::from(Local::now().date_naive()) + } +} + +impl Day { + pub fn date(&self) -> NaiveDate { + self.date + } + + pub fn weekday(&self) -> Weekday { + self.date.weekday() + } + + pub fn weekday_of_month(&self) -> u8 { + self.date.day0() as u8 / 7 + 1 + } +} + +impl Deref for Day { + type Target = NaiveDate; + + fn deref(&self) -> &Self::Target { + &self.date + } +} + +impl From for NaiveDate { + fn from(day: Day) -> Self { + day.date + } +} \ No newline at end of file diff --git a/session_iter/src/lib.rs b/session_iter/src/lib.rs new file mode 100644 index 0000000..e884830 --- /dev/null +++ b/session_iter/src/lib.rs @@ -0,0 +1,5 @@ +pub mod day; +pub mod session; +#[cfg(test)] +mod test_util; + diff --git a/session_iter/src/main.rs b/session_iter/src/main.rs new file mode 100644 index 0000000..e71fdf5 --- /dev/null +++ b/session_iter/src/main.rs @@ -0,0 +1 @@ +fn main() {} \ No newline at end of file diff --git a/session_iter/src/session.rs b/session_iter/src/session.rs new file mode 100644 index 0000000..51134a4 --- /dev/null +++ b/session_iter/src/session.rs @@ -0,0 +1,187 @@ +pub mod iter; +pub mod rule; + +use crate::day::Day; +use crate::session::rule::{SessionRule, SessionRuleLike, WeekdayOfMonth}; +use chrono::NaiveDate; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; + +type Note = Option; + +macro_rules! impl_opt_noted { + ($ty: ident) => { + impl OptNoted for $ty { + fn note(&self) -> Option<&str> { + self.note.as_ref().map(String::as_str) + } + } + }; + ($ty: ident with $($variant: ident),+) => { + impl OptNoted for $ty { + fn note(&self) -> Option<&str> { + match self { + $(Self::$variant(opt_noted) => opt_noted.note(),)+ + } + } + } + }; +} + +macro_rules! impl_with_note { + ($ty: ident) => { + impl WithNote for $ty { + fn with_note(self, note: &str) -> Self { + #[allow(clippy::needless_update)] + Self { + note: Some(note.to_owned()), + ..self + } + } + } + }; +} + +macro_rules! impl_from { + ($what: ident for $ty: ident by $intermediate: ident) => { + impl From<$what> for $ty { + fn from(value: $what) -> Self { + Self::from($intermediate::from(value)) + } + } + }; + ($what: ident for $ty: ident as $variant: ident) => { + impl From<$what> for $ty { + fn from(value: $what) -> Self { + Self::$variant(value) + } + } + }; + ($what: ident for $ty: ident) => { + impl_from!($what for $ty as $what); + }; +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] +pub enum Session { + Regular(RegularSession), + Extra(ExtraSession), +} + +impl_from!(RegularSession for Session as Regular); +impl_from!(ExtraSession for Session as Extra); + +#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] +pub struct RegularSession { + pub rule: SessionRule, + pub note: Note, + pub except: BTreeMap, +} + +impl From for RegularSession { + fn from(rule: SessionRule) -> Self { + Self { + rule, + note: None, + except: Default::default(), + } + } +} + +impl RegularSession { + pub fn except(mut self, day: D, except: E) -> Self + where + D: Into, + E: Into, + { + self.except.insert(day.into(), except.into()); + self + } + + pub fn next_regular_session_day(&self, current_date: D) -> Option + where + D: Into, + { + self + .rule + .to_session_day_iter(current_date) + .find(|day| !self.except.contains_key(day)) + } +} + +impl_from!(WeekdayOfMonth for RegularSession by SessionRule); +impl_opt_noted!(RegularSession); +impl_with_note!(RegularSession); + +#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)] +pub struct ExtraSession { + pub day: Day, + pub note: Note, +} + +impl From for ExtraSession { + fn from(day: Day) -> Self { + Self { day, note: None } + } +} + +impl_from!(NaiveDate for ExtraSession by Day); +impl_opt_noted!(ExtraSession); +impl_with_note!(ExtraSession); + +impl Dated for ExtraSession { + fn day(&self) -> Day { + self.day + } +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] +pub enum Except { + Alternation(Alternation), + Cancellation(Cancellation), +} + +impl_from!(Alternation for Except); +impl_from!(Cancellation for Except); +impl_opt_noted!(Except with Alternation, Cancellation); + +#[derive(Debug, Clone, Eq, PartialEq, Hash, Default, Serialize, Deserialize)] +pub struct Alternation { + pub note: Note, + pub day: Option, +} + +impl From for Alternation { + fn from(day: Day) -> Self { + Self { + day: Some(day), + ..Default::default() + } + } +} + +impl_from!(NaiveDate for Alternation by Day); +impl_opt_noted!(Alternation); +impl_with_note!(Alternation); + +#[derive(Debug, Clone, Eq, PartialEq, Hash, Default, Serialize, Deserialize)] +#[serde(default)] +pub struct Cancellation { + pub note: Note, +} + +impl_opt_noted!(Cancellation); +impl_with_note!(Cancellation); + +pub trait Dated { + /// The day when this should show up in a calendar + fn day(&self) -> Day; +} + +pub trait OptNoted { + fn note(&self) -> Option<&str>; +} + +pub trait WithNote { + fn with_note(self, note: &str) -> Self; +} diff --git a/session_iter/src/session/iter.rs b/session_iter/src/session/iter.rs new file mode 100644 index 0000000..addcaef --- /dev/null +++ b/session_iter/src/session/iter.rs @@ -0,0 +1,123 @@ +use crate::day::Day; +use crate::session::{ + Alternation, Cancellation, Dated, Except, ExtraSession, OptNoted, RegularSession, Session, +}; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; +use std::hash::Hash; + +#[derive(Debug, Clone)] +pub struct DatedSessionIter { + current_date: Day, + sessions: BTreeMap>, +} + +impl DatedSessionIter { + pub fn new(start_date: D, sessions: S) -> DatedSessionIter + where + D: Into, + S: IntoIterator, + { + let current_date = start_date.into(); + + //map every session and their exceptions to a date when they should show up in the calendar + let sessions = + sessions + .into_iter() + .flat_map(|session| match session { + Session::Regular(session) => session + .except + .iter() + .filter(|(&day, _)| current_date <= day) + .map(|(&day, except)| match except { + Except::Alternation(alternation) => DatedSession::Altered { + day: alternation.day.unwrap_or(day), + regular: session.clone(), + cause: alternation.clone(), + }, + Except::Cancellation(cancellation) => DatedSession::Canceled { + day, + regular: session.clone(), + cause: cancellation.clone(), + }, + }) + .chain(session.next_regular_session_day(current_date).map(|day| { + DatedSession::Regular { + day, + session: session.clone(), + } + })) + .collect(), + Session::Extra(extra) => { + //filter out old extra sessions + if current_date <= extra.day() { + vec![extra.into()] + } else { + vec![] + } + } + }) + .fold(BTreeMap::<_, Vec<_>>::new(), |mut map, dated_session| { + map + .entry(dated_session.day()) + .or_default() + .push(dated_session); + map + }); + + Self { + current_date, + sessions, + } + } +} + +//TODO iter implementation for DatedSessionIter + +#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] +pub enum DatedSession { + Regular { + day: Day, + session: RegularSession, + }, + Extra(ExtraSession), + Canceled { + day: Day, + regular: RegularSession, + cause: Cancellation, + }, + Altered { + ///the day when the regular session would have applied + day: Day, + regular: RegularSession, + cause: Alternation, + }, +} + +impl From for DatedSession { + fn from(value: ExtraSession) -> Self { + Self::Extra(value) + } +} + +impl Dated for DatedSession { + fn day(&self) -> Day { + match *self { + DatedSession::Regular { day, .. } => day, + DatedSession::Extra(ref extra) => extra.day(), + DatedSession::Canceled { day, .. } => day, + DatedSession::Altered { day, ref cause, .. } => cause.day.unwrap_or(day), + } + } +} + +impl OptNoted for DatedSession { + fn note(&self) -> Option<&str> { + match self { + DatedSession::Regular { session, .. } => session.note(), + DatedSession::Extra(extra) => extra.note(), + DatedSession::Canceled { cause, .. } => cause.note(), + DatedSession::Altered { cause, .. } => cause.note(), + } + } +} diff --git a/session_iter/src/session/rule.rs b/session_iter/src/session/rule.rs new file mode 100644 index 0000000..8ac0490 --- /dev/null +++ b/session_iter/src/session/rule.rs @@ -0,0 +1,149 @@ +use crate::day::Day; +use chrono::{Datelike, Months, NaiveDate, Weekday}; +use serde::{Deserialize, Serialize}; +use std::ops::Deref; + +#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] +pub enum SessionRule { + WeekdayOfMonth(WeekdayOfMonth), +} + +impl From for SessionRule { + fn from(value: WeekdayOfMonth) -> Self { + Self::WeekdayOfMonth(value) + } +} + +impl SessionRule { + pub fn to_session_day_iter(&self, start_date: D) -> SessionDayIter<&Self> + where + D: Into, + { + SessionDayIter { + rule: self, + current_date: Some(start_date.into()), + } + } +} + +impl SessionRuleLike for SessionRule { + fn determine_next_date(&self, current_date: Day) -> Option { + match self { + SessionRule::WeekdayOfMonth(weekday_of_month) => { + weekday_of_month.determine_next_date(current_date) + } + } + } + + fn accepts(&self, day: Day) -> bool { + match self { + SessionRule::WeekdayOfMonth(weekday_of_month) => weekday_of_month.accepts(day), + } + } +} + +#[derive(Debug, Clone)] +pub struct SessionDayIter { + pub rule: R, + pub current_date: Option, +} + +impl Iterator for SessionDayIter +where + R: Deref, +{ + type Item = Day; + + fn next(&mut self) -> Option { + self.current_date = self + .current_date + .and_then(|date| self.rule.determine_next_date(date)); + self.current_date + } +} + +impl SessionDayIter +where + R: Deref, +{ + pub fn to_owned(&self) -> SessionDayIter { + SessionDayIter { + rule: self.rule.clone(), + current_date: self.current_date, + } + } +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] +pub struct WeekdayOfMonth { + pub n: u8, + pub weekday: Weekday, +} + +impl WeekdayOfMonth { + pub fn new(n: u8, weekday: Weekday) -> Self { + Self { n, weekday } + } +} + +impl SessionRuleLike for WeekdayOfMonth { + fn determine_next_date(&self, current_date: Day) -> Option { + let session_this_month = NaiveDate::from_weekday_of_month_opt( + current_date.year(), + current_date.month(), + self.weekday, + self.n, + )?; + + let date = if session_this_month >= current_date.date() { + session_this_month + } else { + let session_next_month = current_date.checked_add_months(Months::new(1))?; + NaiveDate::from_weekday_of_month_opt( + session_next_month.year(), + session_next_month.month(), + self.weekday, + self.n, + )? + }; + + Some(date.into()) + } + + fn accepts(&self, day: Day) -> bool { + day.weekday() == self.weekday && day.weekday_of_month() == self.n + } +} + +pub trait SessionRuleLike { + /// Determines the next session date in form of a [Day], possibly including the `start_date`. + fn determine_next_date(&self, current_date: Day) -> Option; + + /// Whether this rule would be able to produce the given `day`. + fn accepts(&self, day: Day) -> bool; +} + +#[cfg(test)] +mod test_weekday_of_month { + use crate::session::rule::{SessionRuleLike, WeekdayOfMonth}; + use crate::test_util::day; + use chrono::Weekday; + + #[test] + fn test_next_date() { + let rule = WeekdayOfMonth::new(3, Weekday::Tue); + + assert_eq!( + rule.determine_next_date(day(17, 2, 2025)), + Some(day(18, 2, 2025)) + ); + assert_eq!( + rule.determine_next_date(day(18, 2, 2025)), + Some(day(18, 2, 2025)) + ); + assert_eq!( + rule.determine_next_date(day(19, 2, 2025)), + Some(day(18, 3, 2025)) + ); + } +} diff --git a/session_iter/src/test_util.rs b/session_iter/src/test_util.rs new file mode 100644 index 0000000..9261d54 --- /dev/null +++ b/session_iter/src/test_util.rs @@ -0,0 +1,10 @@ +use crate::day::Day; +use chrono::NaiveDate; + +pub fn date(day: u32, month: u32, year: i32) -> NaiveDate { + NaiveDate::from_ymd_opt(year, month, day).expect("valid date") +} + +pub fn day(day: u32, month: u32, year: i32) -> Day { + date(day, month, year).into() +} \ No newline at end of file