From d7d4b109c3d6856963b077ecd1b7ce25d73f55ab Mon Sep 17 00:00:00 2001 From: stefiosif Date: Wed, 14 Jan 2026 00:07:20 +0200 Subject: [PATCH] Separate user configs from resident workload bounds --- src-tauri/src/bounds.rs | 232 +++++++++++++++++++++++++++++++++++ src-tauri/src/config.rs | 244 ------------------------------------- src-tauri/src/export.rs | 25 ++-- src-tauri/src/generator.rs | 46 ++++--- src-tauri/src/lib.rs | 13 +- src-tauri/src/resident.rs | 2 +- src-tauri/src/schedule.rs | 72 +++++++---- 7 files changed, 331 insertions(+), 303 deletions(-) create mode 100644 src-tauri/src/bounds.rs diff --git a/src-tauri/src/bounds.rs b/src-tauri/src/bounds.rs new file mode 100644 index 0000000..38de34d --- /dev/null +++ b/src-tauri/src/bounds.rs @@ -0,0 +1,232 @@ +use std::collections::HashMap; + +use crate::{config::UserConfig, resident::Resident, schedule::ShiftType, slot::Day}; + +pub struct WorkloadBounds { + pub max_workloads: HashMap, + pub max_holiday_shifts: HashMap, + pub max_by_shift_type: HashMap<(String, ShiftType), u8>, + pub min_by_shift_type: HashMap<(String, ShiftType), u8>, +} + +impl WorkloadBounds { + pub fn new() -> Self { + Self { + max_workloads: HashMap::new(), + max_holiday_shifts: HashMap::new(), + max_by_shift_type: HashMap::new(), + min_by_shift_type: HashMap::new(), + } + } + + pub fn new_with_config(config: &UserConfig) -> Self { + let mut bounds = Self::new(); + bounds.calculate_max_workloads(config); + bounds.calculate_max_holiday_shifts(config); + bounds.calculate_max_by_shift_type(config); + bounds + } + + /// get map with total amount of slots in a month for each type of shift + pub fn get_initial_supply(&self, config: &UserConfig) -> HashMap { + let mut supply = HashMap::new(); + let total_days = config.total_days(); + + for d in 1..=total_days { + if Day(d).is_open_shift() { + *supply.entry(ShiftType::OpenFirst).or_insert(0) += 1; + *supply.entry(ShiftType::OpenSecond).or_insert(0) += 1; + } else { + *supply.entry(ShiftType::Closed).or_insert(0) += 1; + } + } + supply + } + + /// this is called after the user config params have been initialized, can be done with the builder (lite) pattern + /// initialize a hashmap for O(1) search calls for the residents' max workload + pub fn calculate_max_workloads(&mut self, config: &UserConfig) { + let total_slots = config.total_slots(); + + let max_shifts_sum: usize = config + .residents + .iter() + .map(|r| r.max_shifts.unwrap_or(0)) + .sum(); + + let residents_without_max_shifts: Vec<_> = config + .residents + .iter() + .filter(|r| r.max_shifts.is_none()) + .collect(); + + let residents_without_max_shifts_size = residents_without_max_shifts.len(); + + if residents_without_max_shifts_size == 0 { + for r in &config.residents { + self.max_workloads + .insert(r.id.clone(), r.max_shifts.unwrap_or(0) as u8); + } + return; + } + + // Untested scenario: Resident has manual max_shifts and also reduced workload flag + + let total_reduced_loads: usize = residents_without_max_shifts + .iter() + .filter(|r| r.reduced_load) + .count(); + let max_shifts_ceiling = (total_slots - max_shifts_sum as u8 + total_reduced_loads as u8) + .div_ceil(residents_without_max_shifts_size as u8); + + for r in &config.residents { + let max_shifts = if let Some(manual_max_shifts) = r.max_shifts { + manual_max_shifts as u8 + } else if r.reduced_load { + max_shifts_ceiling - 1 + } else { + max_shifts_ceiling + }; + self.max_workloads.insert(r.id.clone(), max_shifts); + } + } + + /// + pub fn calculate_max_holiday_shifts(&mut self, config: &UserConfig) { + let total_slots = config.total_slots(); + let total_holiday_slots = config.total_holiday_slots(); + + for r in &config.residents { + let workload_limit = *self.max_workloads.get(&r.id).unwrap_or(&0); + + let share = (workload_limit as f32 / total_slots as f32) * total_holiday_slots as f32; + let holiday_limit = share.ceil() as u8; + + self.max_holiday_shifts.insert(r.id.clone(), holiday_limit); + } + } + + /// + pub fn calculate_max_by_shift_type(&mut self, config: &UserConfig) { + let mut global_supply = self.get_initial_supply(config); + let mut local_limits = HashMap::new(); + let mut local_thresholds = HashMap::new(); + + let all_shift_types = [ + ShiftType::OpenFirst, + ShiftType::OpenSecond, + ShiftType::Closed, + ]; + + // residents with 1 available shift types + for res in config + .residents + .iter() + .filter(|r| r.allowed_types.len() == 1) + { + let stype = &res.allowed_types[0]; + let total_limit = *self.max_workloads.get(&res.id).unwrap_or(&0); + + local_limits.insert((res.id.clone(), stype.clone()), total_limit); + local_thresholds.insert((res.id.clone(), stype.clone()), total_limit - 1); + + for other_type in &all_shift_types { + if other_type != stype { + local_limits.insert((res.id.clone(), other_type.clone()), 0); + local_thresholds.insert((res.id.clone(), other_type.clone()), 0); + } + } + + if let Some(s) = global_supply.get_mut(stype) { + *s = s.saturating_sub(total_limit) + } + } + + // residents with 2 available shift types + for res in config + .residents + .iter() + .filter(|r| r.allowed_types.len() == 2) + { + let total_limit = *self.max_workloads.get(&res.id).unwrap_or(&0) as f32; + let per_type = (total_limit / 2.0).ceil() as u8; + + for stype in &all_shift_types { + if res.allowed_types.contains(stype) { + local_limits.insert((res.id.clone(), stype.clone()), per_type); + local_thresholds.insert((res.id.clone(), stype.clone()), per_type - 1); + if let Some(s) = global_supply.get_mut(stype) { + *s = s.saturating_sub(per_type) + } + } else { + local_limits.insert((res.id.clone(), stype.clone()), 0); + local_thresholds.insert((res.id.clone(), stype.clone()), 0); + } + } + } + + // residents with 3 available shift types + let generalists: Vec<&Resident> = config + .residents + .iter() + .filter(|r| r.allowed_types.len() == 3) + .collect(); + + if !generalists.is_empty() { + for stype in &all_shift_types { + let remaining = *global_supply.get(stype).unwrap_or(&0); + let fair_slice = (remaining as f32 / generalists.len() as f32) + .ceil() + .max(0.0) as u8; + + for res in &generalists { + local_limits.insert((res.id.clone(), stype.clone()), fair_slice); + local_thresholds.insert((res.id.clone(), stype.clone()), fair_slice - 1); + } + } + } + + self.max_by_shift_type = local_limits; + self.min_by_shift_type = local_thresholds; + } +} + +#[cfg(test)] +mod tests { + use rstest::{fixture, rstest}; + + use crate::{bounds::WorkloadBounds, config::UserConfig, resident::Resident}; + + #[fixture] + fn config() -> UserConfig { + UserConfig::default().with_residents(vec![ + Resident::new("1", "Stefanos").with_max_shifts(2), + Resident::new("2", "Iordanis").with_max_shifts(2), + Resident::new("3", "Maria").with_reduced_load(), + Resident::new("4", "Veatriki"), + Resident::new("5", "Takis"), + ]) + } + + #[rstest] + fn test_max_workloads(config: UserConfig) { + let bounds = WorkloadBounds::new_with_config(&config); + + assert_eq!(bounds.max_workloads["1"], 2); + assert_eq!(bounds.max_workloads["2"], 2); + assert_eq!(bounds.max_workloads["3"], 12); + assert_eq!(bounds.max_workloads["4"], 13); + assert_eq!(bounds.max_workloads["5"], 13); + } + + #[rstest] + fn test_calculate_max_holiday_shifts(config: UserConfig) { + let bounds = WorkloadBounds::new_with_config(&config); + + let stefanos_limit = *bounds.max_holiday_shifts.get("1").unwrap(); + let iordanis_limit = *bounds.max_holiday_shifts.get("2").unwrap(); + + assert_eq!(stefanos_limit, 1); + assert_eq!(iordanis_limit, 1); + } +} diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index e5c4ca1..3c64c50 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -1,5 +1,3 @@ -use std::collections::HashMap; - use chrono::Month; use serde::{Deserialize, Serialize}; @@ -27,12 +25,6 @@ pub struct UserConfig { pub holidays: Vec, pub residents: Vec, pub toxic_pairs: Vec<(String, String)>, - - // calculated from inputs - pub workload_limits: HashMap, - pub holiday_limits: HashMap, - pub shift_type_limits: HashMap<(String, ShiftType), u8>, - pub shift_type_threshold: HashMap<(String, ShiftType), u8>, } impl UserConfig { @@ -43,10 +35,6 @@ impl UserConfig { holidays: vec![], residents: vec![], toxic_pairs: vec![], - workload_limits: HashMap::new(), - holiday_limits: HashMap::new(), - shift_type_limits: HashMap::new(), - shift_type_threshold: HashMap::new(), } } @@ -92,160 +80,6 @@ impl UserConfig { || self.holidays.contains(&(day.0 as usize)) } - /// get map with total amount of slots in a month for each type of shift - pub fn get_initial_supply(&self) -> HashMap { - let mut supply = HashMap::new(); - let total_days = self.total_days(); - - for d in 1..=total_days { - if Day(d).is_open_shift() { - *supply.entry(ShiftType::OpenFirst).or_insert(0) += 1; - *supply.entry(ShiftType::OpenSecond).or_insert(0) += 1; - } else { - *supply.entry(ShiftType::Closed).or_insert(0) += 1; - } - } - supply - } - - /// this is called after the user config params have been initialized, can be done with the builder (lite) pattern - /// initialize a hashmap for O(1) search calls for the residents' max workload - pub fn calculate_workload_limits(&mut self) { - let total_slots = self.total_slots(); - - let max_shifts_sum: usize = self - .residents - .iter() - .map(|r| r.max_shifts.unwrap_or(0)) - .sum(); - - let residents_without_max_shifts: Vec<_> = self - .residents - .iter() - .filter(|r| r.max_shifts.is_none()) - .collect(); - - let residents_without_max_shifts_size = residents_without_max_shifts.len(); - - if residents_without_max_shifts_size == 0 { - for r in &self.residents { - self.workload_limits - .insert(r.id.clone(), r.max_shifts.unwrap_or(0) as u8); - } - return; - } - - // Untested scenario: Resident has manual max_shifts and also reduced workload flag - - let total_reduced_loads: usize = residents_without_max_shifts - .iter() - .filter(|r| r.reduced_load) - .count(); - let max_shifts_ceiling = (total_slots - max_shifts_sum as u8 + total_reduced_loads as u8) - .div_ceil(residents_without_max_shifts_size as u8); - - for r in &self.residents { - let max_shifts = if let Some(manual_max_shifts) = r.max_shifts { - manual_max_shifts as u8 - } else if r.reduced_load { - max_shifts_ceiling - 1 - } else { - max_shifts_ceiling - }; - self.workload_limits.insert(r.id.clone(), max_shifts); - } - } - - pub fn calculate_holiday_limits(&mut self) { - let total_slots = self.total_slots(); - let total_holiday_slots = self.total_holiday_slots(); - - for r in &self.residents { - let workload_limit = *self.workload_limits.get(&r.id).unwrap_or(&0); - - let share = (workload_limit as f32 / total_slots as f32) * total_holiday_slots as f32; - let holiday_limit = share.ceil() as u8; - - self.holiday_limits.insert(r.id.clone(), holiday_limit); - } - } - - /// shift type count fairness - pub fn calculate_shift_type_fairness(&mut self) { - let mut global_supply = self.get_initial_supply(); - let mut local_limits = HashMap::new(); - let mut local_thresholds = HashMap::new(); - - let all_shift_types = [ - ShiftType::OpenFirst, - ShiftType::OpenSecond, - ShiftType::Closed, - ]; - - // residents with 1 available shift types - for res in self.residents.iter().filter(|r| r.allowed_types.len() == 1) { - let stype = &res.allowed_types[0]; - let total_limit = *self.workload_limits.get(&res.id).unwrap_or(&0); - - local_limits.insert((res.id.clone(), stype.clone()), total_limit); - local_thresholds.insert((res.id.clone(), stype.clone()), total_limit - 1); - - for other_type in &all_shift_types { - if other_type != stype { - local_limits.insert((res.id.clone(), other_type.clone()), 0); - local_thresholds.insert((res.id.clone(), other_type.clone()), 0); - } - } - - if let Some(s) = global_supply.get_mut(stype) { - *s = s.saturating_sub(total_limit) - } - } - - // residents with 2 available shift types - for res in self.residents.iter().filter(|r| r.allowed_types.len() == 2) { - let total_limit = *self.workload_limits.get(&res.id).unwrap_or(&0) as f32; - let per_type = (total_limit / 2.0).ceil() as u8; - - for stype in &all_shift_types { - if res.allowed_types.contains(stype) { - local_limits.insert((res.id.clone(), stype.clone()), per_type); - local_thresholds.insert((res.id.clone(), stype.clone()), per_type - 1); - if let Some(s) = global_supply.get_mut(stype) { - *s = s.saturating_sub(per_type) - } - } else { - local_limits.insert((res.id.clone(), stype.clone()), 0); - local_thresholds.insert((res.id.clone(), stype.clone()), 0); - } - } - } - - // residents with 3 available shift types - let generalists: Vec<&Resident> = self - .residents - .iter() - .filter(|r| r.allowed_types.len() == 3) - .collect(); - - if !generalists.is_empty() { - for stype in &all_shift_types { - let remaining = *global_supply.get(stype).unwrap_or(&0); - let fair_slice = (remaining as f32 / generalists.len() as f32) - .ceil() - .max(0.0) as u8; - - for res in &generalists { - local_limits.insert((res.id.clone(), stype.clone()), fair_slice); - local_thresholds.insert((res.id.clone(), stype.clone()), fair_slice - 1); - } - } - } - - self.shift_type_limits = local_limits; - self.shift_type_threshold = local_thresholds; - } - /// Return all possible candidates for the next slot /// TODO: move this to another file, UserConfig should only hold the info set from GUI /// @@ -301,10 +135,6 @@ impl UserConfig { holidays: vec![], residents: vec![], toxic_pairs: vec![], - workload_limits: HashMap::new(), - holiday_limits: HashMap::new(), - shift_type_limits: HashMap::new(), - shift_type_threshold: HashMap::new(), } } } @@ -317,19 +147,12 @@ impl From for UserConfig { holidays: value.holidays, residents: value.residents.into_iter().map(Resident::from).collect(), toxic_pairs: value.toxic_pairs, - workload_limits: HashMap::new(), - holiday_limits: HashMap::new(), - shift_type_limits: HashMap::new(), - shift_type_threshold: HashMap::new(), } } } #[cfg(test)] mod tests { - use std::collections::HashMap; - - use chrono::Month; use rstest::{fixture, rstest}; use crate::{ @@ -384,73 +207,6 @@ mod tests { } } - #[rstest] - fn test_set_limits_fair_distribution() { - let mut config = UserConfig::default().with_residents(vec![ - Resident::new("1", "Stefanos").with_max_shifts(2), - Resident::new("2", "Iordanis").with_max_shifts(2), - Resident::new("3", "Maria").with_reduced_load(), - Resident::new("4", "Veatriki"), - Resident::new("5", "Takis"), - ]); - - config.calculate_workload_limits(); - - assert_eq!(config.workload_limits["1"], 2); - assert_eq!(config.workload_limits["2"], 2); - assert_eq!(config.workload_limits["3"], 12); - assert_eq!(config.workload_limits["4"], 13); - assert_eq!(config.workload_limits["5"], 13); - } - - #[rstest] - fn test_set_limits_complex_distribution() { - let mut config = UserConfig { - month: Month::January, - year: 2026, - holidays: vec![], - toxic_pairs: vec![], - workload_limits: HashMap::new(), - residents: vec![ - Resident::new("1", "Stefanos").with_max_shifts(2), - Resident::new("2", "Iordanis").with_max_shifts(2), - Resident::new("3", "Maria").with_reduced_load(), - Resident::new("4", "Veatriki"), - Resident::new("5", "Takis"), - ], - holiday_limits: HashMap::new(), - shift_type_limits: HashMap::new(), - shift_type_threshold: HashMap::new(), - }; - - config.calculate_workload_limits(); - - assert_eq!(config.workload_limits["1"], 2); - assert_eq!(config.workload_limits["2"], 2); - assert_eq!(config.workload_limits["3"], 14); - assert_eq!(config.workload_limits["4"], 15); - assert_eq!(config.workload_limits["5"], 15); - } - - #[rstest] - fn test_calculate_holiday_limits() { - let mut config = UserConfig::default(); - - let stefanos = Resident::new("1", "Stefanos"); - let iordanis = Resident::new("2", "Iordanis"); - - config.residents = vec![stefanos, iordanis]; - - config.calculate_workload_limits(); - config.calculate_holiday_limits(); - - let stefanos_limit = *config.holiday_limits.get("1").unwrap(); - let iordanis_limit = *config.holiday_limits.get("2").unwrap(); - - assert_eq!(stefanos_limit, 6); - assert_eq!(iordanis_limit, 6); - } - #[rstest] fn test_total_holiday_slots() { let config = UserConfig::default().with_holidays(vec![2, 3, 4]); diff --git a/src-tauri/src/export.rs b/src-tauri/src/export.rs index 191162b..c41e833 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::{ - config::UserConfig, generator::backtracking, resident::Resident, schedule::MonthlySchedule, - slot::Slot, + bounds::WorkloadBounds, config::UserConfig, generator::backtracking, resident::Resident, + schedule::MonthlySchedule, slot::Slot, }; #[fixture] @@ -175,23 +175,28 @@ mod tests { #[fixture] fn config() -> UserConfig { - let mut config = UserConfig::default().with_residents(vec![ + UserConfig::default().with_residents(vec![ Resident::new("1", "Στέφανος"), Resident::new("2", "Ιορδάνης"), Resident::new("3", "Μαρία"), Resident::new("4", "Βεατρίκη"), Resident::new("5", "Τάκης"), Resident::new("6", "Μάκης"), - ]); - config.calculate_workload_limits(); - config.calculate_holiday_limits(); - config.calculate_shift_type_fairness(); - config + ]) + } + + #[fixture] + fn bounds(config: UserConfig) -> WorkloadBounds { + WorkloadBounds::new_with_config(&config) } #[rstest] - pub fn test_export_as_doc(mut schedule: MonthlySchedule, config: UserConfig) { - backtracking(&mut schedule, Slot::default(), &config); + 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); } } diff --git a/src-tauri/src/generator.rs b/src-tauri/src/generator.rs index ee97679..a87a868 100644 --- a/src-tauri/src/generator.rs +++ b/src-tauri/src/generator.rs @@ -1,18 +1,23 @@ use rand::Rng; -use crate::{config::UserConfig, schedule::MonthlySchedule, slot::Slot}; +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) -> bool { - if !slot.is_first() && schedule.restrictions_violated(&slot.previous(), config) { +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) { + if !schedule.is_per_shift_threshold_met(config, bounds) { return false; } @@ -21,7 +26,7 @@ pub fn backtracking(schedule: &mut MonthlySchedule, slot: Slot, config: &UserCon } if schedule.is_slot_manually_assigned(&slot) { - return backtracking(schedule, slot.next(), config); + return backtracking(schedule, slot.next(), config, bounds); } // sort candidates by current workload, add rng for tie breakers @@ -35,7 +40,7 @@ pub fn backtracking(schedule: &mut MonthlySchedule, slot: Slot, config: &UserCon for (resident, _) in candidates { schedule.insert(slot, resident); - if backtracking(schedule, slot.next(), config) { + if backtracking(schedule, slot.next(), config, bounds) { log::trace!("Solution found, exiting recursive algorithm"); return true; } @@ -51,6 +56,7 @@ mod tests { use rstest::{fixture, rstest}; use crate::{ + bounds::WorkloadBounds, config::UserConfig, generator::backtracking, resident::Resident, @@ -65,23 +71,33 @@ mod tests { #[fixture] fn config() -> UserConfig { - let mut config = UserConfig::default().with_residents(vec![ + 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"), - ]); - config.calculate_workload_limits(); - config.calculate_holiday_limits(); - config.calculate_shift_type_fairness(); - config + ]) + } + + #[fixture] + fn bounds(config: UserConfig) -> WorkloadBounds { + WorkloadBounds::new_with_config(&config) } #[rstest] - fn test_backtracking(mut schedule: MonthlySchedule, config: UserConfig) { - assert!(backtracking(&mut schedule, Slot::default(), &config)); + 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); @@ -98,7 +114,7 @@ mod tests { for r in &config.residents { let workload = schedule.current_workload(r); - let limit = *config.workload_limits.get(&r.id).unwrap(); + let limit = *bounds.max_workloads.get(&r.id).unwrap(); assert!(workload <= limit as usize); } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 6a645e7..afb87fb 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -3,6 +3,7 @@ use std::sync::Mutex; use log::info; use crate::{ + bounds::WorkloadBounds, config::{UserConfig, UserConfigDTO}, export::{Export, FileType}, generator::backtracking, @@ -10,10 +11,10 @@ use crate::{ slot::Slot, }; +mod bounds; +mod config; mod export; mod generator; - -mod config; mod resident; mod schedule; mod slot; @@ -28,16 +29,14 @@ struct AppState { #[tauri::command] fn generate(config: UserConfigDTO, state: tauri::State<'_, AppState>) -> MonthlySchedule { info!("{:?}", config); - let mut config = UserConfig::from(config); - config.calculate_workload_limits(); - config.calculate_holiday_limits(); - config.calculate_shift_type_fairness(); + 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)); - backtracking(&mut schedule, Slot::default(), &config); + backtracking(&mut schedule, Slot::default(), &config, &bounds); let mut internal_schedule = state.schedule.lock().unwrap(); *internal_schedule = schedule.clone(); info!("{}", schedule.pretty_print(&config)); diff --git a/src-tauri/src/resident.rs b/src-tauri/src/resident.rs index 3d8e62b..c8cd73e 100644 --- a/src-tauri/src/resident.rs +++ b/src-tauri/src/resident.rs @@ -98,4 +98,4 @@ impl From for Resident { reduced_load: value.reduced_load, } } -} \ No newline at end of file +} diff --git a/src-tauri/src/schedule.rs b/src-tauri/src/schedule.rs index 4a5c888..ccd5961 100644 --- a/src-tauri/src/schedule.rs +++ b/src-tauri/src/schedule.rs @@ -2,6 +2,7 @@ use serde::{ser::SerializeMap, Deserialize, Serialize}; use std::collections::HashMap; use crate::{ + bounds::WorkloadBounds, config::UserConfig, resident::Resident, slot::{weekday_to_greek, Day, ShiftPosition, Slot}, @@ -73,7 +74,7 @@ impl MonthlySchedule { .count() } - pub fn is_per_shift_threshold_met(&self, config: &UserConfig) -> bool { + pub fn is_per_shift_threshold_met(&self, config: &UserConfig, bounds: &WorkloadBounds) -> bool { for res in &config.residents { for stype in [ ShiftType::OpenFirst, @@ -81,8 +82,7 @@ impl MonthlySchedule { ShiftType::Closed, ] { let count = self.count_shifts(&res.id, Some(stype.clone())); - if let Some(&threshold) = config.shift_type_threshold.get(&(res.id.clone(), stype)) - { + if let Some(&threshold) = bounds.min_by_shift_type.get(&(res.id.clone(), stype)) { if count < threshold as usize { return false; } @@ -111,12 +111,17 @@ impl MonthlySchedule { /// /// @slot points to an occupied slot /// @config info manually set on the GUI by the user - pub fn restrictions_violated(&self, slot: &Slot, config: &UserConfig) -> bool { + pub fn restrictions_violated( + &self, + slot: &Slot, + config: &UserConfig, + bounds: &WorkloadBounds, + ) -> bool { self.same_resident_in_consecutive_days(slot) || self.has_toxic_pair(slot, config) - || self.is_workload_unbalanced(slot, config) - || self.is_holiday_workload_imbalanced(slot, config) - || self.is_shift_type_distribution_unfair(slot, config) + || self.is_workload_unbalanced(slot, config, bounds) + || self.is_holiday_workload_imbalanced(slot, config, bounds) + || self.is_shift_type_distribution_unfair(slot, bounds) } /// same_resident_in_consecutive_days @@ -160,7 +165,12 @@ impl MonthlySchedule { } /// is_workload_unbalanced - pub fn is_workload_unbalanced(&self, slot: &Slot, config: &UserConfig) -> bool { + pub fn is_workload_unbalanced( + &self, + slot: &Slot, + config: &UserConfig, + bounds: &WorkloadBounds, + ) -> bool { let res_id = match self.get_resident_id(slot) { Some(id) => id, None => return false, @@ -169,7 +179,7 @@ impl MonthlySchedule { if let Some(resident) = config.residents.iter().find(|r| &r.id == res_id) { let current_workload = self.current_workload(resident); - if let Some(&limit) = config.workload_limits.get(res_id) { + if let Some(&limit) = bounds.max_workloads.get(res_id) { let mut workload_limit = limit; if resident.reduced_load { workload_limit -= 1; @@ -185,7 +195,12 @@ impl MonthlySchedule { } /// is_holiday_workload_imbalanced - pub fn is_holiday_workload_imbalanced(&self, slot: &Slot, config: &UserConfig) -> bool { + pub fn is_holiday_workload_imbalanced( + &self, + slot: &Slot, + config: &UserConfig, + bounds: &WorkloadBounds, + ) -> bool { if !config.is_holiday_or_weekend_slot(slot.day.0) { return false; } @@ -198,7 +213,7 @@ impl MonthlySchedule { if let Some(resident) = config.residents.iter().find(|r| &r.id == res_id) { let current_holiday_workload = self.current_holiday_workload(resident, config); - if let Some(&holiday_limit) = config.holiday_limits.get(res_id) { + if let Some(&holiday_limit) = bounds.max_holiday_shifts.get(res_id) { if current_holiday_workload > holiday_limit as usize { return true; } @@ -209,7 +224,7 @@ impl MonthlySchedule { } /// is_shift_type_distribution_unfair - pub fn is_shift_type_distribution_unfair(&self, slot: &Slot, config: &UserConfig) -> bool { + pub fn is_shift_type_distribution_unfair(&self, slot: &Slot, bounds: &WorkloadBounds) -> bool { let resident_id = match self.get_resident_id(slot) { Some(id) => id, None => return false, @@ -226,8 +241,8 @@ impl MonthlySchedule { let current_count = self.count_shifts(resident_id, Some(current_shift_type.clone())); - if let Some(&limit) = config - .shift_type_limits + if let Some(&limit) = bounds + .max_by_shift_type .get(&(resident_id.clone(), current_shift_type.clone())) { return current_count > limit as usize; @@ -325,6 +340,7 @@ mod tests { use rstest::{fixture, rstest}; use crate::{ + bounds::WorkloadBounds, config::UserConfig, resident::Resident, schedule::{Day, MonthlySchedule, Slot}, @@ -413,7 +429,7 @@ mod tests { } #[rstest] - fn test_is_workload_unbalanced(mut schedule: MonthlySchedule, mut config: UserConfig) { + fn test_is_workload_unbalanced(mut schedule: MonthlySchedule, config: UserConfig) { let slot_1 = Slot::new(Day(1), ShiftPosition::First); let slot_2 = Slot::new(Day(1), ShiftPosition::Second); let slot_3 = Slot::new(Day(2), ShiftPosition::First); @@ -421,21 +437,23 @@ mod tests { let stefanos = &config.residents[0]; let iordanis = &config.residents[1]; - config.workload_limits.insert("1".to_string(), 1); - config.workload_limits.insert("2".to_string(), 2); + let mut bounds = WorkloadBounds::new(); + + bounds.max_workloads.insert("1".to_string(), 1); + bounds.max_workloads.insert("2".to_string(), 2); schedule.insert(slot_1, &stefanos); - assert!(!schedule.is_workload_unbalanced(&slot_1, &config)); + assert!(!schedule.is_workload_unbalanced(&slot_1, &config, &bounds)); schedule.insert(slot_2, &iordanis); - assert!(!schedule.is_workload_unbalanced(&slot_2, &config)); + assert!(!schedule.is_workload_unbalanced(&slot_2, &config, &bounds)); schedule.insert(slot_3, &stefanos); - assert!(schedule.is_workload_unbalanced(&slot_3, &config)); + assert!(schedule.is_workload_unbalanced(&slot_3, &config, &bounds)); } #[rstest] - fn test_is_holiday_workload_imbalanced(mut schedule: MonthlySchedule, mut config: UserConfig) { + fn test_is_holiday_workload_imbalanced(mut schedule: MonthlySchedule, config: UserConfig) { let slot_1 = Slot::new(Day(1), ShiftPosition::First); let slot_2 = Slot::new(Day(1), ShiftPosition::Second); let slot_7 = Slot::new(Day(7), ShiftPosition::First); @@ -443,16 +461,18 @@ mod tests { let stefanos = &config.residents[0]; let iordanis = &config.residents[1]; - config.holiday_limits.insert("1".to_string(), 1); - config.holiday_limits.insert("2".to_string(), 1); + let mut bounds = WorkloadBounds::new(); + + bounds.max_holiday_shifts.insert("1".to_string(), 1); + bounds.max_holiday_shifts.insert("2".to_string(), 1); schedule.insert(slot_1, &stefanos); - assert!(!schedule.is_holiday_workload_imbalanced(&slot_1, &config)); + assert!(!schedule.is_holiday_workload_imbalanced(&slot_1, &config, &bounds)); schedule.insert(slot_2, &iordanis); - assert!(!schedule.is_holiday_workload_imbalanced(&slot_2, &config)); + assert!(!schedule.is_holiday_workload_imbalanced(&slot_2, &config, &bounds)); schedule.insert(slot_7, &stefanos); - assert!(schedule.is_holiday_workload_imbalanced(&slot_7, &config)); + assert!(schedule.is_holiday_workload_imbalanced(&slot_7, &config, &bounds)); } }