Complete backtracking, restriction violations, split model.rs into multiple files
This commit is contained in:
324
src-tauri/src/schedule.rs
Normal file
324
src-tauri/src/schedule.rs
Normal file
@@ -0,0 +1,324 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{
|
||||
config::UserConfig,
|
||||
resident::Resident,
|
||||
slot::{Day, Slot},
|
||||
};
|
||||
|
||||
// each slot has one resident
|
||||
// a day can span between 1 or 2 slots depending on if it is open(odd) or closed(even)
|
||||
#[derive(Debug)]
|
||||
pub struct MonthlySchedule(HashMap<Slot, String>);
|
||||
|
||||
impl MonthlySchedule {
|
||||
pub fn new() -> Self {
|
||||
Self(HashMap::new())
|
||||
}
|
||||
|
||||
pub fn prefill(&mut self, config: &UserConfig) {
|
||||
for r in &config.residents {
|
||||
for s in &r.manual_shifts {
|
||||
self.insert(*s, r);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_resident_id(&self, slot: &Slot) -> Option<&String> {
|
||||
self.0.get(slot)
|
||||
}
|
||||
|
||||
pub fn current_workload(&self, resident: &Resident) -> usize {
|
||||
self.0
|
||||
.values()
|
||||
.filter(|res_id| res_id == &&resident.id)
|
||||
.count()
|
||||
}
|
||||
|
||||
pub fn current_holiday_workload(&self, resident: &Resident, config: &UserConfig) -> usize {
|
||||
self.0
|
||||
.iter()
|
||||
.filter(|(slot, res_id)| {
|
||||
res_id == &&resident.id && config.is_holiday_or_weekend_slot(slot.day.0)
|
||||
})
|
||||
.count()
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, slot: Slot, resident: &Resident) {
|
||||
self.0.insert(slot, resident.id.clone());
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, slot: Slot) {
|
||||
self.0.remove(&slot);
|
||||
}
|
||||
|
||||
pub fn is_slot_manually_assigned(&self, slot: &Slot) -> bool {
|
||||
self.0.contains_key(slot)
|
||||
}
|
||||
|
||||
/// if any restriction is violated => we return true (leading to pruning in the backtracking algorithm)
|
||||
/// 1) no same person in consecutive days
|
||||
/// 2) avoid input toxic pairs
|
||||
/// 3) apply fairness on total shifts split residents also take into account reduced (-1) workload
|
||||
///
|
||||
/// @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 {
|
||||
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)
|
||||
}
|
||||
|
||||
/// same_resident_in_consecutive_days
|
||||
pub fn same_resident_in_consecutive_days(&self, slot: &Slot) -> bool {
|
||||
if slot.day == Day(1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let previous_slot = if slot.is_open_second() {
|
||||
slot.previous().previous()
|
||||
} else {
|
||||
slot.previous()
|
||||
};
|
||||
|
||||
self.get_resident_id(&previous_slot) == self.get_resident_id(slot)
|
||||
}
|
||||
|
||||
/// has_toxic_pair
|
||||
pub fn has_toxic_pair(&self, slot: &Slot, config: &UserConfig) -> bool {
|
||||
// if it is not an open shift and we only just added the first person in an open shift
|
||||
// then we couldn't have just created a new toxic pair
|
||||
if !slot.is_open_shift() || !slot.is_open_second() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let first_id = self.get_resident_id(&slot.previous());
|
||||
let second_id = self.get_resident_id(slot);
|
||||
|
||||
if let (Some(f), Some(s)) = (first_id, second_id) {
|
||||
return config
|
||||
.toxic_pairs
|
||||
.iter()
|
||||
.any(|(r1, r2)| (r1 == f && r2 == s) || (r1 == s && r2 == f));
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// is_workload_unbalanced
|
||||
pub fn is_workload_unbalanced(&self, slot: &Slot, config: &UserConfig) -> bool {
|
||||
let res_id = match self.get_resident_id(slot) {
|
||||
Some(id) => id,
|
||||
None => return false,
|
||||
};
|
||||
|
||||
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) {
|
||||
let mut workload_limit = limit;
|
||||
if resident.reduced_load {
|
||||
workload_limit -= 1;
|
||||
}
|
||||
|
||||
if current_workload > workload_limit as usize {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// is_holiday_workload_imbalanced
|
||||
pub fn is_holiday_workload_imbalanced(&self, slot: &Slot, config: &UserConfig) -> bool {
|
||||
if !config.is_holiday_or_weekend_slot(slot.day.0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let res_id = match self.get_resident_id(slot) {
|
||||
Some(id) => id,
|
||||
None => return false,
|
||||
};
|
||||
|
||||
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 current_holiday_workload > holiday_limit as usize {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub fn pretty_print(&self, config: &UserConfig) -> String {
|
||||
let mut sorted: Vec<_> = self.0.iter().collect();
|
||||
sorted.sort_by_key(|(slot, _)| (slot.day, slot.position));
|
||||
|
||||
let mut output = String::from("MonthlySchedule {\n");
|
||||
for (slot, res_id) in sorted {
|
||||
let res_name = config
|
||||
.residents
|
||||
.iter()
|
||||
.find(|r| &r.id == res_id)
|
||||
.map(|r| r.name.as_str())
|
||||
.unwrap();
|
||||
|
||||
output.push_str(&format!(
|
||||
"Day {:2} ({:?}): {},\n",
|
||||
slot.day.0, slot.position, res_name
|
||||
));
|
||||
}
|
||||
output.push('}');
|
||||
output
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
pub enum ShiftType {
|
||||
Closed,
|
||||
OpenFirst,
|
||||
OpenSecond,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rstest::{fixture, rstest};
|
||||
|
||||
use crate::{
|
||||
config::UserConfig,
|
||||
resident::Resident,
|
||||
schedule::{Day, MonthlySchedule, Slot},
|
||||
slot::ShiftPosition,
|
||||
};
|
||||
|
||||
#[fixture]
|
||||
fn schedule() -> MonthlySchedule {
|
||||
MonthlySchedule::new()
|
||||
}
|
||||
|
||||
#[fixture]
|
||||
fn resident() -> Resident {
|
||||
Resident::new("1", "Stefanos")
|
||||
}
|
||||
|
||||
#[fixture]
|
||||
fn toxic_config() -> UserConfig {
|
||||
UserConfig::default()
|
||||
.with_residents(vec![
|
||||
Resident::new("1", "Stefanos"),
|
||||
Resident::new("2", "Iordanis"),
|
||||
])
|
||||
.with_toxic_pairs(vec![(("1".to_string(), "2".to_string()))])
|
||||
}
|
||||
|
||||
#[fixture]
|
||||
fn config() -> UserConfig {
|
||||
UserConfig::default().with_residents(vec![
|
||||
Resident::new("1", "Stefanos"),
|
||||
Resident::new("2", "Iordanis"),
|
||||
])
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_insert_resident(mut schedule: MonthlySchedule, resident: Resident) {
|
||||
let slot_1 = Slot::new(Day(1), ShiftPosition::First);
|
||||
let slot_2 = Slot::new(Day(1), ShiftPosition::Second);
|
||||
|
||||
schedule.insert(slot_1, &resident);
|
||||
|
||||
assert_eq!(schedule.get_resident_id(&slot_1), Some(&"1".to_string()));
|
||||
assert_eq!(schedule.current_workload(&resident), 1);
|
||||
assert_eq!(schedule.get_resident_id(&slot_2), None);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
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.remove(slot_1);
|
||||
assert_eq!(schedule.get_resident_id(&slot_1), None);
|
||||
assert_eq!(schedule.current_workload(&resident), 0);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_same_resident_in_consecutive_days(mut schedule: MonthlySchedule, resident: Resident) {
|
||||
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);
|
||||
|
||||
schedule.insert(slot_1, &resident);
|
||||
schedule.insert(slot_2, &resident);
|
||||
schedule.insert(slot_3, &resident);
|
||||
|
||||
assert!(!schedule.same_resident_in_consecutive_days(&slot_1));
|
||||
assert!(!schedule.same_resident_in_consecutive_days(&slot_2));
|
||||
assert!(schedule.same_resident_in_consecutive_days(&slot_3));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_has_toxic_pair(mut schedule: MonthlySchedule, toxic_config: UserConfig) {
|
||||
let slot_1 = Slot::new(Day(1), ShiftPosition::First);
|
||||
let slot_2 = Slot::new(Day(1), ShiftPosition::Second);
|
||||
|
||||
let stefanos = &toxic_config.residents[0];
|
||||
let iordanis = &toxic_config.residents[1];
|
||||
|
||||
schedule.insert(slot_1, stefanos);
|
||||
schedule.insert(slot_2, iordanis);
|
||||
|
||||
assert!(schedule.has_toxic_pair(&slot_2, &toxic_config))
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_is_workload_unbalanced(mut schedule: MonthlySchedule, mut 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);
|
||||
|
||||
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);
|
||||
|
||||
schedule.insert(slot_1, &stefanos);
|
||||
assert!(!schedule.is_workload_unbalanced(&slot_1, &config));
|
||||
|
||||
schedule.insert(slot_2, &iordanis);
|
||||
assert!(!schedule.is_workload_unbalanced(&slot_2, &config));
|
||||
|
||||
schedule.insert(slot_3, &stefanos);
|
||||
assert!(schedule.is_workload_unbalanced(&slot_3, &config));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_is_holiday_workload_imbalanced(mut schedule: MonthlySchedule, mut 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);
|
||||
|
||||
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);
|
||||
|
||||
schedule.insert(slot_1, &stefanos);
|
||||
assert!(!schedule.is_holiday_workload_imbalanced(&slot_1, &config));
|
||||
|
||||
schedule.insert(slot_2, &iordanis);
|
||||
assert!(!schedule.is_holiday_workload_imbalanced(&slot_2, &config));
|
||||
|
||||
schedule.insert(slot_7, &stefanos);
|
||||
assert!(schedule.is_holiday_workload_imbalanced(&slot_7, &config));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user