Reformat project

This commit is contained in:
Leonard Steppy 2025-02-20 10:55:18 +01:00
parent a5bc12acae
commit 2fe57be382
15 changed files with 658 additions and 634 deletions

View File

@ -2,7 +2,7 @@
resolver = "2"
members = [
"leptos_webpage", "session_iter",
"leptos_webpage", "session_iter", "app",
]
[profile]

View File

@ -3,13 +3,13 @@
The webpage of Jana Sessions (unofficial name) written in Rust.
The project consists of a logic crate and a first version of the webpage written using leptos.
A switch to dioxus is planned.
A switch to dioxus is planned.
## session_iter
The logic to find the dates of upcoming sessions.
The logic to find the dates of upcoming sessions.
## leptos_webpage
The first functional version of the page, using leptos and trunk.
The first functional version of the page, using leptos and trunk.
Please see the README there for more details.

View File

@ -13,4 +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"
session_iter = { path = "../session_iter"}
session_iter = { path = "../session_iter" }

View File

@ -7,25 +7,30 @@ The webpage for Jana-Sessions (unofficial name), fully written in Rust.
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
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
Just use pythons webserver and point it to the dist folder
```bash
python3 -m http.server 8080 --directoy target/dist
```

View File

@ -1,9 +1,9 @@
<!DOCTYPE html>
<html lang="de">
<head>
<head>
<title>Band Sessions</title>
<link data-trunk rel="css" href="styles.css">
<link data-trunk rel="rust" data-bin="jana_sessions_webpage">
</head>
<body></body>
<link data-trunk href="styles.css" rel="css">
<link data-bin="jana_sessions_webpage" data-trunk rel="rust">
</head>
<body></body>
</html>

View File

@ -5,28 +5,28 @@ use std::path::Path;
use std::{env, fs, io};
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).map_err(|e| format!("failed to create target directory: {e}"))?;
let out_dir = env::var_os("TRUNK_STAGING_DIR").unwrap_or("target/default_configs".into());
fs::create_dir_all(&out_dir).map_err(|e| format!("failed to create target directory: {e}"))?;
create_default_config::<SessionConfig, _>("session_config", &out_dir).map_err(|e| format!("Failed to create session_config: {e}"))?;
Ok(())
create_default_config::<SessionConfig, _>("session_config", &out_dir).map_err(|e| format!("Failed to create session_config: {e}"))?;
Ok(())
}
fn create_default_config<T, P>(name: &str, out_dir: P) -> io::Result<()>
where
T: Serialize + Default,
P: AsRef<Path>,
T: Serialize + Default,
P: AsRef<Path>,
{
create_config(name, out_dir, &T::default())
create_config(name, out_dir, &T::default())
}
fn create_config<T, P>(name: &str, out_dir: P, default_config: &T) -> io::Result<()>
where
T: Serialize,
P: AsRef<Path>,
T: Serialize,
P: AsRef<Path>,
{
let out_path = out_dir.as_ref().join(format!("default_{name}.json"));
let out_file = File::create(&out_path)?;
serde_json::to_writer_pretty(out_file, default_config)?;
Ok(())
let out_path = out_dir.as_ref().join(format!("default_{name}.json"));
let out_file = File::create(&out_path)?;
serde_json::to_writer_pretty(out_file, default_config)?;
Ok(())
}

View File

@ -4,17 +4,17 @@ use session_iter::day::Day;
pub mod webpage;
pub fn localize_day(day: &Day) -> String {
format!(
"{}, {}",
match day.weekday() {
Weekday::Mon => "Montag",
Weekday::Tue => "Dienstag",
Weekday::Wed => "Mittwoch",
Weekday::Thu => "Donnerstag",
Weekday::Fri => "Freitag",
Weekday::Sat => "Samstag",
Weekday::Sun => "Sonntag",
},
day.date().format("%d.%m.%Y")
)
format!(
"{}, {}",
match day.weekday() {
Weekday::Mon => "Montag",
Weekday::Tue => "Dienstag",
Weekday::Wed => "Mittwoch",
Weekday::Thu => "Donnerstag",
Weekday::Fri => "Freitag",
Weekday::Sat => "Samstag",
Weekday::Sun => "Sonntag",
},
day.date().format("%d.%m.%Y")
)
}

View File

@ -2,7 +2,7 @@ use jana_sessions_webpage::webpage;
use leptos::prelude::*;
fn main() {
console_error_panic_hook::set_once();
console_error_panic_hook::set_once();
mount_to_body(webpage::App);
mount_to_body(webpage::App);
}

