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" resolver = "2"
members = [ members = [
"leptos_webpage", "session_iter", "leptos_webpage", "session_iter", "app",
] ]
[profile] [profile]

View File

@ -3,13 +3,13 @@
The webpage of Jana Sessions (unofficial name) written in Rust. 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. 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 ## session_iter
The logic to find the dates of upcoming sessions. The logic to find the dates of upcoming sessions.
## leptos_webpage ## 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. 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" console_error_panic_hook = "0.1.7"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" 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`). 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`). 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 ```bash
trunk build --release trunk build --release
``` ```
which will create the app in the `target/dist` folder. which will create the app in the `target/dist` folder.
Alternatively you can serve it locally with Alternatively you can serve it locally with
```bash ```bash
trunk serve trunk serve
``` ```
To also access the local hosted page from other devices, use To also access the local hosted page from other devices, use
```bash ```bash
trunk serve -a 0.0.0.0 trunk serve -a 0.0.0.0
``` ```
## Deployment ## Deployment
Just use pythons webserver and point it to the dist folder Just use pythons webserver and point it to the dist folder
```bash ```bash
python3 -m http.server 8080 --directoy target/dist python3 -m http.server 8080 --directoy target/dist
``` ```

View File

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

View File

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

View File

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

View File

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

View File

@ -1,93 +1,112 @@
:root { :root {
--darkred: darkred; --darkred: darkred;
--darkgreen: #196e0a; --darkgreen: #196e0a;
--darkgray: #292929; --darkgray: #292929;
--gray: #606060; --gray: #606060;
--white: white; --white: white;
--black: black; --black: black;
--red: red; --red: red;
--yellow: #bbbb11; --yellow: #bbbb11;
--green: #1fd51f; --green: #1fd51f;
} }
ul { ul {
list-style-position: inside; list-style-position: inside;
} }
body { body {
font-size: 2rem; font-size: 2rem;
} }
html, body { html, body {
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
p { p {
margin: 0; margin: 0;
} }
@media screen and (max-width: 1000px) { @media screen and (max-width: 1000px) {
body { body {
font-size: 3rem; font-size: 3rem;
} }
} }
.background { .background {
background-color: var(--darkgray); background-color: var(--darkgray);
color: var(--white); color: var(--white);
font-family: Arial, sans-serif; font-family: Arial, sans-serif;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
height: 100vh; height: 100vh;
margin: 0; margin: 0;
overflow: scroll; overflow: scroll;
} }
.column { .column {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 10px; gap: 10px;
} }
.box { .box {
padding: 20px; padding: 20px;
border-radius: 10px; border-radius: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
text-align: center; text-align: center;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
margin: 0 auto; margin: 0 auto;
width: 80%; width: 80%;
max-width: 800px; max-width: 800px;
box-sizing: border-box; box-sizing: border-box;
} }
.wide { .wide {
width: 100%; width: 100%;
max-width: 1000px; max-width: 1000px;
} }
.button { .button {
cursor: pointer; cursor: pointer;
} }
.button:hover { .button:hover {
filter: brightness(1.2); filter: brightness(1.2);
} }
.elem-background { .elem-background {
background-color: var(--darkgreen); background-color: var(--darkgreen);
} }
.error-background { .error-background {
background-color: var(--darkred); background-color: var(--darkred);
} }
.highlight-background { .highlight-background {
background-color: var(--green); background-color: var(--green);
} }
.cancel-background { .cancel-background {
background-color: var(--red); background-color: var(--red);
} }
.change-background { .change-background {
background-color: var(--yellow); background-color: var(--yellow);
} }
.small-text { .small-text {
color: var(--darkgray); color: var(--darkgray);
font-size: 1rem; font-size: 1rem;
text-align: left; text-align: left;
} }
.strikethrough { .strikethrough {
text-decoration: line-through; text-decoration: line-through;
} }
.bold { .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)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
#[repr(transparent)] #[repr(transparent)]
pub struct Day { pub struct Day {
date: NaiveDate, date: NaiveDate,
} }
impl From<NaiveDate> for Day { impl From<NaiveDate> for Day {
fn from(date: NaiveDate) -> Self { fn from(date: NaiveDate) -> Self {
Self { date } Self { date }
} }
} }
impl Default for Day { impl Default for Day {
fn default() -> Self { fn default() -> Self {
Self::from(Local::now().date_naive()) Self::from(Local::now().date_naive())
} }
} }
impl Day { impl Day {
pub fn date(&self) -> NaiveDate { pub fn date(&self) -> NaiveDate {
self.date self.date
} }
pub fn weekday(&self) -> Weekday { pub fn weekday(&self) -> Weekday {
self.date.weekday() self.date.weekday()
} }
pub fn weekday_of_month(&self) -> u8 { pub fn weekday_of_month(&self) -> u8 {
self.date.day0() as u8 / 7 + 1 self.date.day0() as u8 / 7 + 1
} }
} }
impl Deref for Day { impl Deref for Day {
type Target = NaiveDate; type Target = NaiveDate;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
&self.date &self.date
} }
} }
impl From<Day> for NaiveDate { impl From<Day> for NaiveDate {
fn from(day: Day) -> Self { fn from(day: Day) -> Self {
day.date day.date
} }
} }

