From e11e1376eb461618ae2daf0fdb4a12a5f78fd3cb Mon Sep 17 00:00:00 2001 From: stefiosif Date: Sun, 28 Dec 2025 21:32:08 +0200 Subject: [PATCH] Sync progress --- src-tauri/src/generator.rs | 76 +++--- src-tauri/src/lib.rs | 31 ++- src-tauri/src/model.rs | 235 ++++++++++++++++-- src/routes/+page.svelte | 97 ++++++-- .../components/configurations/advanced.svelte | 23 +- .../components/configurations/basic.svelte | 151 ++++++----- src/routes/state.svelte.ts | 14 +- 7 files changed, 459 insertions(+), 168 deletions(-) diff --git a/src-tauri/src/generator.rs b/src-tauri/src/generator.rs index 8921da5..f200c52 100644 --- a/src-tauri/src/generator.rs +++ b/src-tauri/src/generator.rs @@ -2,45 +2,65 @@ //! //! here lies the schedule generator which uses a simple backtracking algorithm -use crate::model::{MonthlySchedule, Resident}; +use crate::model::{Configurations, Day, MonthlySchedule, Slot}; -// schedule param contains the schedule with maybe some manually input days -// returns the complete schedule after applying all rules/restrictions/fairness -fn generate(schedule: &MonthlySchedule) -> MonthlySchedule { - todo!() -} - -// make part of Schedule -// examines Schedule for validity, if no then backtrack -fn is_state_valid() { - todo!() -} - -// From https://en.wikipedia.org/wiki/Backtracking -// Recursively fill a partially filled MonthlySchedule until shifts are set for all days of the month -// Collection of Resident is immutable and only there for reference -// TODO: we can create a struct `Generator` that contains the collection of Resident upon initialization and then call self.people inside self.backtracking -// Returns error if there is no solution for the specific set of constaints -fn backtracking(schedule: &mut MonthlySchedule, people: &Vec, mut day: usize) { - if schedule.restrictions_violated() { - log::info!("restrictions_violated due to..."); - return; +/// https://en.wikipedia.org/wiki/Backtracking +/// https://en.wikipedia.org/wiki/Nurse_scheduling_problem +/// +/// DFS where maximum depth is calculated by total_days_of_month + odd_days_of_month (open shifts) +/// Starts with schedule partially completed from the user interface +/// Ends with a full schedule following restrictions and fairness +pub fn backtracking(schedule: &mut MonthlySchedule, slot: Slot, config: &Configurations) -> bool { + // check if schedule was fully filled in the previous iteration + if slot.out_of_range(config.total_days()) { + log::info!("Solution found, exiting recursive algorithm"); + return true; } - if schedule.is_filled() { - log::info!("..?"); - return; + // TODO:check if slot is already assigned with manual shifts + + // check if any of the rules is violated before continuing + if schedule.restrictions_violated(&slot) { + // log::info!("Cutting branch due to restriction violation"); + return false; } - for resident in people {} + // get all candidate options for current step/slot/depth + let candidates = config.candidates(slot, schedule); - todo!() + // TODO: sort the candidates by workload left in the month helping the algorithm + + for (resident, shift_type) in candidates { + schedule.insert(slot, resident, shift_type); + + if backtracking(schedule, slot.increment(), config) { + log::info!("Solution found..."); + return true; + } + + schedule.remove(slot, resident, shift_type); + } + + false } #[cfg(test)] mod tests { use rstest::rstest; + use crate::{ + generator::backtracking, + model::{Configurations, MonthlySchedule, Slot}, + }; + #[rstest] - pub fn xxxt() {} + fn test_backtracking() { + const JAN: usize = 0; + let config = Configurations::new(JAN); + let mut schedule = MonthlySchedule::new(); + + backtracking(&mut schedule, Slot::default(), &config); + + // assert!(schedule.is_filled(config.total_days())) + } } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 65d91fd..2ea5590 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,6 +1,9 @@ +use log::info; + use crate::{ export::{Export, FileType}, - model::{MonthlySchedule, Resident}, + generator::backtracking, + model::{Configurations, Day, MonthlySchedule, Resident, Slot}, }; mod export; @@ -24,9 +27,31 @@ fn add_resident(resident: Resident) -> String { format!("{:?}", resident) } +/// argument to this must be the rota state including all +/// information for the residents, the forbidden pairs the holidays +/// and the period of the schedule #[tauri::command] fn generate() -> String { - "Generating new rota".to_string() + const JAN: usize = 0; + const DAYS_IN_JAN: usize = 31; + let config = Configurations::new(JAN); + let mut schedule = MonthlySchedule::new(); + + // find total number of slots MEANING + + //TODO: return a result instead of () + backtracking(&mut schedule, Slot::default(), &config); + + info!( + "Generated rota for month {}, final schedule {:?}", + JAN, schedule + ); + + //TODO: this should in fact return a JSON of the schedule instance solution + format!( + "Generated rota for month {}, final schedule {:?}", + JAN, schedule + ) } // takes the list of active residents and their configurations @@ -42,7 +67,7 @@ fn possible_swap_locations(people: Vec) -> Vec { fn export() -> String { // param must have filetype as string from svelte // somehow get the current schedule - let rota = MonthlySchedule::new(10); + let rota = MonthlySchedule::new(); let _ = rota.export(FileType::Json); // locally store the _? diff --git a/src-tauri/src/model.rs b/src-tauri/src/model.rs index fd129b4..6e6e601 100644 --- a/src-tauri/src/model.rs +++ b/src-tauri/src/model.rs @@ -2,28 +2,51 @@ use std::collections::HashMap; use chrono::Month; use itertools::Itertools; +use log::warn; use serde::{Deserialize, Serialize}; -// We're always talking about entities that are created for the single month of the rota generation +// here add all info that we will need when we unmake the move after recursing... +pub struct CalendarState { + // here make unmake stuff +} + +pub struct Restriction {} + +pub struct Rule {} const YEAR: i32 = 2026; pub struct Configurations { month: Month, + holidays: Vec, + residents: Vec, } impl Configurations { pub fn new(month: usize) -> Self { Self { month: Month::try_from(month as u8).unwrap(), + holidays: vec![], + residents: vec![], } } + pub fn default() -> Self { + Self { + month: Month::try_from(0).unwrap(), + holidays: vec![], + residents: vec![], + } + } + + pub fn add(&mut self, resident: Resident) { + self.residents.push(resident); + } + pub fn total_days(&self) -> u8 { self.month.num_days(YEAR).unwrap() } - // open shfits -> 2 people per night pub fn total_open_shifts(&self) -> u8 { let mut total_open_shifts = 0; for i in 1..=self.total_days() { @@ -34,31 +57,170 @@ impl Configurations { total_open_shifts } - // closed shifts -> 1 resident per night pub fn total_closed_shifts(&self) -> u8 { self.total_days() - self.total_open_shifts() } + + /// Return all possible candidates for the next slot + pub fn candidates( + &self, + slot: Slot, + schedule: &MonthlySchedule, + ) -> Vec<(&Resident, &ShiftType)> { + let mut candidates = vec![]; + let is_open = slot.is_open_shift(); + + for resident in &self.residents { + if resident.negative_shifts.contains(&slot.day) { + continue; + } + + if schedule.is_already_assigned_in_slot(&slot) { + continue; + } + + for shift_type in &resident.allowed_types { + match shift_type { + ShiftType::OpenFirst => { + if is_open { + candidates.push((resident, &ShiftType::OpenFirst)) + } + } + ShiftType::OpenSecond => { + if is_open { + candidates.push((resident, &ShiftType::OpenSecond)) + } + } + ShiftType::Closed => { + if !is_open { + candidates.push((resident, &ShiftType::Closed)) + } + } + } + } + } + + candidates + } } -pub struct MonthlySchedule { - rotation: Vec, +#[derive(Serialize, Deserialize, PartialEq, PartialOrd, Ord, Eq, Debug, Hash, Clone, Copy)] +#[serde(rename_all = "camelCase")] +pub struct Slot { + day: Day, + index: usize, } +impl Slot { + pub fn new(day: Day, index: usize) -> Self { + Self { day, index } + } + + pub fn is_open_shift(&self) -> bool { + self.day.is_open_shift() + } + + pub fn increment(&self) -> Self { + if self.index == 0 { + Self { + day: self.day, + index: 1, + } + } else { + Self { + day: self.day.increment(), + index: 0, + } + } + } + + pub fn out_of_range(&self, limit: u8) -> bool { + self.day.0 > limit + } + + pub fn default() -> Self { + Self { + day: Day(0), + index: 0, + } + } +} + +#[derive(Debug)] +pub struct MonthlySchedule(HashMap); + impl MonthlySchedule { - pub fn new(days_of_month: usize) -> Self { - Self { rotation: vec![] } + pub fn new() -> Self { + Self(HashMap::new()) } - pub fn restrictions_violated(&self) -> bool { - todo!() - } - - pub fn is_filled(&self) -> bool { + pub fn get_resident(&self, slot: &Slot) -> Option { + let shift = self.0.get(slot); + + if let Some(shift) = shift { + if slot.index == 0 { + return shift.resident_1_id.clone(); + } else { + return shift.resident_1_id.clone(); + } + } + None + } + + pub fn insert(&mut self, slot: Slot, resident: &Resident, shift_type: &ShiftType) { + match self.0.get_mut(&slot) { + Some(shift) => { + warn!("one shift already set for today"); + + if shift.resident_2_id.is_none() && slot.is_open_shift() { + shift.resident_2_id = Some(resident.id.clone()) + } + } + None => { + let new_shift = Shift::new(resident.id.clone()); + self.0.insert(slot, new_shift); + } + } + } + + pub fn remove(&mut self, slot: Slot, resident: &Resident, shift_type: &ShiftType) { + if let Some(shift) = self.0.get_mut(&slot) { + if let Some(res_1_id) = &shift.resident_1_id { + if &resident.id == res_1_id { + shift.resident_1_id = None; + } + } else if let Some(res_1_id) = &shift.resident_2_id { + if &resident.id == res_1_id { + shift.resident_1_id = None; + } + } + } + } + + pub fn is_already_assigned_in_slot(&self, slot: &Slot) -> bool { + if let Some(shift) = self.0.get(slot) { + if shift.resident_1_id.is_some() || shift.resident_2_id.is_some() { + return true; + } + } + false + } + + /// here we check for the validity of all environment restrictions + /// if any is violated we return true (leading to pruning in the backtracking algorithm) + pub fn restrictions_violated(&self, slot: &Slot) -> bool { + // self.contains_any_consecutive_shifts() + + // see if the spot in the self calendar for the slot has the same person like the previous? + // this would take out both back to back days, but also same day 2 times the same resident + let resident = self.get_resident(slot); + todo!() } + /// return true if pub fn contains_any_consecutive_shifts(&self) -> bool { - // self.rotation + // self.0 // .into_iter() // .sorted_unstable_by_key(|shift| shift.day) // .tuple_windows() @@ -83,18 +245,20 @@ impl MonthlySchedule { // }); todo!() } - - // pub fn } -#[derive(Serialize, Deserialize, PartialEq, PartialOrd, Ord, Eq, Debug)] +#[derive(Serialize, Deserialize, PartialEq, PartialOrd, Ord, Eq, Debug, Hash, Clone, Copy)] #[serde(rename_all = "camelCase")] -pub struct Day(usize); +pub struct Day(pub u8); impl Day { pub fn is_open_shift(&self) -> bool { - self.0.is_multiple_of(2) + !self.0.is_multiple_of(2) + } + + pub fn increment(&self) -> Self { + Self(self.0 + 1) } } @@ -137,13 +301,40 @@ impl WeeklySchedule { // todo } +// this is agnostic to what specific types the resident shave +// we cant add here shift_Type except if we just do OPEN/CLOSED. +// also there's a discussion to be had regardng the Option of resident 1 id since a shift should always have one.. +// (But due to the backtracking algorithm maybe its ok to work without any residents in a shift? cauise we add/remove all the time) +#[derive(Debug)] pub struct Shift { - day: Day, - resident_a: Resident, - resident_b: Option, + resident_1_id: Option, + // should be filled in odd days, in other words in open shifts (OpenFirst, OpenSecond) + resident_2_id: Option, } -#[derive(Serialize, Deserialize, Debug)] +impl Shift { + pub fn new_empty() -> Self { + Self { + resident_1_id: None, + resident_2_id: None, + } + } + pub fn new(resident_1_id: String) -> Self { + Self { + resident_1_id: Some(resident_1_id), + resident_2_id: None, + } + } + + pub fn new_open(resident_1_id: String, resident_2_id: String) -> Self { + Self { + resident_1_id: Some(resident_1_id), + resident_2_id: Some(resident_2_id), + } + } +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] pub enum ShiftType { Closed, OpenFirst, diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index af112a5..a6e4606 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -26,33 +26,84 @@ }); -
-