View File

@ -10,30 +10,30 @@ use session_iter::session::{Alternation, Cancellation, Dated, ExtraSession, Regu
#[component]
pub fn App() -> impl IntoView {
let session_config =
LocalResource::new(|| async { load_config::<SessionConfig>("session_config").await });
let session_config =
LocalResource::new(|| async { load_config::<SessionConfig>("session_config").await });
let session_dates = move || {
session_config
.get()
.as_deref()
.cloned()
.map(|config| match config {
Ok(config) => {
let config = config.unwrap_or_default();
view! { <Sessions config /> }.into_any()
}
Err(e) => view! {
let session_dates = move || {
session_config
.get()
.as_deref()
.cloned()
.map(|config| match config {
Ok(config) => {
let config = config.unwrap_or_default();
view! { <Sessions config /> }.into_any()
}
Err(e) => view! {
<div class="box error-background">
<h1>"Error"</h1>
<p>{e}</p>
</div>
}
.into_any(),
})
};
.into_any(),
})
};
view! {
view! {
<div class="background">
<Suspense fallback=|| "Laden...">{session_dates}</Suspense>
</div>
@ -42,11 +42,11 @@ pub fn App() -> impl IntoView {
#[component]
fn Sessions(config: SessionConfig) -> impl IntoView {
let mut session_iter = DatedSessionIter::new(Day::default(), config.sessions);
let (dated_sessions, mut_dated_sessions) =
signal(session_iter.by_ref().take(2).collect::<Vec<_>>());
let mut session_iter = DatedSessionIter::new(Day::default(), config.sessions);
let (dated_sessions, mut_dated_sessions) =
signal(session_iter.by_ref().take(2).collect::<Vec<_>>());
view! {
view! {
<div class="column">
<div class="wide box elem-background">
<h1>"Anstehende Proben Termine"</h1>
@ -75,87 +75,87 @@ fn Sessions(config: SessionConfig) -> impl IntoView {
#[component]
fn DatedSession(session: DatedSession) -> impl IntoView {
match session {
DatedSession::Regular { session, day, .. } => view! {
match session {
DatedSession::Regular { session, day, .. } => view! {
<div class="box elem-background">
<p class="small-text">"<Regulärer Termin>"</p>
<p><b>{localize_day(&day)}</b></p>
<p>{session.note}</p>
</div>
}
.into_any(),
DatedSession::Extra(session) => view! {
.into_any(),
DatedSession::Extra(session) => view! {
<div class="box highlight-background">
<p class="small-text">"<Extra Probe>"</p>
<p><b>{localize_day(&session.day)}</b></p>
<p>{session.note}</p>
</div>
}
.into_any(),
DatedSession::Canceled { regular, day, .. } => view! {
.into_any(),
DatedSession::Canceled { regular, day, .. } => view! {
<div class="box cancel-background">
<p class="small-text">"<Entfall>"</p>
<p class="strikethrough bold">{localize_day(&day)}</p>
<p>{regular.note}</p>
</div>
}
.into_any(),
DatedSession::Altered {
regular,
day,
cause,
..
} => {
let day = move || match cause.new_day {
None => view! { {localize_day(&day)} }.into_any(),
Some(new_day) => view! {
.into_any(),
DatedSession::Altered {
regular,
day,
cause,
..
} => {
let day = move || match cause.new_day {
None => view! { {localize_day(&day)} }.into_any(),
Some(new_day) => view! {
<span class="strikethrough">{localize_day(&day)}</span>
" "
{localize_day(&new_day)}
}
.into_any(),
};
.into_any(),
};
view! {
view! {
<div class="box change-background">
<p class="small-text">"<Änderung>"</p>
<p class="bold">{day}</p>
<p>{cause.note.or(regular.note)}</p>
</div>
}
.into_any()
.into_any()
}
}
}
}
async fn load_config<T>(name: &str) -> Result<Option<T>, String>
where
T: for<'a> Deserialize<'a>,
T: for<'a> Deserialize<'a>,
{
let response = Request::get(&format!("./{name}.json"))
.send()
.await
.map_err(|e| format!("HTTP error: {e}"))?;
if response
.headers()
.get("content-type")
.is_none_or(|content_type| content_type != "application/json")
{
return Ok(None);
}
let response = Request::get(&format!("./{name}.json"))
.send()
.await
.map_err(|e| format!("HTTP error: {e}"))?;
if response
.headers()
.get("content-type")
.is_none_or(|content_type| content_type != "application/json")
{
return Ok(None);
}
let config = response
.json()
.await
.map_err(|e| format!("JSON error: {e}"))?;
Ok(Some(config))
let config = response
.json()
.await
.map_err(|e| format!("JSON error: {e}"))?;
Ok(Some(config))
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct SessionConfig {
pub motd: Option<String>,
pub sessions: Vec<Session>,
pub motd: Option<String>,
pub sessions: Vec<Session>,
}
macro_rules! date {
@ -165,49 +165,49 @@ macro_rules! date {
}
impl Default for SessionConfig {
fn default() -> Self {
const YEAR: i32 = 2024;
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(
some_day_next_month.year(),
some_day_next_month.month(),
Weekday::Sun,
1,
)
.unwrap()
};
fn default() -> Self {
const YEAR: i32 = 2024;
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(
some_day_next_month.year(),
some_day_next_month.month(),
Weekday::Sun,
1,
)
.unwrap()
};
Self {
motd: Some("Jeder 1. Sonntag und 3. Dienstag im Monat".into()),
sessions: vec![
RegularSession::from(WeekdayOfMonth::new(1, Weekday::Sun))
.with_note("10:00 Uhr")
.except(
NEXT_SUN_SESSION,
Alternation {
new_day: Some(NEXT_SUN_SESSION.checked_sub_days(Days::new(1)).unwrap().into()),
..Default::default()
},
)
.except(
next_next_sun_session,
Alternation {
note: Some("11 Uhr".into()),
..Default::default()
},
)
.into(),
RegularSession::from(WeekdayOfMonth::new(3, Weekday::Tue))
.with_note("18:30 Uhr")
.except(NEXT_TUE_SESSION, Cancellation::new())
.into(),
ExtraSession::from(NEXT_TUE_SESSION.checked_add_days(Days::new(2)).unwrap())
.with_note("18 Uhr")
.into(),
],
Self {
motd: Some("Jeder 1. Sonntag und 3. Dienstag im Monat".into()),
sessions: vec![
RegularSession::from(WeekdayOfMonth::new(1, Weekday::Sun))
.with_note("10:00 Uhr")
.except(
NEXT_SUN_SESSION,
Alternation {
new_day: Some(NEXT_SUN_SESSION.checked_sub_days(Days::new(1)).unwrap().into()),
..Default::default()
},
)
.except(
next_next_sun_session,
Alternation {
note: Some("11 Uhr".into()),
..Default::default()
},
)
.into(),
RegularSession::from(WeekdayOfMonth::new(3, Weekday::Tue))
.with_note("18:30 Uhr")
.except(NEXT_TUE_SESSION, Cancellation::new())
.into(),
ExtraSession::from(NEXT_TUE_SESSION.checked_add_days(Days::new(2)).unwrap())
.with_note("18 Uhr")
.into(),
],
}
}
}
}

View File

@ -1,93 +1,112 @@
:root {
--darkred: darkred;
--darkgreen: #196e0a;
--darkgray: #292929;
--gray: #606060;
--white: white;
--black: black;
--red: red;
--yellow: #bbbb11;
--green: #1fd51f;
--darkred: darkred;
--darkgreen: #196e0a;
--darkgray: #292929;
--gray: #606060;
--white: white;
--black: black;
--red: red;
--yellow: #bbbb11;
--green: #1fd51f;
}
ul {
list-style-position: inside;
list-style-position: inside;
}
body {
font-size: 2rem;
font-size: 2rem;
}
html, body {
margin: 0;
padding: 0;
margin: 0;
padding: 0;
}
p {
margin: 0;
margin: 0;
}
@media screen and (max-width: 1000px) {
body {
font-size: 3rem;
}
body {
font-size: 3rem;
}
}
.background {
background-color: var(--darkgray);
color: var(--white);
font-family: Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
overflow: scroll;
background-color: var(--darkgray);
color: var(--white);
font-family: Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
overflow: scroll;
}
.column {
display: flex;
flex-direction: column;
gap: 10px;
display: flex;
flex-direction: column;
gap: 10px;
}
.box {
padding: 20px;
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
text-align: center;
justify-content: center;
align-items: center;
margin: 0 auto;
width: 80%;
max-width: 800px;
box-sizing: border-box;
padding: 20px;
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
text-align: center;
justify-content: center;
align-items: center;
margin: 0 auto;
width: 80%;
max-width: 800px;
box-sizing: border-box;
}
.wide {
width: 100%;
max-width: 1000px;
width: 100%;
max-width: 1000px;
}
.button {
cursor: pointer;
cursor: pointer;
}
.button:hover {
filter: brightness(1.2);
filter: brightness(1.2);
}
.elem-background {
background-color: var(--darkgreen);
background-color: var(--darkgreen);
}
.error-background {
background-color: var(--darkred);
background-color: var(--darkred);
}
.highlight-background {
background-color: var(--green);
background-color: var(--green);
}
.cancel-background {
background-color: var(--red);
background-color: var(--red);
}
.change-background {
background-color: var(--yellow);
background-color: var(--yellow);
}
.small-text {
color: var(--darkgray);
font-size: 1rem;
text-align: left;
color: var(--darkgray);
font-size: 1rem;
text-align: left;
}
.strikethrough {
text-decoration: line-through;
text-decoration: line-through;
}
.bold {
font-weight: bold;
font-weight: bold;
}

View File

@ -5,45 +5,45 @@ use std::ops::Deref;
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
#[repr(transparent)]
pub struct Day {
date: NaiveDate,
date: NaiveDate,
}
impl From<NaiveDate> for Day {
fn from(date: NaiveDate) -> Self {
Self { date }
}
fn from(date: NaiveDate) -> Self {
Self { date }
}
}
impl Default for Day {
fn default() -> Self {
Self::from(Local::now().date_naive())
}
fn default() -> Self {
Self::from(Local::now().date_naive())
}
}
impl Day {
pub fn date(&self) -> NaiveDate {
self.date
}
pub fn date(&self) -> NaiveDate {
self.date
}
pub fn weekday(&self) -> Weekday {
self.date.weekday()
}
pub fn weekday(&self) -> Weekday {
self.date.weekday()
}
pub fn weekday_of_month(&self) -> u8 {
self.date.day0() as u8 / 7 + 1
}
pub fn weekday_of_month(&self) -> u8 {
self.date.day0() as u8 / 7 + 1
}
}
impl Deref for Day {
type Target = NaiveDate;
type Target = NaiveDate;
fn deref(&self) -> &Self::Target {
&self.date
}
fn deref(&self) -> &Self::Target {
&self.date
}
}
impl From<Day> for NaiveDate {
fn from(day: Day) -> Self {
day.date
}
fn from(day: Day) -> Self {
day.date
}
}

View File

@ -64,8 +64,8 @@ macro_rules! impl_from {
#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub enum Session {
Regular(RegularSession),
Extra(ExtraSession),
Regular(RegularSession),
Extra(ExtraSession),
}
impl_from!(RegularSession for Session as Regular);
@ -73,42 +73,42 @@ impl_from!(ExtraSession for Session as Extra);
#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub struct RegularSession {
pub rule: SessionRule,
pub note: Note,
#[serde(with = "serde_json_any_key::any_key_map")]
pub except: BTreeMap<Day, Except>,
pub rule: SessionRule,
pub note: Note,
#[serde(with = "serde_json_any_key::any_key_map")]
pub except: BTreeMap<Day, Except>,
}
impl From<SessionRule> for RegularSession {
fn from(rule: SessionRule) -> Self {
Self {
rule,
note: None,
except: Default::default(),
fn from(rule: SessionRule) -> Self {
Self {
rule,
note: None,
except: Default::default(),
}
}
}
}
impl RegularSession {
pub fn except<D, E>(mut self, day: D, except: E) -> Self
where
D: Into<Day>,
E: Into<Except>,
{
self.except.insert(day.into(), except.into());
self
}
pub fn except<D, E>(mut self, day: D, except: E) -> Self
where
D: Into<Day>,
E: Into<Except>,
{
self.except.insert(day.into(), except.into());
self
}
///gets the next session day, where no except applies. Can possibly return the `current_date`.
pub fn next_regular_session_day<D>(&self, current_date: D) -> Option<Day>
where
D: Into<Day>,
{
self
.rule
.to_session_day_iter(current_date)
.find(|day| !self.except.contains_key(day))
}
///gets the next session day, where no except applies. Can possibly return the `current_date`.
pub fn next_regular_session_day<D>(&self, current_date: D) -> Option<Day>
where
D: Into<Day>,
{
self
.rule
.to_session_day_iter(current_date)
.find(|day| !self.except.contains_key(day))
}
}
impl_from!(WeekdayOfMonth for RegularSession by SessionRule);
@ -117,14 +117,14 @@ impl_with_note!(RegularSession);
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
pub struct ExtraSession {
pub day: Day,
pub note: Note,
pub day: Day,
pub note: Note,
}
impl From<Day> for ExtraSession {
fn from(day: Day) -> Self {
Self { day, note: None }
}
fn from(day: Day) -> Self {
Self { day, note: None }
}
}
impl_from!(NaiveDate for ExtraSession by Day);
@ -132,15 +132,15 @@ impl_opt_noted!(ExtraSession);
impl_with_note!(ExtraSession);
impl Dated for ExtraSession {
fn day(&self) -> Day {
self.day
}
fn day(&self) -> Day {
self.day
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub enum Except {
Alternation(Alternation),
Cancellation(Cancellation),
Alternation(Alternation),
Cancellation(Cancellation),
}
impl_from!(Alternation for Except);
@ -149,18 +149,18 @@ impl_opt_noted!(Except with Alternation, Cancellation);
#[derive(Debug, Clone, Eq, PartialEq, Hash, Default, Serialize, Deserialize)]
pub struct Alternation {
pub note: Note,
///the date when the alternation should show up in the calendar, or the original date
pub new_day: Option<Day>,
pub note: Note,
///the date when the alternation should show up in the calendar, or the original date
pub new_day: Option<Day>,
}
impl From<Day> for Alternation {
fn from(day: Day) -> Self {
Self {
new_day: Some(day),
..Default::default()
fn from(day: Day) -> Self {
Self {
new_day: Some(day),
..Default::default()
}
}
}
}
impl_from!(NaiveDate for Alternation by Day);
@ -170,27 +170,27 @@ impl_with_note!(Alternation);
#[derive(Debug, Clone, Eq, PartialEq, Hash, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct Cancellation {
pub note: Note,
pub note: Note,
}
impl Cancellation {
pub fn new() -> Self {
Self::default()
}
pub fn new() -> Self {
Self::default()
}
}
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;
/// The day when this should show up in a calendar
fn day(&self) -> Day;
}
pub trait OptNoted {
fn note(&self) -> Option<&str>;
fn note(&self) -> Option<&str>;
}
pub trait WithNote {
fn with_note(self, note: &str) -> Self;
fn with_note(self, note: &str) -> Self;
}

View File

@ -1,7 +1,7 @@
use crate::day::Day;
use crate::session::rule::SessionRuleLike;
use crate::session::{
Alternation, Cancellation, Dated, Except, ExtraSession, OptNoted, RegularSession, Session,
Alternation, Cancellation, Dated, Except, ExtraSession, OptNoted, RegularSession, Session,
};
use chrono::Days;
use serde::{Deserialize, Serialize};
@ -10,308 +10,308 @@ use std::hash::Hash;
#[derive(Debug, Clone)]
pub struct DatedSessionIter {
sessions: BTreeMap<Day, VecDeque<DatedSession>>,
sessions: BTreeMap<Day, VecDeque<DatedSession>>,
}
impl DatedSessionIter {
pub fn new<D, S>(start_date: D, sessions: S) -> DatedSessionIter
where
D: Into<Day>,
S: IntoIterator<Item = Session>,
{
let current_date = start_date.into();
pub fn new<D, S>(start_date: D, sessions: S) -> DatedSessionIter
where
D: Into<Day>,
S: IntoIterator<Item=Session>,
{
let current_date = start_date.into();
//map every session and their excepts 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, _)| session.rule.accepts(day))
.map(|(&day, except)| match except {
Except::Alternation(alternation) => DatedSession::Altered {
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) => vec![extra.into()],
})
//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
},
);
//map every session and their excepts 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, _)| session.rule.accepts(day))
.map(|(&day, except)| match except {
Except::Alternation(alternation) => DatedSession::Altered {
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) => vec![extra.into()],
})
//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 { sessions }
}
Self { sessions }
}
}
impl Iterator for DatedSessionIter {
type Item = DatedSession;
type Item = DatedSession;
fn next(&mut self) -> Option<Self::Item> {
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(),
})
fn next(&mut self) -> Option<Self::Item> {
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)
}
Some(session)
}
}
#[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,
},
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<ExtraSession> for DatedSession {
fn from(value: ExtraSession) -> Self {
Self::Extra(value)
}
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.new_day.unwrap_or(day),
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.new_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(),
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(),
}
}
}
}
#[cfg(test)]
mod test {
use crate::session::iter::{DatedSession, DatedSessionIter};
use crate::session::rule::WeekdayOfMonth;
use crate::session::{Alternation, Cancellation, ExtraSession, RegularSession, WithNote};
use crate::test_util::{date, day};
use chrono::Weekday;
use crate::session::iter::{DatedSession, DatedSessionIter};
use crate::session::rule::WeekdayOfMonth;
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));
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()],
);
#[test]
fn test_regular() {
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()],
);
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()
})
);
}
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()
})
);
}
#[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)));
#[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()],
);
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(),
})
);
}
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());
#[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()]);
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()
})
);
}
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());
#[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(),
],
);
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()
})
);
}
//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()
})
);
}
}

