diff --git a/src-tauri/src/export.rs b/src-tauri/src/export.rs index c41e833..df25afc 100644 --- a/src-tauri/src/export.rs +++ b/src-tauri/src/export.rs @@ -164,8 +164,8 @@ mod tests { use rstest::{fixture, rstest}; use crate::{ - bounds::WorkloadBounds, config::UserConfig, generator::backtracking, resident::Resident, - schedule::MonthlySchedule, slot::Slot, + bounds::WorkloadBounds, config::UserConfig, resident::Resident, schedule::MonthlySchedule, + scheduler::Scheduler, slot::Slot, }; #[fixture] @@ -190,13 +190,14 @@ mod tests { WorkloadBounds::new_with_config(&config) } + #[fixture] + fn scheduler(config: UserConfig, bounds: WorkloadBounds) -> Scheduler { + Scheduler::new(config, bounds) + } + #[rstest] - pub fn test_export_as_doc( - mut schedule: MonthlySchedule, - config: UserConfig, - bounds: WorkloadBounds, - ) { - backtracking(&mut schedule, Slot::default(), &config, &bounds); - schedule.export_as_doc(&config); + pub fn test_export_as_doc(mut schedule: MonthlySchedule, scheduler: Scheduler) { + scheduler.search(&mut schedule, Slot::default()); + schedule.export_as_doc(&scheduler.config); } } diff --git a/src-tauri/src/generator.rs b/src-tauri/src/generator.rs deleted file mode 100644 index a87a868..0000000 --- a/src-tauri/src/generator.rs +++ /dev/null @@ -1,123 +0,0 @@ -use rand::Rng; - -use crate::{bounds::WorkloadBounds, config::UserConfig, schedule::MonthlySchedule, slot::Slot}; - -/// DFS where maximum depth is calculated by total_days_of_month + odd_days_of_month each node is called a slot -/// 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: &UserConfig, - bounds: &WorkloadBounds, -) -> bool { - if !slot.is_first() && schedule.restrictions_violated(&slot.previous(), config, bounds) { - log::trace!("Cutting branch due to restriction violation"); - return false; - } - - if slot.greater_than(config.total_days()) { - if !schedule.is_per_shift_threshold_met(config, bounds) { - return false; - } - - log::trace!("Solution found, exiting recursive algorithm"); - return true; - } - - if schedule.is_slot_manually_assigned(&slot) { - return backtracking(schedule, slot.next(), config, bounds); - } - - // sort candidates by current workload, add rng for tie breakers - let mut candidates = config.candidates(slot, schedule); - candidates.sort_unstable_by_key(|&(resident, _)| { - let workload = schedule.current_workload(resident); - let tie_breaker: f64 = rand::rng().random(); - (workload, (tie_breaker * 1000.0) as usize) - }); - - for (resident, _) in candidates { - schedule.insert(slot, resident); - - if backtracking(schedule, slot.next(), config, bounds) { - log::trace!("Solution found, exiting recursive algorithm"); - return true; - } - - schedule.remove(slot); - } - - false -} - -#[cfg(test)] -mod tests { - use rstest::{fixture, rstest}; - - use crate::{ - bounds::WorkloadBounds, - config::UserConfig, - generator::backtracking, - resident::Resident, - schedule::MonthlySchedule, - slot::{Day, ShiftPosition, Slot}, - }; - - #[fixture] - fn schedule() -> MonthlySchedule { - MonthlySchedule::new() - } - - #[fixture] - fn config() -> UserConfig { - UserConfig::default().with_residents(vec![ - Resident::new("1", "Stefanos"), - Resident::new("2", "Iordanis"), - Resident::new("3", "Maria"), - Resident::new("4", "Veatriki"), - Resident::new("5", "Takis"), - Resident::new("6", "Akis"), - ]) - } - - #[fixture] - fn bounds(config: UserConfig) -> WorkloadBounds { - WorkloadBounds::new_with_config(&config) - } - - #[rstest] - fn test_backtracking( - mut schedule: MonthlySchedule, - config: UserConfig, - bounds: WorkloadBounds, - ) { - assert!(backtracking( - &mut schedule, - Slot::default(), - &config, - &bounds - )); - - for d in 1..=config.total_days() { - let day = Day(d); - if day.is_open_shift() { - let slot_first = Slot::new(day, ShiftPosition::First); - assert!(schedule.get_resident_id(&slot_first).is_some()); - let slot_second = Slot::new(day, ShiftPosition::Second); - assert!(schedule.get_resident_id(&slot_second).is_some()); - } else { - let slot_first = Slot::new(day, ShiftPosition::First); - assert!(schedule.get_resident_id(&slot_first).is_some()); - } - } - - for r in &config.residents { - let workload = schedule.current_workload(r); - let limit = *bounds.max_workloads.get(&r.id).unwrap(); - assert!(workload <= limit as usize); - } - - println!("{}", schedule.pretty_print(&config)); - } -} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index afb87fb..2c61722 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,22 +1,19 @@ use std::sync::Mutex; -use log::info; - use crate::{ bounds::WorkloadBounds, config::{UserConfig, UserConfigDTO}, export::{Export, FileType}, - generator::backtracking, schedule::MonthlySchedule, - slot::Slot, + scheduler::Scheduler, }; mod bounds; mod config; mod export; -mod generator; mod resident; mod schedule; +mod scheduler; mod slot; struct AppState { @@ -28,18 +25,16 @@ struct AppState { /// and the period of the schedule #[tauri::command] fn generate(config: UserConfigDTO, state: tauri::State<'_, AppState>) -> MonthlySchedule { - info!("{:?}", config); let config = UserConfig::from(config); - let bounds = WorkloadBounds::new_with_config(&config); - info!("{:?}", config); let mut schedule = MonthlySchedule::new(); schedule.prefill(&config); - info!("{}", schedule.pretty_print(&config)); + + let bounds = WorkloadBounds::new_with_config(&config); + let scheduler = Scheduler::new(config, bounds); + scheduler.run(&mut schedule); - backtracking(&mut schedule, Slot::default(), &config, &bounds); let mut internal_schedule = state.schedule.lock().unwrap(); *internal_schedule = schedule.clone(); - info!("{}", schedule.pretty_print(&config)); schedule } diff --git a/src-tauri/src/scheduler.rs b/src-tauri/src/scheduler.rs new file mode 100644 index 0000000..8159e38 --- /dev/null +++ b/src-tauri/src/scheduler.rs @@ -0,0 +1,131 @@ +use crate::{bounds::WorkloadBounds, config::UserConfig, schedule::MonthlySchedule, slot::Slot}; + +use rand::Rng; + +pub struct Scheduler { + pub config: UserConfig, + pub bounds: WorkloadBounds, +} + +impl Scheduler { + pub fn new(config: UserConfig, bounds: WorkloadBounds) -> Self { + Self { config, bounds } + } + + pub fn run(&self, schedule: &mut MonthlySchedule) -> bool { + self.search(schedule, Slot::default()) + } + + /// DFS where maximum depth is calculated by total_days_of_month + odd_days_of_month each node is called a slot + /// Starts with schedule partially completed from the user interface + /// Ends with a full schedule following restrictions and fairness + pub fn search(&self, schedule: &mut MonthlySchedule, slot: Slot) -> bool { + if !slot.is_first() + && schedule.restrictions_violated(&slot.previous(), &self.config, &self.bounds) + { + log::trace!("Cutting branch due to restriction violation"); + return false; + } + + if slot.greater_than(self.config.total_days()) { + if !schedule.is_per_shift_threshold_met(&self.config, &self.bounds) { + return false; + } + + log::trace!("Solution found, exiting recursive algorithm"); + return true; + } + + if schedule.is_slot_manually_assigned(&slot) { + return self.search(schedule, slot.next()); + } + + // sort candidates by current workload, add rng for tie breakers + let mut candidates = self.config.candidates(slot, schedule); + candidates.sort_unstable_by_key(|&(resident, _)| { + let workload = schedule.current_workload(resident); + let tie_breaker: f64 = rand::rng().random(); + (workload, (tie_breaker * 1000.0) as usize) + }); + + for (resident, _) in candidates { + schedule.insert(slot, resident); + + if self.search(schedule, slot.next()) { + log::trace!("Solution found, exiting recursive algorithm"); + return true; + } + + schedule.remove(slot); + } + + false + } +} + +#[cfg(test)] +mod tests { + use rstest::{fixture, rstest}; + + use crate::{ + bounds::WorkloadBounds, + config::UserConfig, + resident::Resident, + schedule::MonthlySchedule, + scheduler::Scheduler, + slot::{Day, ShiftPosition, Slot}, + }; + + #[fixture] + fn schedule() -> MonthlySchedule { + MonthlySchedule::new() + } + + #[fixture] + fn config() -> UserConfig { + UserConfig::default().with_residents(vec![ + Resident::new("1", "Stefanos"), + Resident::new("2", "Iordanis"), + Resident::new("3", "Maria"), + Resident::new("4", "Veatriki"), + Resident::new("5", "Takis"), + Resident::new("6", "Akis"), + ]) + } + + #[fixture] + fn bounds(config: UserConfig) -> WorkloadBounds { + WorkloadBounds::new_with_config(&config) + } + + #[fixture] + fn scheduler(config: UserConfig, bounds: WorkloadBounds) -> Scheduler { + Scheduler::new(config, bounds) + } + + #[rstest] + fn test_search(mut schedule: MonthlySchedule, scheduler: Scheduler) { + assert!(scheduler.search(&mut schedule, Slot::default())); + + for d in 1..=scheduler.config.total_days() { + let day = Day(d); + if day.is_open_shift() { + let slot_first = Slot::new(day, ShiftPosition::First); + assert!(schedule.get_resident_id(&slot_first).is_some()); + let slot_second = Slot::new(day, ShiftPosition::Second); + assert!(schedule.get_resident_id(&slot_second).is_some()); + } else { + let slot_first = Slot::new(day, ShiftPosition::First); + assert!(schedule.get_resident_id(&slot_first).is_some()); + } + } + + for r in &scheduler.config.residents { + let workload = schedule.current_workload(r); + let limit = *scheduler.bounds.max_workloads.get(&r.id).unwrap(); + assert!(workload <= limit as usize); + } + + println!("{}", schedule.pretty_print(&scheduler.config)); + } +}