From fe9c97bb19c3e8e9d686d682061d21abfd2dac23 Mon Sep 17 00:00:00 2001 From: Steppy Date: Sun, 16 Feb 2025 17:47:33 +0100 Subject: [PATCH 1/7] Migrate project to workspace --- Cargo.toml | 20 +++--------- README.md | 32 +++---------------- leptos_webpage/Cargo.toml | 16 ++++++++++ leptos_webpage/README.md | 32 +++++++++++++++++++ Trunk.toml => leptos_webpage/Trunk.toml | 2 +- index.html => leptos_webpage/index.html | 0 leptosfmt.sh => leptos_webpage/leptosfmt.sh | 0 rustfmt.toml => leptos_webpage/rustfmt.toml | 0 {src => leptos_webpage/src}/bin/cli.rs | 0 .../src}/bin/create_default_configs.rs | 0 {src => leptos_webpage/src}/lib.rs | 0 {src => leptos_webpage/src}/main.rs | 0 {src => leptos_webpage/src}/session.rs | 0 .../src}/session_date_calculator.rs | 0 {src => leptos_webpage/src}/webpage.rs | 0 styles.css => leptos_webpage/styles.css | 0 16 files changed, 59 insertions(+), 43 deletions(-) create mode 100644 leptos_webpage/Cargo.toml create mode 100644 leptos_webpage/README.md rename Trunk.toml => leptos_webpage/Trunk.toml (50%) rename index.html => leptos_webpage/index.html (100%) rename leptosfmt.sh => leptos_webpage/leptosfmt.sh (100%) rename rustfmt.toml => leptos_webpage/rustfmt.toml (100%) rename {src => leptos_webpage/src}/bin/cli.rs (100%) rename {src => leptos_webpage/src}/bin/create_default_configs.rs (100%) rename {src => leptos_webpage/src}/lib.rs (100%) rename {src => leptos_webpage/src}/main.rs (100%) rename {src => leptos_webpage/src}/session.rs (100%) rename {src => leptos_webpage/src}/session_date_calculator.rs (100%) rename {src => leptos_webpage/src}/webpage.rs (100%) rename styles.css => leptos_webpage/styles.css (100%) diff --git a/Cargo.toml b/Cargo.toml index 3e7bfff..9a4de6a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,16 +1,6 @@ -[package] -name = "jana_sessions_webpage" -version = "0.1.0" -edition = "2021" +[workspace] +resolver = "2" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -default-run = "jana_sessions_webpage" - -[dependencies] -chrono = { version = "0.4", features = ["serde"] } -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 +members = [ + "leptos_webpage", +] diff --git a/README.md b/README.md index 1c6b785..b976090 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,10 @@ # Jana Sessions Webpage -The webpage for Jana-Sessions (unofficial name), fully written in Rust. +The webpage of Jana Sessions (unofficial name) written in Rust. -## Building +The project currently consists of only a single crate, but a mayor refactoring is planned. -The project currently uses leptos, so you'll want to install trunk (`cargo install trunk`). -For the build to work you'll need the `wasm32-unknown-unknown` target (`rustup target add wasm32-unknown-unknown`). - -You can build the projekt with -```bash -trunk build --release -``` -which will create the app in the `target/dist` folder. - -Alternatively you can serve it locally with -```bash -trunk serve -``` - -To also access the local hosted page from other devices, use -```bash -trunk serve -a 0.0.0.0 -``` - -## Deployment - -Just use pythons webserver and point it to the dist folder -```bash -python3 -m http.server 8080 --directoy target/dist -``` +## leptos_webpage +The first functional version of the page, using leptos and trunk. +Please see the README there for more details. \ No newline at end of file diff --git a/leptos_webpage/Cargo.toml b/leptos_webpage/Cargo.toml new file mode 100644 index 0000000..3e7bfff --- /dev/null +++ b/leptos_webpage/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "jana_sessions_webpage" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +default-run = "jana_sessions_webpage" + +[dependencies] +chrono = { version = "0.4", features = ["serde"] } +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 diff --git a/leptos_webpage/README.md b/leptos_webpage/README.md new file mode 100644 index 0000000..1c6b785 --- /dev/null +++ b/leptos_webpage/README.md @@ -0,0 +1,32 @@ +# Jana Sessions Webpage + +The webpage for Jana-Sessions (unofficial name), fully written in Rust. + +## Building + +The project currently uses leptos, so you'll want to install trunk (`cargo install trunk`). +For the build to work you'll need the `wasm32-unknown-unknown` target (`rustup target add wasm32-unknown-unknown`). + +You can build the projekt with +```bash +trunk build --release +``` +which will create the app in the `target/dist` folder. + +Alternatively you can serve it locally with +```bash +trunk serve +``` + +To also access the local hosted page from other devices, use +```bash +trunk serve -a 0.0.0.0 +``` + +## Deployment + +Just use pythons webserver and point it to the dist folder +```bash +python3 -m http.server 8080 --directoy target/dist +``` + diff --git a/Trunk.toml b/leptos_webpage/Trunk.toml similarity index 50% rename from Trunk.toml rename to leptos_webpage/Trunk.toml index a107850..04719dc 100644 --- a/Trunk.toml +++ b/leptos_webpage/Trunk.toml @@ -1,6 +1,6 @@ [build] filehash = false -dist = "target/dist" +dist = "../target/dist" public_url = "." [[hooks]] diff --git a/index.html b/leptos_webpage/index.html similarity index 100% rename from index.html rename to leptos_webpage/index.html diff --git a/leptosfmt.sh b/leptos_webpage/leptosfmt.sh similarity index 100% rename from leptosfmt.sh rename to leptos_webpage/leptosfmt.sh diff --git a/rustfmt.toml b/leptos_webpage/rustfmt.toml similarity index 100% rename from rustfmt.toml rename to leptos_webpage/rustfmt.toml diff --git a/src/bin/cli.rs b/leptos_webpage/src/bin/cli.rs similarity index 100% rename from src/bin/cli.rs rename to leptos_webpage/src/bin/cli.rs diff --git a/src/bin/create_default_configs.rs b/leptos_webpage/src/bin/create_default_configs.rs similarity index 100% rename from src/bin/create_default_configs.rs rename to leptos_webpage/src/bin/create_default_configs.rs diff --git a/src/lib.rs b/leptos_webpage/src/lib.rs similarity index 100% rename from src/lib.rs rename to leptos_webpage/src/lib.rs diff --git a/src/main.rs b/leptos_webpage/src/main.rs similarity index 100% rename from src/main.rs rename to leptos_webpage/src/main.rs diff --git a/src/session.rs b/leptos_webpage/src/session.rs similarity index 100% rename from src/session.rs rename to leptos_webpage/src/session.rs diff --git a/src/session_date_calculator.rs b/leptos_webpage/src/session_date_calculator.rs similarity index 100% rename from src/session_date_calculator.rs rename to leptos_webpage/src/session_date_calculator.rs diff --git a/src/webpage.rs b/leptos_webpage/src/webpage.rs similarity index 100% rename from src/webpage.rs rename to leptos_webpage/src/webpage.rs diff --git a/styles.css b/leptos_webpage/styles.css similarity index 100% rename from styles.css rename to leptos_webpage/styles.css From 04a6aa644b8e64cfe4557e9194c0e9eadf62f616 Mon Sep 17 00:00:00 2001 From: Steppy Date: Mon, 17 Feb 2025 12:46:29 +0100 Subject: [PATCH 2/7] Reformatting and first dioxus experiments --- Cargo.toml | 12 ++++++++++++ leptos_webpage/src/bin/cli.rs | 16 ++++++++++++---- leptos_webpage/src/main.rs | 1 - leptos_webpage/rustfmt.toml => rustfmt.toml | 0 4 files changed, 24 insertions(+), 5 deletions(-) rename leptos_webpage/rustfmt.toml => rustfmt.toml (100%) diff --git a/Cargo.toml b/Cargo.toml index 9a4de6a..cae0d90 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,3 +4,15 @@ resolver = "2" members = [ "leptos_webpage", ] + +[profile] + +[profile.wasm-dev] +inherits = "dev" +opt-level = 1 + +[profile.server-dev] +inherits = "dev" + +[profile.android-dev] +inherits = "dev" diff --git a/leptos_webpage/src/bin/cli.rs b/leptos_webpage/src/bin/cli.rs index 8babed3..8a3101d 100644 --- a/leptos_webpage/src/bin/cli.rs +++ b/leptos_webpage/src/bin/cli.rs @@ -5,17 +5,25 @@ use std::io::stdin; #[derive(Debug, Parser)] struct Args { - sessions: Vec + sessions: Vec, } fn main() { let args = Args::parse(); - let mut iter = DayIter::default().filter(|day| args.sessions.iter().any(|nth_weekday| nth_weekday.matches(day))); + let mut iter = DayIter::default().filter(|day| { + args + .sessions + .iter() + .any(|nth_weekday| nth_weekday.matches(day)) + }); loop { - iter.by_ref().take(3).for_each(|day| println!("{}", localize_day(&day))); + iter + .by_ref() + .take(3) + .for_each(|day| println!("{}", localize_day(&day))); println!("Press enter for more..."); stdin().read_line(&mut String::new()).unwrap(); } -} \ No newline at end of file +} diff --git a/leptos_webpage/src/main.rs b/leptos_webpage/src/main.rs index aeaae4b..08ee84d 100644 --- a/leptos_webpage/src/main.rs +++ b/leptos_webpage/src/main.rs @@ -6,4 +6,3 @@ fn main() { mount_to_body(webpage::App); } - diff --git a/leptos_webpage/rustfmt.toml b/rustfmt.toml similarity index 100% rename from leptos_webpage/rustfmt.toml rename to rustfmt.toml From 4fd43332e04e7a82d4688d716f89f651defbf6a8 Mon Sep 17 00:00:00 2001 From: Steppy Date: Mon, 17 Feb 2025 20:18:41 +0100 Subject: [PATCH 3/7] [WIP] refactor SessionDayIter --- Cargo.toml | 2 +- leptos_webpage/Cargo.toml | 3 +- leptos_webpage/src/session.rs | 16 --- session_iter/Cargo.toml | 9 ++ session_iter/src/day.rs | 49 ++++++++ session_iter/src/lib.rs | 5 + session_iter/src/main.rs | 1 + session_iter/src/session.rs | 187 +++++++++++++++++++++++++++++++ session_iter/src/session/iter.rs | 123 ++++++++++++++++++++ session_iter/src/session/rule.rs | 149 ++++++++++++++++++++++++ session_iter/src/test_util.rs | 10 ++ 11 files changed, 536 insertions(+), 18 deletions(-) create mode 100644 session_iter/Cargo.toml create mode 100644 session_iter/src/day.rs create mode 100644 session_iter/src/lib.rs create mode 100644 session_iter/src/main.rs create mode 100644 session_iter/src/session.rs create mode 100644 session_iter/src/session/iter.rs create mode 100644 session_iter/src/session/rule.rs create mode 100644 session_iter/src/test_util.rs 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 From bc3c4dedf9b8ca66263f96811c84eefad42a2340 Mon Sep 17 00:00:00 2001 From: Steppy Date: Mon, 17 Feb 2025 22:40:49 +0100 Subject: [PATCH 4/7] Implement Iterator for SessionDayIter --- session_iter/src/session.rs | 8 ++- session_iter/src/session/iter.rs | 120 ++++++++++++++++++++++++------- session_iter/src/session/rule.rs | 25 ++++--- 3 files changed, 112 insertions(+), 41 deletions(-) diff --git a/session_iter/src/session.rs b/session_iter/src/session.rs index 51134a4..d927cfb 100644 --- a/session_iter/src/session.rs +++ b/session_iter/src/session.rs @@ -2,7 +2,7 @@ pub mod iter; pub mod rule; use crate::day::Day; -use crate::session::rule::{SessionRule, SessionRuleLike, WeekdayOfMonth}; +use crate::session::rule::{SessionRule, WeekdayOfMonth}; use chrono::NaiveDate; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; @@ -98,6 +98,7 @@ impl RegularSession { self } + ///gets the next session day, where no except applies. Can possibly return the `current_date`. pub fn next_regular_session_day(&self, current_date: D) -> Option where D: Into, @@ -148,13 +149,14 @@ 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, + ///the date when the alternation should show up in the calendar, or the original date + pub new_day: Option, } impl From for Alternation { fn from(day: Day) -> Self { Self { - day: Some(day), + new_day: Some(day), ..Default::default() } } diff --git a/session_iter/src/session/iter.rs b/session_iter/src/session/iter.rs index addcaef..098b7ac 100644 --- a/session_iter/src/session/iter.rs +++ b/session_iter/src/session/iter.rs @@ -2,14 +2,14 @@ use crate::day::Day; use crate::session::{ Alternation, Cancellation, Dated, Except, ExtraSession, OptNoted, RegularSession, Session, }; +use chrono::Days; use serde::{Deserialize, Serialize}; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, VecDeque}; use std::hash::Hash; #[derive(Debug, Clone)] pub struct DatedSessionIter { - current_date: Day, - sessions: BTreeMap>, + sessions: BTreeMap>, } impl DatedSessionIter { @@ -20,7 +20,7 @@ impl DatedSessionIter { { let current_date = start_date.into(); - //map every session and their exceptions to a date when they should show up in the calendar + //map every session and their excepts to a date when they should show up in the calendar let sessions = sessions .into_iter() @@ -28,10 +28,9 @@ impl DatedSessionIter { 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), + day, regular: session.clone(), cause: alternation.clone(), }, @@ -48,31 +47,55 @@ impl DatedSessionIter { } })) .collect(), - Session::Extra(extra) => { - //filter out old extra sessions - if current_date <= extra.day() { - vec![extra.into()] - } else { - vec![] - } - } + Session::Extra(extra) => vec![extra.into()], }) - .fold(BTreeMap::<_, Vec<_>>::new(), |mut map, dated_session| { - map - .entry(dated_session.day()) - .or_default() - .push(dated_session); - map - }); + //filter out and entries which would lay in the past + .filter(|dated_session| dated_session.day() >= current_date) + //group sessions on the same day together + .fold( + BTreeMap::<_, VecDeque<_>>::new(), + |mut map, dated_session| { + map + .entry(dated_session.day()) + .or_default() + .push_back(dated_session); + map + }, + ); - Self { - current_date, - sessions, - } + Self { sessions } } } -//TODO iter implementation for DatedSessionIter +impl Iterator for DatedSessionIter { + type Item = DatedSession; + + fn next(&mut self) -> Option { + let mut entry = self.sessions.first_entry()?; + let session = entry.get_mut().pop_front()?; + if entry.get().is_empty() { + entry.remove(); + } + + //make sure regular sessions remain in map + if let DatedSession::Regular { ref session, day } = session { + //calculate next day, because next_regular_session_day of a session day returns that very session day + if let Some(next_day) = day.checked_add_days(Days::new(1)) { + if let Some(next_session_day) = session.next_regular_session_day(next_day) { + self + .sessions + .entry(next_session_day) + .or_default() + .push_back(DatedSession::Regular { + day: next_session_day, + session: session.clone(), + }) + } + } + } + Some(session) + } +} #[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] pub enum DatedSession { @@ -106,7 +129,7 @@ impl Dated for DatedSession { DatedSession::Regular { day, .. } => day, DatedSession::Extra(ref extra) => extra.day(), DatedSession::Canceled { day, .. } => day, - DatedSession::Altered { day, ref cause, .. } => cause.day.unwrap_or(day), + DatedSession::Altered { day, ref cause, .. } => cause.new_day.unwrap_or(day), } } } @@ -121,3 +144,46 @@ impl OptNoted for DatedSession { } } } + +#[cfg(test)] +mod test { + use crate::session::iter::{DatedSession, DatedSessionIter}; + use crate::session::rule::WeekdayOfMonth; + use crate::session::{RegularSession, WithNote}; + use crate::test_util::{date, day}; + use chrono::Weekday; + + #[test] + fn test_regular() { + let tue_session = + RegularSession::from(WeekdayOfMonth::new(3, Weekday::Tue)).with_note("18:30 Uhr"); + let sun_session = + RegularSession::from(WeekdayOfMonth::new(1, Weekday::Sun)).with_note("10:00 Uhr"); + let mut iter = DatedSessionIter::new( + date(17, 2, 2025), + [tue_session.clone().into(), sun_session.clone().into()], + ); + + assert_eq!( + iter.next(), + Some(DatedSession::Regular { + day: day(18, 2, 2025), + session: tue_session.clone() + }) + ); + assert_eq!( + iter.next(), + Some(DatedSession::Regular { + day: day(2, 3, 2025), + session: sun_session.clone() + }) + ); + assert_eq!( + iter.next(), + Some(DatedSession::Regular { + day: day(18, 3, 2025), + session: tue_session.clone() + }) + ); + } +} diff --git a/session_iter/src/session/rule.rs b/session_iter/src/session/rule.rs index 8ac0490..b7793b2 100644 --- a/session_iter/src/session/rule.rs +++ b/session_iter/src/session/rule.rs @@ -1,5 +1,5 @@ use crate::day::Day; -use chrono::{Datelike, Months, NaiveDate, Weekday}; +use chrono::{Datelike, Days, Months, NaiveDate, Weekday}; use serde::{Deserialize, Serialize}; use std::ops::Deref; @@ -21,7 +21,7 @@ impl SessionRule { { SessionDayIter { rule: self, - current_date: Some(start_date.into()), + start_date: Some(start_date.into()), } } } @@ -45,20 +45,23 @@ impl SessionRuleLike for SessionRule { #[derive(Debug, Clone)] pub struct SessionDayIter { pub rule: R, - pub current_date: Option, + pub start_date: Option, } -impl Iterator for SessionDayIter +impl Iterator for SessionDayIter where - R: Deref, + R: Deref, + S: SessionRuleLike, { 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 + let start_date = self.start_date?; + let session_date = self.rule.determine_next_date(start_date); + self.start_date = session_date + .and_then(|session_date| session_date.checked_add_days(Days::new(1))) + .map(Day::from); + session_date } } @@ -69,7 +72,7 @@ where pub fn to_owned(&self) -> SessionDayIter { SessionDayIter { rule: self.rule.clone(), - current_date: self.current_date, + start_date: self.start_date, } } } @@ -116,7 +119,7 @@ impl SessionRuleLike for WeekdayOfMonth { } pub trait SessionRuleLike { - /// Determines the next session date in form of a [Day], possibly including the `start_date`. + /// Determines the next session date in form of a [Day], possibly including the `current_date`. fn determine_next_date(&self, current_date: Day) -> Option; /// Whether this rule would be able to produce the given `day`. From 6208e5d1dc9045b7b2ddf816aa970d3faf3b6410 Mon Sep 17 00:00:00 2001 From: Steppy Date: Mon, 17 Feb 2025 23:30:44 +0100 Subject: [PATCH 5/7] Add DatedSessionIter unit tests --- session_iter/src/session.rs | 6 ++ session_iter/src/session/iter.rs | 138 +++++++++++++++++++++++++++++-- 2 files changed, 139 insertions(+), 5 deletions(-) diff --git a/session_iter/src/session.rs b/session_iter/src/session.rs index d927cfb..9c245a6 100644 --- a/session_iter/src/session.rs +++ b/session_iter/src/session.rs @@ -172,6 +172,12 @@ pub struct Cancellation { pub note: Note, } +impl Cancellation { + pub fn new() -> Self { + Self::default() + } +} + impl_opt_noted!(Cancellation); impl_with_note!(Cancellation); diff --git a/session_iter/src/session/iter.rs b/session_iter/src/session/iter.rs index 098b7ac..78643a3 100644 --- a/session_iter/src/session/iter.rs +++ b/session_iter/src/session/iter.rs @@ -1,4 +1,5 @@ use crate::day::Day; +use crate::session::rule::SessionRuleLike; use crate::session::{ Alternation, Cancellation, Dated, Except, ExtraSession, OptNoted, RegularSession, Session, }; @@ -28,6 +29,7 @@ impl DatedSessionIter { Session::Regular(session) => session .except .iter() + .filter(|(&day, _)| session.rule.accepts(day)) .map(|(&day, except)| match except { Except::Alternation(alternation) => DatedSession::Altered { day, @@ -149,16 +151,14 @@ impl OptNoted for DatedSession { mod test { use crate::session::iter::{DatedSession, DatedSessionIter}; use crate::session::rule::WeekdayOfMonth; - use crate::session::{RegularSession, WithNote}; + use crate::session::{Alternation, Cancellation, ExtraSession, RegularSession, WithNote}; use crate::test_util::{date, day}; use chrono::Weekday; #[test] fn test_regular() { - let tue_session = - RegularSession::from(WeekdayOfMonth::new(3, Weekday::Tue)).with_note("18:30 Uhr"); - let sun_session = - RegularSession::from(WeekdayOfMonth::new(1, Weekday::Sun)).with_note("10:00 Uhr"); + let tue_session = RegularSession::from(WeekdayOfMonth::new(3, Weekday::Tue)); + let sun_session = RegularSession::from(WeekdayOfMonth::new(1, Weekday::Sun)); let mut iter = DatedSessionIter::new( date(17, 2, 2025), [tue_session.clone().into(), sun_session.clone().into()], @@ -186,4 +186,132 @@ mod test { }) ); } + + #[test] + fn test_old() { + let extra_session = ExtraSession::from(day(15, 2, 2025)); + let regular_session = RegularSession::from(WeekdayOfMonth::new(3, Weekday::Tue)) + .with_note("18:30 Uhr") + .except(day(18, 2, 2025), Cancellation::new()) + .except(day(18, 3, 2025), Alternation::from(day(21, 1, 2025))); + + let mut iter = DatedSessionIter::new( + day(19, 2, 2025), + [regular_session.clone().into(), extra_session.into()], + ); + + assert_eq!( + iter.next(), + Some(DatedSession::Regular { + day: day(15, 4, 2025), + session: regular_session.clone(), + }) + ); + } + + #[test] + fn test_invalid_except() { + let regular_session = RegularSession::from(WeekdayOfMonth::new(3, Weekday::Tue)) + .except(day(17, 2, 2025), Cancellation::new()); + + let mut iter = DatedSessionIter::new(day(17, 2, 2025), [regular_session.clone().into()]); + + assert_eq!( + iter.next(), + Some(DatedSession::Regular { + day: day(18, 2, 2025), + session: regular_session.clone() + }) + ); + } + + #[test] + fn test_extra_altered_and_canceled() { + //an extra session on the same day as a tuesday session + let extra_session = ExtraSession::from(day(18, 2, 2025)).with_note("morning session"); + //an alternation of a tuesday session without moving it + let (note_alternation_day, note_alternation) = ( + day(18, 3, 2025), + Alternation { + note: Some("a little different today".into()), + ..Default::default() + }, + ); + let tue_session = RegularSession::from(WeekdayOfMonth::new(3, Weekday::Tue)) + .except(note_alternation_day, note_alternation.clone()); + //a sunday session moved in front of a tuesday session + let moved_sun_session_day = day(16, 2, 2025); + let (day_alternation_day, day_alternation) = + (day(2, 3, 2025), Alternation::from(moved_sun_session_day)); + //a canceled sunday session + let (cancel_day, cancellation) = (day(6, 4, 2025), Cancellation::new()); + let sun_session = RegularSession::from(WeekdayOfMonth::new(1, Weekday::Sun)) + .except(day_alternation_day, day_alternation.clone()) + .except(cancel_day, cancellation.clone()); + + let mut iter = DatedSessionIter::new( + day(15, 2, 2025), + [ + extra_session.clone().into(), + tue_session.clone().into(), + sun_session.clone().into(), + ], + ); + + //at first comes the moved sunday + assert_eq!( + iter.next(), + Some(DatedSession::Altered { + day: day_alternation_day, + regular: sun_session.clone(), + cause: day_alternation.clone(), + }) + ); + //then comes the extra session on tuesday (because it was first in the list) + assert_eq!( + iter.next(), + Some(DatedSession::Extra(extra_session.clone())) + ); + //after that comes a regular tuesday session + assert_eq!( + iter.next(), + Some(DatedSession::Regular { + day: day(18, 2, 2025), + session: tue_session.clone() + }) + ); + //now we have an altered but not moved tuesday session + assert_eq!( + iter.next(), + Some(DatedSession::Altered { + day: note_alternation_day, + regular: tue_session.clone(), + cause: note_alternation.clone(), + }) + ); + //the next sunday session was canceled + assert_eq!( + iter.next(), + Some(DatedSession::Canceled { + day: cancel_day, + regular: sun_session.clone(), + cause: cancellation.clone(), + }) + ); + //and now we are back to regular sessions + assert_eq!( + iter.next(), + Some(DatedSession::Regular { + day: day(15, 4, 2025), + session: tue_session.clone() + }) + ); + assert_eq!( + iter.next(), + Some(DatedSession::Regular { + day: day(4, 5, 2025), + session: sun_session.clone() + }) + ); + } } From ca5a8312e49a8918187eeb7afb7fab511adb5685 Mon Sep 17 00:00:00 2001 From: Steppy Date: Tue, 18 Feb 2025 17:11:07 +0100 Subject: [PATCH 6/7] Migrate leptos_webpage to session_iter logic (build now broken) --- leptos_webpage/Cargo.toml | 1 - leptos_webpage/src/bin/cli.rs | 29 --- .../src/bin/create_default_configs.rs | 6 +- leptos_webpage/src/lib.rs | 8 +- leptos_webpage/src/session.rs | 243 ------------------ leptos_webpage/src/session_date_calculator.rs | 140 ---------- leptos_webpage/src/webpage.rs | 53 ++-- session_iter/src/session.rs | 2 + 8 files changed, 31 insertions(+), 451 deletions(-) delete mode 100644 leptos_webpage/src/bin/cli.rs delete mode 100644 leptos_webpage/src/session.rs delete mode 100644 leptos_webpage/src/session_date_calculator.rs diff --git a/leptos_webpage/Cargo.toml b/leptos_webpage/Cargo.toml index 255443c..82b0afa 100644 --- a/leptos_webpage/Cargo.toml +++ b/leptos_webpage/Cargo.toml @@ -13,5 +13,4 @@ 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"] } session_iter = { path = "../session_iter"} \ No newline at end of file diff --git a/leptos_webpage/src/bin/cli.rs b/leptos_webpage/src/bin/cli.rs deleted file mode 100644 index 8a3101d..0000000 --- a/leptos_webpage/src/bin/cli.rs +++ /dev/null @@ -1,29 +0,0 @@ -use clap::Parser; -use jana_sessions_webpage::localize_day; -use jana_sessions_webpage::session_date_calculator::{DayIter, NthWeekday}; -use std::io::stdin; - -#[derive(Debug, Parser)] -struct Args { - sessions: Vec, -} - -fn main() { - let args = Args::parse(); - - let mut iter = DayIter::default().filter(|day| { - args - .sessions - .iter() - .any(|nth_weekday| nth_weekday.matches(day)) - }); - - loop { - iter - .by_ref() - .take(3) - .for_each(|day| println!("{}", localize_day(&day))); - println!("Press enter for more..."); - stdin().read_line(&mut String::new()).unwrap(); - } -} diff --git a/leptos_webpage/src/bin/create_default_configs.rs b/leptos_webpage/src/bin/create_default_configs.rs index dff3530..c260e91 100644 --- a/leptos_webpage/src/bin/create_default_configs.rs +++ b/leptos_webpage/src/bin/create_default_configs.rs @@ -4,11 +4,11 @@ use std::fs::File; use std::path::Path; use std::{env, fs, io}; -fn main() -> io::Result<()> { +fn main() -> Result<(), String> { let out_dir = env::var_os("TRUNK_STAGING_DIR").unwrap_or("target/default_configs".into()); - fs::create_dir_all(&out_dir)?; + fs::create_dir_all(&out_dir).map_err(|e| format!("failed to create target directory: {e}"))?; - create_default_config::("session_config", &out_dir)?; + create_default_config::("session_config", &out_dir).map_err(|e| format!("Failed to create session_config: {e}"))?; Ok(()) } diff --git a/leptos_webpage/src/lib.rs b/leptos_webpage/src/lib.rs index f770fa1..e0f6f66 100644 --- a/leptos_webpage/src/lib.rs +++ b/leptos_webpage/src/lib.rs @@ -1,14 +1,12 @@ -use crate::session_date_calculator::Day; use chrono::Weekday; +use session_iter::day::Day; -pub mod session; -pub mod session_date_calculator; pub mod webpage; pub fn localize_day(day: &Day) -> String { format!( "{}, {}", - match day.weekday { + match day.weekday() { Weekday::Mon => "Montag", Weekday::Tue => "Dienstag", Weekday::Wed => "Mittwoch", @@ -17,6 +15,6 @@ pub fn localize_day(day: &Day) -> String { Weekday::Sat => "Samstag", Weekday::Sun => "Sonntag", }, - day.date.format("%d.%m.%Y") + day.date().format("%d.%m.%Y") ) } diff --git a/leptos_webpage/src/session.rs b/leptos_webpage/src/session.rs deleted file mode 100644 index 1f86678..0000000 --- a/leptos_webpage/src/session.rs +++ /dev/null @@ -1,243 +0,0 @@ -use crate::session_date_calculator::{Day, NthWeekday}; -use chrono::NaiveDate; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; - -#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] -pub enum DatedSession { - Regular { - session: RegularSession, - day: Day, - }, - Extra(ExtraSession), - Cancelled { - session: RegularSession, - day: Day, - exception: CancelException, - }, - Altered { - session: RegularSession, - day: Day, - exception: AlterException, - }, -} - -impl DatedSession { - pub fn date(&self) -> &NaiveDate { - match self { - DatedSession::Regular { day, .. } => &day.date, - DatedSession::Extra(ExtraSession { date, .. }) => date, - DatedSession::Cancelled { day, .. } => &day.date, - DatedSession::Altered { day, .. } => &day.date, - } - } -} - -impl Noted for DatedSession { - fn get_note(&self) -> Option<&String> { - match self { - DatedSession::Regular { session, .. } => session.get_note(), - DatedSession::Extra(session) => session.get_note(), - DatedSession::Cancelled { exception, .. } => exception.get_note(), - DatedSession::Altered { exception, .. } => exception.get_note(), - } - } -} - -#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] -pub enum Session { - Regular(RegularSession), - Extra(ExtraSession), -} - -impl From for Session { - fn from(value: RegularSession) -> Self { - Self::Regular(value) - } -} - -impl From for Session { - fn from(value: ExtraSession) -> Self { - Self::Extra(value) - } -} - -impl Session { - pub fn into_dated(self, day: Day) -> Result { - if !self.is_applicable_to(&day) { - return Err(self); - } - - let dated_session = match self { - Session::Regular(session) => match session.except.get(&day.date) { - Some(exception) => match exception.clone() { - Exception::Cancel(exception) => DatedSession::Cancelled { - session, - day, - exception, - }, - Exception::Alter(exception) => DatedSession::Altered { - session, - day, - exception, - }, - }, - None => DatedSession::Regular { session, day }, - }, - Session::Extra(session) => DatedSession::Extra(session), - }; - Ok(dated_session) - } - - pub fn is_applicable_to(&self, day: &Day) -> bool { - match *self { - Session::Regular(RegularSession { rule, .. }) => rule.matches(day), - Session::Extra(ExtraSession { date, .. }) => day.date == date, - } - } -} - -impl Noted for Session { - fn get_note(&self) -> Option<&String> { - match self { - Session::Regular(session) => session.get_note(), - Session::Extra(session) => session.get_note(), - } - } -} - -#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] -pub struct RegularSession { - pub rule: NthWeekday, - pub note: Option, - pub except: HashMap, -} - -impl From for RegularSession { - fn from(rule: NthWeekday) -> Self { - Self { - rule, - note: None, - except: Default::default(), - } - } -} - -impl RegularSession { - pub fn with_exception(mut self, date: D, exception: E) -> Self - where - D: Into, - E: Into, - { - self.except.insert(date.into(), exception.into()); - self - } -} - -#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] -pub struct ExtraSession { - pub date: NaiveDate, - pub note: Option, -} - -impl From for ExtraSession { - fn from(date: NaiveDate) -> Self { - Self { date, note: None } - } -} - -#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] -pub enum Exception { - Cancel(CancelException), - Alter(AlterException), -} - -impl From for Exception { - fn from(value: CancelException) -> Self { - Self::Cancel(value) - } -} - -impl From for Exception { - fn from(value: AlterException) -> Self { - Self::Alter(value) - } -} - -impl Noted for Exception { - fn get_note(&self) -> Option<&String> { - match self { - Exception::Cancel(exception) => exception.get_note(), - Exception::Alter(exception) => exception.get_note(), - } - } -} - -#[derive(Debug, Clone, Eq, PartialEq, Hash, Default, Serialize, Deserialize)] -#[serde(default)] -pub struct CancelException { - pub note: Option, -} - -#[derive(Debug, Clone, Eq, PartialEq, Hash, Default, Serialize, Deserialize)] -#[serde(default)] -pub struct AlterException { - pub date: Option, - pub note: Option, -} - -impl From for AlterException { - fn from(value: NaiveDate) -> Self { - Self { - date: Some(value), - ..Self::default() - } - } -} - -pub trait Noted { - fn get_note(&self) -> Option<&String>; -} - -macro_rules! impl_noted { - ($ty: ident) => { - impl Noted for $ty { - fn get_note(&self) -> Option<&String> { - self.note.as_ref() - } - } - }; -} - -impl_noted!(RegularSession); -impl_noted!(ExtraSession); -impl_noted!(AlterException); -impl_noted!(CancelException); - -pub trait WithNote { - fn with_note(self, note: S) -> Self - where - S: ToString; -} - -macro_rules! impl_with_node { - ($ty: ident) => { - impl WithNote for $ty { - fn with_note(self, note: S) -> Self - where - S: ToString, - { - #[allow(clippy::needless_update)] - Self { - note: Some(note.to_string()), - ..self - } - } - } - }; -} - -impl_with_node!(AlterException); -impl_with_node!(CancelException); -impl_with_node!(ExtraSession); -impl_with_node!(RegularSession); diff --git a/leptos_webpage/src/session_date_calculator.rs b/leptos_webpage/src/session_date_calculator.rs deleted file mode 100644 index 0ed3a5b..0000000 --- a/leptos_webpage/src/session_date_calculator.rs +++ /dev/null @@ -1,140 +0,0 @@ -use chrono::{Datelike, Days, Local, NaiveDate, ParseWeekdayError, Weekday}; -use serde::{Deserialize, Serialize}; -use std::error::Error; -use std::fmt::{Display, Formatter}; -use std::num::ParseIntError; -use std::str::FromStr; - -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] -pub struct NthWeekday { - pub n: u8, - pub weekday: Weekday, -} - -impl NthWeekday { - pub fn new(n: u8, weekday: Weekday) -> Self { - Self { n, weekday } - } - - pub fn matches(&self, day: &Day) -> bool { - self.weekday == day.weekday && self.n == day.week_of_month - } -} - -impl FromStr for NthWeekday { - type Err = NthWeekdayParseError; - - fn from_str(s: &str) -> Result { - let (number, day) = s - .split_once(' ') - .ok_or(NthWeekdayParseError::InvalidFormat)?; - let weekday = Weekday::from_str(day) - .map_err(|e| NthWeekdayParseError::InvalidWeekday(day.to_string(), e))?; - let number = number - .trim_end_matches(|c: char| !c.is_ascii_digit()) - .parse::() - .map_err(|e| NthWeekdayParseError::InvalidN(number.to_string(), e))?; - Ok(Self::new(number, weekday)) - } -} - -#[derive(Debug)] -pub enum NthWeekdayParseError { - InvalidFormat, - InvalidWeekday(String, ParseWeekdayError), - InvalidN(String, ParseIntError), -} - -impl Display for NthWeekdayParseError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - NthWeekdayParseError::InvalidFormat => write!(f, "Invalid format, use e.g '3rd fri'"), - NthWeekdayParseError::InvalidWeekday(week_day, e) => { - write!(f, "Invalid weekday '{week_day}': {e}") - } - NthWeekdayParseError::InvalidN(number, e) => write!(f, "Invalid number '{number}': {e}"), - } - } -} - -impl Error for NthWeekdayParseError {} - -#[derive(Debug)] -pub struct DayIter { - date: Option, -} - -impl From for DayIter { - fn from(value: NaiveDate) -> Self { - Self { date: value.into() } - } -} - -impl From for DayIter { - fn from(value: Day) -> Self { - Self { - date: value.date.into(), - } - } -} - -impl Default for DayIter { - fn default() -> Self { - Self { - date: today().into(), - } - } -} - -impl Iterator for DayIter { - type Item = Day; - - fn next(&mut self) -> Option { - let date = self.date; - self.date = date.and_then(|date| date.checked_add_days(Days::new(1))); - date.map(Day::from) - } -} - -#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] -pub struct Day { - pub date: NaiveDate, - pub weekday: Weekday, - pub week_of_month: u8, -} - -impl From for Day { - fn from(date: NaiveDate) -> Self { - Self { - date, - weekday: date.weekday(), - week_of_month: date.day0() as u8 / 7 + 1, - } - } -} - -impl Default for Day { - fn default() -> Self { - Self::from(today()) - } -} - -fn today() -> NaiveDate { - Local::now().date_naive() -} - -#[cfg(test)] -mod test { - use crate::session_date_calculator::DayIter; - use chrono::NaiveDate; - - #[test] - fn test_day_iter() { - let mut day_iter = DayIter::from(NaiveDate::from_ymd_opt(2025, 2, 1).expect("valid date")); - for week_of_month in 1..=4 { - for _ in 0..7 { - assert_eq!(day_iter.next().unwrap().week_of_month, week_of_month) - } - } - } -} diff --git a/leptos_webpage/src/webpage.rs b/leptos_webpage/src/webpage.rs index e8f54cf..7e27e2c 100644 --- a/leptos_webpage/src/webpage.rs +++ b/leptos_webpage/src/webpage.rs @@ -1,12 +1,12 @@ use crate::localize_day; -use crate::session::{ - AlterException, CancelException, DatedSession, ExtraSession, RegularSession, Session, WithNote, -}; -use crate::session_date_calculator::{Day, DayIter, NthWeekday}; use chrono::{Datelike, Days, Months, NaiveDate, Weekday}; use leptos::prelude::*; use leptos::server_fn::request::browser::Request; use serde::{Deserialize, Serialize}; +use session_iter::day::Day; +use session_iter::session::iter::{DatedSession, DatedSessionIter}; +use session_iter::session::rule::WeekdayOfMonth; +use session_iter::session::{Alternation, Cancellation, Dated, ExtraSession, RegularSession, Session, WithNote}; #[component] pub fn App() -> impl IntoView { @@ -42,13 +42,7 @@ pub fn App() -> impl IntoView { #[component] fn Sessions(config: SessionConfig) -> impl IntoView { - let mut session_iter = DayIter::default().flat_map(move |day| { - config - .sessions - .clone() - .into_iter() - .filter_map(move |session| session.clone().into_dated(day.clone()).ok()) - }); + let mut session_iter = DatedSessionIter::new(Day::default(), config.sessions); let (dated_sessions, mut_dated_sessions) = signal(session_iter.by_ref().take(2).collect::>()); @@ -60,7 +54,7 @@ fn Sessions(config: SessionConfig) -> impl IntoView { @@ -93,26 +87,26 @@ fn DatedSession(session: DatedSession) -> impl IntoView { DatedSession::Extra(session) => view! {