View File

@ -64,8 +64,8 @@ macro_rules! impl_from {
#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] #[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub enum Session { pub enum Session {
Regular(RegularSession), Regular(RegularSession),
Extra(ExtraSession), Extra(ExtraSession),
} }
impl_from!(RegularSession for Session as Regular); 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)] #[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub struct RegularSession { pub struct RegularSession {
pub rule: SessionRule, pub rule: SessionRule,
pub note: Note, pub note: Note,
#[serde(with = "serde_json_any_key::any_key_map")] #[serde(with = "serde_json_any_key::any_key_map")]
pub except: BTreeMap<Day, Except>, pub except: BTreeMap<Day, Except>,
} }
impl From<SessionRule> for RegularSession { impl From<SessionRule> for RegularSession {
fn from(rule: SessionRule) -> Self { fn from(rule: SessionRule) -> Self {
Self { Self {
rule, rule,
note: None, note: None,
except: Default::default(), except: Default::default(),
}
} }
}
} }
impl RegularSession { impl RegularSession {
pub fn except<D, E>(mut self, day: D, except: E) -> Self pub fn except<D, E>(mut self, day: D, except: E) -> Self
where where
D: Into<Day>, D: Into<Day>,
E: Into<Except>, E: Into<Except>,
{ {
self.except.insert(day.into(), except.into()); self.except.insert(day.into(), except.into());
self self
} }
///gets the next session day, where no except applies. Can possibly return the `current_date`. ///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> pub fn next_regular_session_day<D>(&self, current_date: D) -> Option<Day>
where where
D: Into<Day>, D: Into<Day>,
{ {
self self
.rule .rule
.to_session_day_iter(current_date) .to_session_day_iter(current_date)
.find(|day| !self.except.contains_key(day)) .find(|day| !self.except.contains_key(day))
} }
} }
impl_from!(WeekdayOfMonth for RegularSession by SessionRule); 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)] #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
pub struct ExtraSession { pub struct ExtraSession {
pub day: Day, pub day: Day,
pub note: Note, pub note: Note,
} }
impl From<Day> for ExtraSession { impl From<Day> for ExtraSession {
fn from(day: Day) -> Self { fn from(day: Day) -> Self {
Self { day, note: None } Self { day, note: None }
} }
} }
impl_from!(NaiveDate for ExtraSession by Day); impl_from!(NaiveDate for ExtraSession by Day);
@ -132,15 +132,15 @@ impl_opt_noted!(ExtraSession);
impl_with_note!(ExtraSession); impl_with_note!(ExtraSession);
impl Dated for ExtraSession { impl Dated for ExtraSession {
fn day(&self) -> Day { fn day(&self) -> Day {
self.day self.day
} }
} }
#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] #[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub enum Except { pub enum Except {
Alternation(Alternation), Alternation(Alternation),
Cancellation(Cancellation), Cancellation(Cancellation),
} }
impl_from!(Alternation for Except); 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)] #[derive(Debug, Clone, Eq, PartialEq, Hash, Default, Serialize, Deserialize)]
pub struct Alternation { pub struct Alternation {
pub note: Note, pub note: Note,
///the date when the alternation should show up in the calendar, or the original date ///the date when the alternation should show up in the calendar, or the original date
pub new_day: Option<Day>, pub new_day: Option<Day>,
} }
impl From<Day> for Alternation { impl From<Day> for Alternation {
fn from(day: Day) -> Self { fn from(day: Day) -> Self {
Self { Self {
new_day: Some(day), new_day: Some(day),
..Default::default() ..Default::default()
}
} }
}
} }
impl_from!(NaiveDate for Alternation by Day); impl_from!(NaiveDate for Alternation by Day);
@ -170,27 +170,27 @@ impl_with_note!(Alternation);
#[derive(Debug, Clone, Eq, PartialEq, Hash, Default, Serialize, Deserialize)] #[derive(Debug, Clone, Eq, PartialEq, Hash, Default, Serialize, Deserialize)]
#[serde(default)] #[serde(default)]
pub struct Cancellation { pub struct Cancellation {
pub note: Note, pub note: Note,
} }
impl Cancellation { impl Cancellation {
pub fn new() -> Self { pub fn new() -> Self {
Self::default() Self::default()
} }
} }
impl_opt_noted!(Cancellation); impl_opt_noted!(Cancellation);
impl_with_note!(Cancellation); impl_with_note!(Cancellation);
pub trait Dated { pub trait Dated {
/// The day when this should show up in a calendar /// The day when this should show up in a calendar
fn day(&self) -> Day; fn day(&self) -> Day;
} }
pub trait OptNoted { pub trait OptNoted {
fn note(&self) -> Option<&str>; fn note(&self) -> Option<&str>;
} }
pub trait WithNote { 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::day::Day;
use crate::session::rule::SessionRuleLike; use crate::session::rule::SessionRuleLike;
use crate::session::{ use crate::session::{
Alternation, Cancellation, Dated, Except, ExtraSession, OptNoted, RegularSession, Session, Alternation, Cancellation, Dated, Except, ExtraSession, OptNoted, RegularSession, Session,
}; };
use chrono::Days; use chrono::Days;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -10,308 +10,308 @@ use std::hash::Hash;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct DatedSessionIter { pub struct DatedSessionIter {
sessions: BTreeMap<Day, VecDeque<DatedSession>>, sessions: BTreeMap<Day, VecDeque<DatedSession>>,
} }
impl DatedSessionIter { impl DatedSessionIter {
pub fn new<D, S>(start_date: D, sessions: S) -> DatedSessionIter pub fn new<D, S>(start_date: D, sessions: S) -> DatedSessionIter
where where
D: Into<Day>, D: Into<Day>,
S: IntoIterator<Item = Session>, S: IntoIterator<Item=Session>,
{ {
let current_date = start_date.into(); let current_date = start_date.into();
//map every session and their excepts 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 = let sessions =
sessions sessions
.into_iter() .into_iter()
.flat_map(|session| match session { .flat_map(|session| match session {
Session::Regular(session) => session Session::Regular(session) => session
.except .except
.iter() .iter()
.filter(|(&day, _)| session.rule.accepts(day)) .filter(|(&day, _)| session.rule.accepts(day))
.map(|(&day, except)| match except { .map(|(&day, except)| match except {
Except::Alternation(alternation) => DatedSession::Altered { Except::Alternation(alternation) => DatedSession::Altered {
day, day,
regular: session.clone(), regular: session.clone(),
cause: alternation.clone(), cause: alternation.clone(),
}, },
Except::Cancellation(cancellation) => DatedSession::Canceled { Except::Cancellation(cancellation) => DatedSession::Canceled {
day, day,
regular: session.clone(), regular: session.clone(),
cause: cancellation.clone(), cause: cancellation.clone(),
}, },
}) })
.chain(session.next_regular_session_day(current_date).map(|day| { .chain(session.next_regular_session_day(current_date).map(|day| {
DatedSession::Regular { DatedSession::Regular {
day, day,
session: session.clone(), session: session.clone(),
} }
})) }))
.collect(), .collect(),
Session::Extra(extra) => vec![extra.into()], Session::Extra(extra) => vec![extra.into()],
}) })
//filter out and entries which would lay in the past //filter out and entries which would lay in the past
.filter(|dated_session| dated_session.day() >= current_date) .filter(|dated_session| dated_session.day() >= current_date)
//group sessions on the same day together //group sessions on the same day together
.fold( .fold(
BTreeMap::<_, VecDeque<_>>::new(), BTreeMap::<_, VecDeque<_>>::new(),
|mut map, dated_session| { |mut map, dated_session| {
map map
.entry(dated_session.day()) .entry(dated_session.day())
.or_default() .or_default()
.push_back(dated_session); .push_back(dated_session);
map map
}, },
); );
Self { sessions } Self { sessions }
} }
} }
impl Iterator for DatedSessionIter { impl Iterator for DatedSessionIter {
type Item = DatedSession; type Item = DatedSession;
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
let mut entry = self.sessions.first_entry()?; let mut entry = self.sessions.first_entry()?;
let session = entry.get_mut().pop_front()?; let session = entry.get_mut().pop_front()?;
if entry.get().is_empty() { if entry.get().is_empty() {
entry.remove(); 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(),
})
} }
}
//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)] #[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub enum DatedSession { pub enum DatedSession {
Regular { Regular {
day: Day, day: Day,
session: RegularSession, session: RegularSession,
}, },
Extra(ExtraSession), Extra(ExtraSession),
Canceled { Canceled {
day: Day, day: Day,
regular: RegularSession, regular: RegularSession,
cause: Cancellation, cause: Cancellation,
}, },
Altered { Altered {
///the day when the regular session would have applied ///the day when the regular session would have applied
day: Day, day: Day,
regular: RegularSession, regular: RegularSession,
cause: Alternation, cause: Alternation,
}, },
} }
impl From<ExtraSession> for DatedSession { impl From<ExtraSession> for DatedSession {
fn from(value: ExtraSession) -> Self { fn from(value: ExtraSession) -> Self {
Self::Extra(value) Self::Extra(value)
} }
} }
impl Dated for DatedSession { impl Dated for DatedSession {
fn day(&self) -> Day { fn day(&self) -> Day {
match *self { match *self {
DatedSession::Regular { day, .. } => day, DatedSession::Regular { day, .. } => day,
DatedSession::Extra(ref extra) => extra.day(), DatedSession::Extra(ref extra) => extra.day(),
DatedSession::Canceled { day, .. } => day, DatedSession::Canceled { day, .. } => day,
DatedSession::Altered { day, ref cause, .. } => cause.new_day.unwrap_or(day), DatedSession::Altered { day, ref cause, .. } => cause.new_day.unwrap_or(day),
}
} }
}
} }
impl OptNoted for DatedSession { impl OptNoted for DatedSession {
fn note(&self) -> Option<&str> { fn note(&self) -> Option<&str> {
match self { match self {
DatedSession::Regular { session, .. } => session.note(), DatedSession::Regular { session, .. } => session.note(),
DatedSession::Extra(extra) => extra.note(), DatedSession::Extra(extra) => extra.note(),
DatedSession::Canceled { cause, .. } => cause.note(), DatedSession::Canceled { cause, .. } => cause.note(),
DatedSession::Altered { cause, .. } => cause.note(), DatedSession::Altered { cause, .. } => cause.note(),
}
} }
}
} }
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::session::iter::{DatedSession, DatedSessionIter}; use crate::session::iter::{DatedSession, DatedSessionIter};
use crate::session::rule::WeekdayOfMonth; use crate::session::rule::WeekdayOfMonth;
use crate::session::{Alternation, Cancellation, ExtraSession, RegularSession, WithNote}; use crate::session::{Alternation, Cancellation, ExtraSession, RegularSession, WithNote};
use crate::test_util::{date, day}; use crate::test_util::{date, day};
use chrono::Weekday; use chrono::Weekday;
#[test] #[test]
fn test_regular() { fn test_regular() {
let tue_session = RegularSession::from(WeekdayOfMonth::new(3, Weekday::Tue)); let tue_session = RegularSession::from(WeekdayOfMonth::new(3, Weekday::Tue));
let sun_session = RegularSession::from(WeekdayOfMonth::new(1, Weekday::Sun)); let sun_session = RegularSession::from(WeekdayOfMonth::new(1, Weekday::Sun));
let mut iter = DatedSessionIter::new( let mut iter = DatedSessionIter::new(
date(17, 2, 2025), date(17, 2, 2025),
[tue_session.clone().into(), sun_session.clone().into()], [tue_session.clone().into(), sun_session.clone().into()],
); );
assert_eq!( assert_eq!(
iter.next(), iter.next(),
Some(DatedSession::Regular { Some(DatedSession::Regular {
day: day(18, 2, 2025), day: day(18, 2, 2025),
session: tue_session.clone() session: tue_session.clone()
}) })
); );
assert_eq!( assert_eq!(
iter.next(), iter.next(),
Some(DatedSession::Regular { Some(DatedSession::Regular {
day: day(2, 3, 2025), day: day(2, 3, 2025),
session: sun_session.clone() session: sun_session.clone()
}) })
); );
assert_eq!( assert_eq!(
iter.next(), iter.next(),
Some(DatedSession::Regular { Some(DatedSession::Regular {
day: day(18, 3, 2025), day: day(18, 3, 2025),
session: tue_session.clone() session: tue_session.clone()
}) })
); );
} }
#[test] #[test]
fn test_old() { fn test_old() {
let extra_session = ExtraSession::from(day(15, 2, 2025)); let extra_session = ExtraSession::from(day(15, 2, 2025));
let regular_session = RegularSession::from(WeekdayOfMonth::new(3, Weekday::Tue)) let regular_session = RegularSession::from(WeekdayOfMonth::new(3, Weekday::Tue))
.with_note("18:30 Uhr") .with_note("18:30 Uhr")
.except(day(18, 2, 2025), Cancellation::new()) .except(day(18, 2, 2025), Cancellation::new())
.except(day(18, 3, 2025), Alternation::from(day(21, 1, 2025))); .except(day(18, 3, 2025), Alternation::from(day(21, 1, 2025)));
let mut iter = DatedSessionIter::new( let mut iter = DatedSessionIter::new(
day(19, 2, 2025), day(19, 2, 2025),
[regular_session.clone().into(), extra_session.into()], [regular_session.clone().into(), extra_session.into()],
); );
assert_eq!( assert_eq!(
iter.next(), iter.next(),
Some(DatedSession::Regular { Some(DatedSession::Regular {
day: day(15, 4, 2025), day: day(15, 4, 2025),
session: regular_session.clone(), session: regular_session.clone(),
}) })
); );
} }
#[test] #[test]
fn test_invalid_except() { fn test_invalid_except() {
let regular_session = RegularSession::from(WeekdayOfMonth::new(3, Weekday::Tue)) let regular_session = RegularSession::from(WeekdayOfMonth::new(3, Weekday::Tue))
.except(day(17, 2, 2025), Cancellation::new()); .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!( assert_eq!(
iter.next(), iter.next(),
Some(DatedSession::Regular { Some(DatedSession::Regular {
day: day(18, 2, 2025), day: day(18, 2, 2025),
session: regular_session.clone() session: regular_session.clone()
}) })
); );
} }
#[test] #[test]
fn test_extra_altered_and_canceled() { fn test_extra_altered_and_canceled() {
//an extra session on the same day as a tuesday session //an extra session on the same day as a tuesday session
let extra_session = ExtraSession::from(day(18, 2, 2025)).with_note("morning session"); let extra_session = ExtraSession::from(day(18, 2, 2025)).with_note("morning session");
//an alternation of a tuesday session without moving it //an alternation of a tuesday session without moving it
let (note_alternation_day, note_alternation) = ( let (note_alternation_day, note_alternation) = (
day(18, 3, 2025), day(18, 3, 2025),
Alternation { Alternation {
note: Some("a little different today".into()), note: Some("a little different today".into()),
..Default::default() ..Default::default()
}, },
); );
let tue_session = RegularSession::from(WeekdayOfMonth::new(3, Weekday::Tue)) let tue_session = RegularSession::from(WeekdayOfMonth::new(3, Weekday::Tue))
.except(note_alternation_day, note_alternation.clone()); .except(note_alternation_day, note_alternation.clone());
//a sunday session moved in front of a tuesday session //a sunday session moved in front of a tuesday session
let moved_sun_session_day = day(16, 2, 2025); let moved_sun_session_day = day(16, 2, 2025);
let (day_alternation_day, day_alternation) = let (day_alternation_day, day_alternation) =
(day(2, 3, 2025), Alternation::from(moved_sun_session_day)); (day(2, 3, 2025), Alternation::from(moved_sun_session_day));
//a canceled sunday session //a canceled sunday session
let (cancel_day, cancellation) = (day(6, 4, 2025), Cancellation::new()); let (cancel_day, cancellation) = (day(6, 4, 2025), Cancellation::new());
let sun_session = RegularSession::from(WeekdayOfMonth::new(1, Weekday::Sun)) let sun_session = RegularSession::from(WeekdayOfMonth::new(1, Weekday::Sun))
.except(day_alternation_day, day_alternation.clone()) .except(day_alternation_day, day_alternation.clone())
.except(cancel_day, cancellation.clone()); .except(cancel_day, cancellation.clone());
let mut iter = DatedSessionIter::new( let mut iter = DatedSessionIter::new(
day(15, 2, 2025), day(15, 2, 2025),
[ [
extra_session.clone().into(), extra_session.clone().into(),
tue_session.clone().into(), tue_session.clone().into(),
sun_session.clone().into(), sun_session.clone().into(),
], ],
); );
//at first comes the moved sunday //at first comes the moved sunday
assert_eq!( assert_eq!(
iter.next(), iter.next(),
Some(DatedSession::Altered { Some(DatedSession::Altered {
day: day_alternation_day, day: day_alternation_day,
regular: sun_session.clone(), regular: sun_session.clone(),
cause: day_alternation.clone(), cause: day_alternation.clone(),
}) })
); );
//then comes the extra session on tuesday (because it was first in the list) //then comes the extra session on tuesday (because it was first in the list)
assert_eq!( assert_eq!(
iter.next(), iter.next(),
Some(DatedSession::Extra(extra_session.clone())) Some(DatedSession::Extra(extra_session.clone()))
); );
//after that comes a regular tuesday session //after that comes a regular tuesday session
assert_eq!( assert_eq!(
iter.next(), iter.next(),
Some(DatedSession::Regular { Some(DatedSession::Regular {
day: day(18, 2, 2025), day: day(18, 2, 2025),
session: tue_session.clone() session: tue_session.clone()
}) })
); );
//now we have an altered but not moved tuesday session //now we have an altered but not moved tuesday session
assert_eq!( assert_eq!(
iter.next(), iter.next(),
Some(DatedSession::Altered { Some(DatedSession::Altered {
day: note_alternation_day, day: note_alternation_day,
regular: tue_session.clone(), regular: tue_session.clone(),
cause: note_alternation.clone(), cause: note_alternation.clone(),
}) })
); );
//the next sunday session was canceled //the next sunday session was canceled
assert_eq!( assert_eq!(
iter.next(), iter.next(),
Some(DatedSession::Canceled { Some(DatedSession::Canceled {
day: cancel_day, day: cancel_day,
regular: sun_session.clone(), regular: sun_session.clone(),
cause: cancellation.clone(), cause: cancellation.clone(),
}) })
); );
//and now we are back to regular sessions //and now we are back to regular sessions
assert_eq!( assert_eq!(
iter.next(), iter.next(),
Some(DatedSession::Regular { Some(DatedSession::Regular {
day: day(15, 4, 2025), day: day(15, 4, 2025),
session: tue_session.clone() session: tue_session.clone()
}) })
); );
assert_eq!( assert_eq!(
iter.next(), iter.next(),
Some(DatedSession::Regular { Some(DatedSession::Regular {
day: day(4, 5, 2025), day: day(4, 5, 2025),
session: sun_session.clone() session: sun_session.clone()
}) })
); );
} }
} }