View File

@ -5,148 +5,148 @@ use std::ops::Deref;
#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub enum SessionRule {
WeekdayOfMonth(WeekdayOfMonth),
WeekdayOfMonth(WeekdayOfMonth),
}
impl From<WeekdayOfMonth> for SessionRule {
fn from(value: WeekdayOfMonth) -> Self {
Self::WeekdayOfMonth(value)
}
fn from(value: WeekdayOfMonth) -> Self {
Self::WeekdayOfMonth(value)
}
}
impl SessionRule {
pub fn to_session_day_iter<D>(&self, start_date: D) -> SessionDayIter<&Self>
where
D: Into<Day>,
{
SessionDayIter {
rule: self,
start_date: Some(start_date.into()),
pub fn to_session_day_iter<D>(&self, start_date: D) -> SessionDayIter<&Self>
where
D: Into<Day>,
{
SessionDayIter {
rule: self,
start_date: Some(start_date.into()),
}
}
}
}
impl SessionRuleLike for SessionRule {
fn determine_next_date(&self, current_date: Day) -> Option<Day> {
match self {
SessionRule::WeekdayOfMonth(weekday_of_month) => {
weekday_of_month.determine_next_date(current_date)
}
fn determine_next_date(&self, current_date: Day) -> Option<Day> {
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),
fn accepts(&self, day: Day) -> bool {
match self {
SessionRule::WeekdayOfMonth(weekday_of_month) => weekday_of_month.accepts(day),
}
}
}
}
#[derive(Debug, Clone)]
pub struct SessionDayIter<R> {
pub rule: R,
pub start_date: Option<Day>,
pub rule: R,
pub start_date: Option<Day>,
}
impl<R, S> Iterator for SessionDayIter<R>
where
R: Deref<Target = S>,
S: SessionRuleLike,
R: Deref<Target=S>,
S: SessionRuleLike,
{
type Item = Day;
type Item = Day;
fn next(&mut self) -> Option<Self::Item> {
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
}
fn next(&mut self) -> Option<Self::Item> {
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
}
}
impl<R> SessionDayIter<R>
where
R: Deref<Target = SessionRule>,
R: Deref<Target=SessionRule>,
{
pub fn to_owned(&self) -> SessionDayIter<SessionRule> {
SessionDayIter {
rule: self.rule.clone(),
start_date: self.start_date,
pub fn to_owned(&self) -> SessionDayIter<SessionRule> {
SessionDayIter {
rule: self.rule.clone(),
start_date: self.start_date,
}
}
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub struct WeekdayOfMonth {
pub n: u8,
pub weekday: Weekday,
pub n: u8,
pub weekday: Weekday,
}
impl WeekdayOfMonth {
pub fn new(n: u8, weekday: Weekday) -> Self {
Self { n, weekday }
}
pub fn new(n: u8, weekday: Weekday) -> Self {
Self { n, weekday }
}
}
impl SessionRuleLike for WeekdayOfMonth {
fn determine_next_date(&self, current_date: Day) -> Option<Day> {
let session_this_month = NaiveDate::from_weekday_of_month_opt(
current_date.year(),
current_date.month(),
self.weekday,
self.n,
)?;
fn determine_next_date(&self, current_date: Day) -> Option<Day> {
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,
)?
};
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())
}
Some(date.into())
}
fn accepts(&self, day: Day) -> bool {
day.weekday() == self.weekday && day.weekday_of_month() == self.n
}
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 `current_date`.
fn determine_next_date(&self, current_date: Day) -> Option<Day>;
/// Determines the next session date in form of a [Day], possibly including the `current_date`.
fn determine_next_date(&self, current_date: Day) -> Option<Day>;
/// Whether this rule would be able to produce the given `day`.
fn accepts(&self, day: Day) -> bool;
/// 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;
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);
#[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))
);
}
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))
);
}
}

View File

@ -2,9 +2,9 @@ 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")
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()
date(day, month, year).into()
}