""

-

{localize_day(&session.date.into())}

+

{localize_day(&session.day)}

{session.note}

} .into_any(), - DatedSession::Cancelled { session, day, .. } => view! { + DatedSession::Canceled { regular, day, .. } => view! {

""

{localize_day(&day)}

-

{session.note}

+

{regular.note}

} .into_any(), DatedSession::Altered { - session, + regular, day, - exception, + cause, .. } => { - let day = move || match exception.date.map(Day::from) { + let day = move || match cause.new_day { None => view! { {localize_day(&day)} }.into_any(), Some(new_day) => view! { {localize_day(&day)} @@ -126,7 +120,7 @@ fn DatedSession(session: DatedSession) -> impl IntoView {

"<Änderung>"

{day}

-

{exception.note.or(session.note)}

+

{cause.note.or(regular.note)}

} .into_any() @@ -171,11 +165,10 @@ macro_rules! date { } impl Default for SessionConfig { - #[allow(clippy::zero_prefixed_literal)] fn default() -> Self { const YEAR: i32 = 2024; - const NEXT_TUE_SESSION: NaiveDate = date!(18, 02, YEAR); - const NEXT_SUN_SESSION: NaiveDate = date!(02, 03, YEAR); + const NEXT_TUE_SESSION: NaiveDate = date!(18, 2, YEAR); + const NEXT_SUN_SESSION: NaiveDate = date!(2, 3, YEAR); let next_next_sun_session = { let some_day_next_month = NEXT_SUN_SESSION.checked_add_months(Months::new(1)).unwrap(); NaiveDate::from_weekday_of_month_opt( @@ -190,26 +183,26 @@ impl Default for SessionConfig { Self { motd: Some("Jeder 1. Sonntag und 3. Dienstag im Monat".into()), sessions: vec![ - RegularSession::from(NthWeekday::new(1, Weekday::Sun)) + RegularSession::from(WeekdayOfMonth::new(1, Weekday::Sun)) .with_note("10:00 Uhr") - .with_exception( + .except( NEXT_SUN_SESSION, - AlterException { - date: Some(NEXT_SUN_SESSION.checked_sub_days(Days::new(1)).unwrap()), + Alternation { + new_day: Some(NEXT_SUN_SESSION.checked_sub_days(Days::new(1)).unwrap().into()), ..Default::default() }, ) - .with_exception( + .except( next_next_sun_session, - AlterException { + Alternation { note: Some("11 Uhr".into()), ..Default::default() }, ) .into(), - RegularSession::from(NthWeekday::new(3, Weekday::Tue)) + RegularSession::from(WeekdayOfMonth::new(3, Weekday::Tue)) .with_note("18:30 Uhr") - .with_exception(NEXT_TUE_SESSION, CancelException::default()) + .except(NEXT_TUE_SESSION, Cancellation::new()) .into(), ExtraSession::from(NEXT_TUE_SESSION.checked_add_days(Days::new(2)).unwrap()) .with_note("18 Uhr") diff --git a/session_iter/src/session.rs b/session_iter/src/session.rs index 9c245a6..e9ba002 100644 --- a/session_iter/src/session.rs +++ b/session_iter/src/session.rs @@ -78,6 +78,8 @@ pub struct RegularSession { pub except: BTreeMap, } +//TODO we need to implement serialize ourselves, since json doesn't support anything other than string keys + impl From for RegularSession { fn from(rule: SessionRule) -> Self { Self { From 60294ed18bb5644d07d5cd35f06d33d85196df7e Mon Sep 17 00:00:00 2001 From: Steppy Date: Tue, 18 Feb 2025 17:18:50 +0100 Subject: [PATCH 7/7] Fix RegularSession serialization --- session_iter/Cargo.toml | 2 +- session_iter/src/session.rs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/session_iter/Cargo.toml b/session_iter/Cargo.toml index 59e1ac1..e7bdb79 100644 --- a/session_iter/Cargo.toml +++ b/session_iter/Cargo.toml @@ -6,4 +6,4 @@ 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 +serde_json_any_key = "2.0" \ No newline at end of file diff --git a/session_iter/src/session.rs b/session_iter/src/session.rs index e9ba002..5276905 100644 --- a/session_iter/src/session.rs +++ b/session_iter/src/session.rs @@ -75,11 +75,10 @@ impl_from!(ExtraSession for Session as Extra); pub struct RegularSession { pub rule: SessionRule, pub note: Note, + #[serde(with = "serde_json_any_key::any_key_map")] pub except: BTreeMap, } -//TODO we need to implement serialize ourselves, since json doesn't support anything other than string keys - impl From for RegularSession { fn from(rule: SessionRule) -> Self { Self {