From 1ce55675db30c5e4a324c04eff5b3a0f6ea841b8 Mon Sep 17 00:00:00 2001 From: stefiosif Date: Wed, 14 Jan 2026 21:15:57 +0200 Subject: [PATCH] Move valid_residents to the scheduler impl and simplify it --- src-tauri/src/config.rs | 90 +------------------------------------- src-tauri/src/schedule.rs | 46 +++++++++---------- src-tauri/src/scheduler.rs | 33 +++++++++++--- src-tauri/src/slot.rs | 12 +++++ 4 files changed, 63 insertions(+), 118 deletions(-) diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index 3c64c50..077f9cd 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -3,8 +3,7 @@ use serde::{Deserialize, Serialize}; use crate::{ resident::{Resident, ResidentDTO}, - schedule::{MonthlySchedule, ShiftType}, - slot::{Day, ShiftPosition, Slot}, + slot::Day }; const YEAR: i32 = 2026; @@ -80,54 +79,6 @@ impl UserConfig { || self.holidays.contains(&(day.0 as usize)) } - /// Return all possible candidates for the next slot - /// TODO: move this to another file, UserConfig should only hold the info set from GUI - /// - /// @slot - /// @schedule - pub fn candidates( - &self, - slot: Slot, - schedule: &MonthlySchedule, - ) -> Vec<(&Resident, &ShiftType)> { - let mut candidates = vec![]; - let is_open = slot.is_open_shift(); - - let other_position = match slot.position { - ShiftPosition::First => ShiftPosition::Second, - ShiftPosition::Second => ShiftPosition::First, - }; - let other_slot = Slot::new(slot.day, other_position); - let already_on_duty = schedule.get_resident_id(&other_slot); - - for resident in &self.residents { - if let Some(on_duty_id) = &already_on_duty { - if &&resident.id == on_duty_id { - continue; - } - } - - if resident.negative_shifts.contains(&slot.day) { - continue; - } - - for shift_type in &resident.allowed_types { - match (shift_type, is_open, slot.position) { - (ShiftType::OpenFirst, true, ShiftPosition::First) => { - candidates.push((resident, shift_type)) - } - (ShiftType::OpenSecond, true, ShiftPosition::Second) => { - candidates.push((resident, shift_type)) - } - (ShiftType::Closed, false, _) => candidates.push((resident, shift_type)), - _ => continue, - } - } - } - - candidates - } - pub fn default() -> Self { Self { month: Month::try_from(2).unwrap(), @@ -155,12 +106,7 @@ impl From for UserConfig { mod tests { use rstest::{fixture, rstest}; - use crate::{ - config::UserConfig, - resident::Resident, - schedule::{MonthlySchedule, ShiftType}, - slot::{Day, ShiftPosition, Slot}, - }; + use crate::{config::UserConfig, resident::Resident, schedule::MonthlySchedule}; #[fixture] fn setup() -> (UserConfig, MonthlySchedule) { @@ -175,38 +121,6 @@ mod tests { (config, schedule) } - #[rstest] - fn test_candidates_prevents_double_booking_on_open_day(setup: (UserConfig, MonthlySchedule)) { - let (config, mut schedule) = setup; - - let slot_1 = Slot::new(Day(1), ShiftPosition::First); - let slot_2 = Slot::new(Day(1), ShiftPosition::Second); - - let stefanos = &config.residents[0]; - let iordanis = &config.residents[1]; - - schedule.insert(slot_1, stefanos); - - let candidates = config.candidates(slot_2, &schedule); - - let stefanos_is_candidate = candidates.iter().any(|(r, _)| r.id == stefanos.id); - assert!(!stefanos_is_candidate); - - let iordanis_is_candidate = candidates.iter().any(|(r, _)| r.id == iordanis.id); - assert!(iordanis_is_candidate); - } - - #[rstest] - fn test_candidates_respects_shift_type_position(setup: (UserConfig, MonthlySchedule)) { - let (config, schedule) = setup; - let slot_1 = Slot::new(Day(1), ShiftPosition::First); - let candidates = config.candidates(slot_1, &schedule); - - for (_, shift_type) in candidates { - assert_eq!(shift_type, &ShiftType::OpenFirst); - } - } - #[rstest] fn test_total_holiday_slots() { let config = UserConfig::default().with_holidays(vec![2, 3, 4]); diff --git a/src-tauri/src/schedule.rs b/src-tauri/src/schedule.rs index ccd5961..37db799 100644 --- a/src-tauri/src/schedule.rs +++ b/src-tauri/src/schedule.rs @@ -23,7 +23,7 @@ impl MonthlySchedule { pub fn prefill(&mut self, config: &UserConfig) { for r in &config.residents { for s in &r.manual_shifts { - self.insert(*s, r); + self.insert(*s, &r.id); } } } @@ -32,10 +32,10 @@ impl MonthlySchedule { self.0.get(slot) } - pub fn current_workload(&self, resident: &Resident) -> usize { + pub fn current_workload(&self, resident_id: &str) -> usize { self.0 .values() - .filter(|res_id| res_id == &&resident.id) + .filter(|res_id| res_id == &resident_id) .count() } @@ -92,8 +92,8 @@ impl MonthlySchedule { true } - pub fn insert(&mut self, slot: Slot, resident: &Resident) { - self.0.insert(slot, resident.id.clone()); + pub fn insert(&mut self, slot: Slot, resident_id: &str) { + self.0.insert(slot, resident_id.to_string()); } pub fn remove(&mut self, slot: Slot) { @@ -177,7 +177,7 @@ impl MonthlySchedule { }; if let Some(resident) = config.residents.iter().find(|r| &r.id == res_id) { - let current_workload = self.current_workload(resident); + let current_workload = self.current_workload(&resident.id); if let Some(&limit) = bounds.max_workloads.get(res_id) { let mut workload_limit = limit; @@ -293,7 +293,7 @@ impl MonthlySchedule { residents.sort_by_key(|r| &r.name); for res in residents { - let total = self.current_workload(res); + let total = self.current_workload(&res.id); let o1 = self.count_shifts(&res.id, Some(ShiftType::OpenFirst)); let o2 = self.count_shifts(&res.id, Some(ShiftType::OpenSecond)); let cl = self.count_shifts(&res.id, Some(ShiftType::Closed)); @@ -380,10 +380,10 @@ mod tests { let slot_1 = Slot::new(Day(1), ShiftPosition::First); let slot_2 = Slot::new(Day(1), ShiftPosition::Second); - schedule.insert(slot_1, &resident); + schedule.insert(slot_1, &resident.id); assert_eq!(schedule.get_resident_id(&slot_1), Some(&"1".to_string())); - assert_eq!(schedule.current_workload(&resident), 1); + assert_eq!(schedule.current_workload(&resident.id), 1); assert_eq!(schedule.get_resident_id(&slot_2), None); } @@ -391,12 +391,12 @@ mod tests { fn test_remove_resident(mut schedule: MonthlySchedule, resident: Resident) { let slot_1 = Slot::new(Day(1), ShiftPosition::First); - schedule.insert(slot_1, &resident); - assert_eq!(schedule.current_workload(&resident), 1); + schedule.insert(slot_1, &resident.id); + assert_eq!(schedule.current_workload(&resident.id), 1); schedule.remove(slot_1); assert_eq!(schedule.get_resident_id(&slot_1), None); - assert_eq!(schedule.current_workload(&resident), 0); + assert_eq!(schedule.current_workload(&resident.id), 0); } #[rstest] @@ -405,9 +405,9 @@ mod tests { let slot_2 = Slot::new(Day(1), ShiftPosition::Second); let slot_3 = Slot::new(Day(2), ShiftPosition::First); - schedule.insert(slot_1, &resident); - schedule.insert(slot_2, &resident); - schedule.insert(slot_3, &resident); + schedule.insert(slot_1, &resident.id); + schedule.insert(slot_2, &resident.id); + schedule.insert(slot_3, &resident.id); assert!(!schedule.same_resident_in_consecutive_days(&slot_1)); assert!(!schedule.same_resident_in_consecutive_days(&slot_2)); @@ -422,8 +422,8 @@ mod tests { let stefanos = &toxic_config.residents[0]; let iordanis = &toxic_config.residents[1]; - schedule.insert(slot_1, stefanos); - schedule.insert(slot_2, iordanis); + schedule.insert(slot_1, &stefanos.id); + schedule.insert(slot_2, &iordanis.id); assert!(schedule.has_toxic_pair(&slot_2, &toxic_config)) } @@ -442,13 +442,13 @@ mod tests { bounds.max_workloads.insert("1".to_string(), 1); bounds.max_workloads.insert("2".to_string(), 2); - schedule.insert(slot_1, &stefanos); + schedule.insert(slot_1, &stefanos.id); assert!(!schedule.is_workload_unbalanced(&slot_1, &config, &bounds)); - schedule.insert(slot_2, &iordanis); + schedule.insert(slot_2, &iordanis.id); assert!(!schedule.is_workload_unbalanced(&slot_2, &config, &bounds)); - schedule.insert(slot_3, &stefanos); + schedule.insert(slot_3, &stefanos.id); assert!(schedule.is_workload_unbalanced(&slot_3, &config, &bounds)); } @@ -466,13 +466,13 @@ mod tests { bounds.max_holiday_shifts.insert("1".to_string(), 1); bounds.max_holiday_shifts.insert("2".to_string(), 1); - schedule.insert(slot_1, &stefanos); + schedule.insert(slot_1, &stefanos.id); assert!(!schedule.is_holiday_workload_imbalanced(&slot_1, &config, &bounds)); - schedule.insert(slot_2, &iordanis); + schedule.insert(slot_2, &iordanis.id); assert!(!schedule.is_holiday_workload_imbalanced(&slot_2, &config, &bounds)); - schedule.insert(slot_7, &stefanos); + schedule.insert(slot_7, &stefanos.id); assert!(schedule.is_holiday_workload_imbalanced(&slot_7, &config, &bounds)); } } diff --git a/src-tauri/src/scheduler.rs b/src-tauri/src/scheduler.rs index 8159e38..7c0e00e 100644 --- a/src-tauri/src/scheduler.rs +++ b/src-tauri/src/scheduler.rs @@ -1,4 +1,7 @@ -use crate::{bounds::WorkloadBounds, config::UserConfig, schedule::MonthlySchedule, slot::Slot}; +use crate::{ + bounds::WorkloadBounds, config::UserConfig, schedule::MonthlySchedule, + slot::Slot, +}; use rand::Rng; @@ -41,15 +44,15 @@ impl Scheduler { } // 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 mut valid_resident_ids = self.valid_residents(slot, schedule); + valid_resident_ids.sort_unstable_by_key(|res_id| { + let workload = schedule.current_workload(res_id); let tie_breaker: f64 = rand::rng().random(); (workload, (tie_breaker * 1000.0) as usize) }); - for (resident, _) in candidates { - schedule.insert(slot, resident); + for id in &valid_resident_ids { + schedule.insert(slot, id); if self.search(schedule, slot.next()) { log::trace!("Solution found, exiting recursive algorithm"); @@ -61,6 +64,19 @@ impl Scheduler { false } + + /// Return all valid residents for the current slot + pub fn valid_residents(&self, slot: Slot, schedule: &MonthlySchedule) -> Vec { + let other_slot_resident_id = schedule.get_resident_id(&slot.other_position()); + + self.config + .residents + .iter() + .filter(|r| Some(&r.id) != other_slot_resident_id) + .filter(|r| !r.negative_shifts.contains(&slot.day)) + .map(|r| r.id.clone()) + .collect() + } } #[cfg(test)] @@ -121,11 +137,14 @@ mod tests { } for r in &scheduler.config.residents { - let workload = schedule.current_workload(r); + let workload = schedule.current_workload(&r.id); let limit = *scheduler.bounds.max_workloads.get(&r.id).unwrap(); assert!(workload <= limit as usize); } println!("{}", schedule.pretty_print(&scheduler.config)); } + + #[rstest] + fn test_valid_residents(mut schedule: MonthlySchedule, scheduler: Scheduler) {} } diff --git a/src-tauri/src/slot.rs b/src-tauri/src/slot.rs index 20d1495..793a364 100644 --- a/src-tauri/src/slot.rs +++ b/src-tauri/src/slot.rs @@ -77,6 +77,18 @@ impl Slot { self.day.greater_than(&Day(limit)) } + pub fn other_position(&self) -> Self { + let other_pos = match self.position { + ShiftPosition::First => ShiftPosition::Second, + ShiftPosition::Second => ShiftPosition::First, + }; + + Self { + day: self.day, + position: other_pos, + } + } + pub fn default() -> Self { Self { day: Day(1),