View File

@ -5,148 +5,148 @@ use std::ops::Deref;
#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] #[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub enum SessionRule { pub enum SessionRule {
WeekdayOfMonth(WeekdayOfMonth), WeekdayOfMonth(WeekdayOfMonth),
} }
impl From<WeekdayOfMonth> for SessionRule { impl From<WeekdayOfMonth> for SessionRule {
fn from(value: WeekdayOfMonth) -> Self { fn from(value: WeekdayOfMonth) -> Self {
Self::WeekdayOfMonth(value) Self::WeekdayOfMonth(value)
} }
} }
impl SessionRule { impl SessionRule {
pub fn to_session_day_iter<D>(&self, start_date: D) -> SessionDayIter<&Self> pub fn to_session_day_iter<D>(&self, start_date: D) -> SessionDayIter<&Self>
where where
D: Into<Day>, D: Into<Day>,
{ {
SessionDayIter { SessionDayIter {
rule: self, rule: self,
start_date: Some(start_date.into()), start_date: Some(start_date.into()),
}
} }
}
} }
impl SessionRuleLike for SessionRule { impl SessionRuleLike for SessionRule {
fn determine_next_date(&self, current_date: Day) -> Option<Day> { fn determine_next_date(&self, current_date: Day) -> Option<Day> {
match self { match self {
SessionRule::WeekdayOfMonth(weekday_of_month) => { SessionRule::WeekdayOfMonth(weekday_of_month) => {
weekday_of_month.determine_next_date(current_date) weekday_of_month.determine_next_date(current_date)
} }
}
} }
}
fn accepts(&self, day: Day) -> bool { fn accepts(&self, day: Day) -> bool {
match self { match self {
SessionRule::WeekdayOfMonth(weekday_of_month) => weekday_of_month.accepts(day), SessionRule::WeekdayOfMonth(weekday_of_month) => weekday_of_month.accepts(day),
}
} }
}
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct SessionDayIter<R> { pub struct SessionDayIter<R> {
pub rule: R, pub rule: R,
pub start_date: Option<Day>, pub start_date: Option<Day>,
} }
impl<R, S> Iterator for SessionDayIter<R> impl<R, S> Iterator for SessionDayIter<R>
where where
R: Deref<Target = S>, R: Deref<Target=S>,
S: SessionRuleLike, S: SessionRuleLike,
{ {
type Item = Day; type Item = Day;
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
let start_date = self.start_date?; let start_date = self.start_date?;
let session_date = self.rule.determine_next_date(start_date); let session_date = self.rule.determine_next_date(start_date);
self.start_date = session_date self.start_date = session_date
.and_then(|session_date| session_date.checked_add_days(Days::new(1))) .and_then(|session_date| session_date.checked_add_days(Days::new(1)))
.map(Day::from); .map(Day::from);
session_date session_date
} }
} }
impl<R> SessionDayIter<R> impl<R> SessionDayIter<R>
where where
R: Deref<Target = SessionRule>, R: Deref<Target=SessionRule>,
{ {
pub fn to_owned(&self) -> SessionDayIter<SessionRule> { pub fn to_owned(&self) -> SessionDayIter<SessionRule> {
SessionDayIter { SessionDayIter {
rule: self.rule.clone(), rule: self.rule.clone(),
start_date: self.start_date, start_date: self.start_date,
}
} }
}
} }
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub struct WeekdayOfMonth { pub struct WeekdayOfMonth {
pub n: u8, pub n: u8,
pub weekday: Weekday, pub weekday: Weekday,
} }
impl WeekdayOfMonth { impl WeekdayOfMonth {
pub fn new(n: u8, weekday: Weekday) -> Self { pub fn new(n: u8, weekday: Weekday) -> Self {
Self { n, weekday } Self { n, weekday }
} }
} }
impl SessionRuleLike for WeekdayOfMonth { impl SessionRuleLike for WeekdayOfMonth {
fn determine_next_date(&self, current_date: Day) -> Option<Day> { fn determine_next_date(&self, current_date: Day) -> Option<Day> {
let session_this_month = NaiveDate::from_weekday_of_month_opt( let session_this_month = NaiveDate::from_weekday_of_month_opt(
current_date.year(), current_date.year(),
current_date.month(), current_date.month(),
self.weekday, self.weekday,
self.n, self.n,
)?; )?;
let date = if session_this_month >= current_date.date() { let date = if session_this_month >= current_date.date() {
session_this_month session_this_month
} else { } else {
let session_next_month = current_date.checked_add_months(Months::new(1))?; let session_next_month = current_date.checked_add_months(Months::new(1))?;
NaiveDate::from_weekday_of_month_opt( NaiveDate::from_weekday_of_month_opt(
session_next_month.year(), session_next_month.year(),
session_next_month.month(), session_next_month.month(),
self.weekday, self.weekday,
self.n, self.n,
)? )?
}; };
Some(date.into()) Some(date.into())
} }
fn accepts(&self, day: Day) -> bool { fn accepts(&self, day: Day) -> bool {
day.weekday() == self.weekday && day.weekday_of_month() == self.n day.weekday() == self.weekday && day.weekday_of_month() == self.n
} }
} }
pub trait SessionRuleLike { pub trait SessionRuleLike {
/// Determines the next session date in form of a [Day], possibly including the `current_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<Day>; fn determine_next_date(&self, current_date: Day) -> Option<Day>;
/// Whether this rule would be able to produce the given `day`. /// Whether this rule would be able to produce the given `day`.
fn accepts(&self, day: Day) -> bool; fn accepts(&self, day: Day) -> bool;
} }
#[cfg(test)] #[cfg(test)]
mod test_weekday_of_month { mod test_weekday_of_month {
use crate::session::rule::{SessionRuleLike, WeekdayOfMonth}; use crate::session::rule::{SessionRuleLike, WeekdayOfMonth};
use crate::test_util::day; use crate::test_util::day;
use chrono::Weekday; use chrono::Weekday;
#[test] #[test]
fn test_next_date() { fn test_next_date() {
let rule = WeekdayOfMonth::new(3, Weekday::Tue); let rule = WeekdayOfMonth::new(3, Weekday::Tue);
assert_eq!( assert_eq!(
rule.determine_next_date(day(17, 2, 2025)), rule.determine_next_date(day(17, 2, 2025)),
Some(day(18, 2, 2025)) Some(day(18, 2, 2025))
); );
assert_eq!( assert_eq!(
rule.determine_next_date(day(18, 2, 2025)), rule.determine_next_date(day(18, 2, 2025)),
Some(day(18, 2, 2025)) Some(day(18, 2, 2025))
); );
assert_eq!( assert_eq!(
rule.determine_next_date(day(19, 2, 2025)), rule.determine_next_date(day(19, 2, 2025)),
Some(day(18, 3, 2025)) Some(day(18, 3, 2025))
); );
} }
} }

View File

@ -2,9 +2,9 @@ use crate::day::Day;
use chrono::NaiveDate; use chrono::NaiveDate;
pub fn date(day: u32, month: u32, year: i32) -> 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 { pub fn day(day: u32, month: u32, year: i32) -> Day {
date(day, month, year).into() date(day, month, year).into()
} }