Move valid_residents to the scheduler impl and simplify it
This commit is contained in:
@@ -3,8 +3,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
resident::{Resident, ResidentDTO},
|
resident::{Resident, ResidentDTO},
|
||||||
schedule::{MonthlySchedule, ShiftType},
|
slot::Day
|
||||||
slot::{Day, ShiftPosition, Slot},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const YEAR: i32 = 2026;
|
const YEAR: i32 = 2026;
|
||||||
@@ -80,54 +79,6 @@ impl UserConfig {
|
|||||||
|| self.holidays.contains(&(day.0 as usize))
|
|| 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 {
|
pub fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
month: Month::try_from(2).unwrap(),
|
month: Month::try_from(2).unwrap(),
|
||||||
@@ -155,12 +106,7 @@ impl From<UserConfigDTO> for UserConfig {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use rstest::{fixture, rstest};
|
use rstest::{fixture, rstest};
|
||||||
|
|
||||||
use crate::{
|
use crate::{config::UserConfig, resident::Resident, schedule::MonthlySchedule};
|
||||||
config::UserConfig,
|
|
||||||
resident::Resident,
|
|
||||||
schedule::{MonthlySchedule, ShiftType},
|
|
||||||
slot::{Day, ShiftPosition, Slot},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[fixture]
|
#[fixture]
|
||||||
fn setup() -> (UserConfig, MonthlySchedule) {
|
fn setup() -> (UserConfig, MonthlySchedule) {
|
||||||
@@ -175,38 +121,6 @@ mod tests {
|
|||||||
(config, schedule)
|
(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]
|
#[rstest]
|
||||||
fn test_total_holiday_slots() {
|
fn test_total_holiday_slots() {
|
||||||
let config = UserConfig::default().with_holidays(vec![2, 3, 4]);
|
let config = UserConfig::default().with_holidays(vec![2, 3, 4]);
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ impl MonthlySchedule {
|
|||||||
pub fn prefill(&mut self, config: &UserConfig) {
|
pub fn prefill(&mut self, config: &UserConfig) {
|
||||||
for r in &config.residents {
|
for r in &config.residents {
|
||||||
for s in &r.manual_shifts {
|
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)
|
self.0.get(slot)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn current_workload(&self, resident: &Resident) -> usize {
|
pub fn current_workload(&self, resident_id: &str) -> usize {
|
||||||
self.0
|
self.0
|
||||||
.values()
|
.values()
|
||||||
.filter(|res_id| res_id == &&resident.id)
|
.filter(|res_id| res_id == &resident_id)
|
||||||
.count()
|
.count()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,8 +92,8 @@ impl MonthlySchedule {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert(&mut self, slot: Slot, resident: &Resident) {
|
pub fn insert(&mut self, slot: Slot, resident_id: &str) {
|
||||||
self.0.insert(slot, resident.id.clone());
|
self.0.insert(slot, resident_id.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove(&mut self, slot: Slot) {
|
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) {
|
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) {
|
if let Some(&limit) = bounds.max_workloads.get(res_id) {
|
||||||
let mut workload_limit = limit;
|
let mut workload_limit = limit;
|
||||||
@@ -293,7 +293,7 @@ impl MonthlySchedule {
|
|||||||
residents.sort_by_key(|r| &r.name);
|
residents.sort_by_key(|r| &r.name);
|
||||||
|
|
||||||
for res in residents {
|
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 o1 = self.count_shifts(&res.id, Some(ShiftType::OpenFirst));
|
||||||
let o2 = self.count_shifts(&res.id, Some(ShiftType::OpenSecond));
|
let o2 = self.count_shifts(&res.id, Some(ShiftType::OpenSecond));
|
||||||
let cl = self.count_shifts(&res.id, Some(ShiftType::Closed));
|
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_1 = Slot::new(Day(1), ShiftPosition::First);
|
||||||
let slot_2 = Slot::new(Day(1), ShiftPosition::Second);
|
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.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);
|
assert_eq!(schedule.get_resident_id(&slot_2), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -391,12 +391,12 @@ mod tests {
|
|||||||
fn test_remove_resident(mut schedule: MonthlySchedule, resident: Resident) {
|
fn test_remove_resident(mut schedule: MonthlySchedule, resident: Resident) {
|
||||||
let slot_1 = Slot::new(Day(1), ShiftPosition::First);
|
let slot_1 = Slot::new(Day(1), ShiftPosition::First);
|
||||||
|
|
||||||
schedule.insert(slot_1, &resident);
|
schedule.insert(slot_1, &resident.id);
|
||||||
assert_eq!(schedule.current_workload(&resident), 1);
|
assert_eq!(schedule.current_workload(&resident.id), 1);
|
||||||
|
|
||||||
schedule.remove(slot_1);
|
schedule.remove(slot_1);
|
||||||
assert_eq!(schedule.get_resident_id(&slot_1), None);
|
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]
|
#[rstest]
|
||||||
@@ -405,9 +405,9 @@ mod tests {
|
|||||||
let slot_2 = Slot::new(Day(1), ShiftPosition::Second);
|
let slot_2 = Slot::new(Day(1), ShiftPosition::Second);
|
||||||
let slot_3 = Slot::new(Day(2), ShiftPosition::First);
|
let slot_3 = Slot::new(Day(2), ShiftPosition::First);
|
||||||
|
|
||||||
schedule.insert(slot_1, &resident);
|
schedule.insert(slot_1, &resident.id);
|
||||||
schedule.insert(slot_2, &resident);
|
schedule.insert(slot_2, &resident.id);
|
||||||
schedule.insert(slot_3, &resident);
|
schedule.insert(slot_3, &resident.id);
|
||||||
|
|
||||||
assert!(!schedule.same_resident_in_consecutive_days(&slot_1));
|
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_2));
|
||||||
@@ -422,8 +422,8 @@ mod tests {
|
|||||||
let stefanos = &toxic_config.residents[0];
|
let stefanos = &toxic_config.residents[0];
|
||||||
let iordanis = &toxic_config.residents[1];
|
let iordanis = &toxic_config.residents[1];
|
||||||
|
|
||||||
schedule.insert(slot_1, stefanos);
|
schedule.insert(slot_1, &stefanos.id);
|
||||||
schedule.insert(slot_2, iordanis);
|
schedule.insert(slot_2, &iordanis.id);
|
||||||
|
|
||||||
assert!(schedule.has_toxic_pair(&slot_2, &toxic_config))
|
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("1".to_string(), 1);
|
||||||
bounds.max_workloads.insert("2".to_string(), 2);
|
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));
|
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));
|
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));
|
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("1".to_string(), 1);
|
||||||
bounds.max_holiday_shifts.insert("2".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));
|
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));
|
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));
|
assert!(schedule.is_holiday_workload_imbalanced(&slot_7, &config, &bounds));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
use rand::Rng;
|
||||||
|
|
||||||
@@ -41,15 +44,15 @@ impl Scheduler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// sort candidates by current workload, add rng for tie breakers
|
// sort candidates by current workload, add rng for tie breakers
|
||||||
let mut candidates = self.config.candidates(slot, schedule);
|
let mut valid_resident_ids = self.valid_residents(slot, schedule);
|
||||||
candidates.sort_unstable_by_key(|&(resident, _)| {
|
valid_resident_ids.sort_unstable_by_key(|res_id| {
|
||||||
let workload = schedule.current_workload(resident);
|
let workload = schedule.current_workload(res_id);
|
||||||
let tie_breaker: f64 = rand::rng().random();
|
let tie_breaker: f64 = rand::rng().random();
|
||||||
(workload, (tie_breaker * 1000.0) as usize)
|
(workload, (tie_breaker * 1000.0) as usize)
|
||||||
});
|
});
|
||||||
|
|
||||||
for (resident, _) in candidates {
|
for id in &valid_resident_ids {
|
||||||
schedule.insert(slot, resident);
|
schedule.insert(slot, id);
|
||||||
|
|
||||||
if self.search(schedule, slot.next()) {
|
if self.search(schedule, slot.next()) {
|
||||||
log::trace!("Solution found, exiting recursive algorithm");
|
log::trace!("Solution found, exiting recursive algorithm");
|
||||||
@@ -61,6 +64,19 @@ impl Scheduler {
|
|||||||
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return all valid residents for the current slot
|
||||||
|
pub fn valid_residents(&self, slot: Slot, schedule: &MonthlySchedule) -> Vec<String> {
|
||||||
|
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)]
|
#[cfg(test)]
|
||||||
@@ -121,11 +137,14 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for r in &scheduler.config.residents {
|
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();
|
let limit = *scheduler.bounds.max_workloads.get(&r.id).unwrap();
|
||||||
assert!(workload <= limit as usize);
|
assert!(workload <= limit as usize);
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("{}", schedule.pretty_print(&scheduler.config));
|
println!("{}", schedule.pretty_print(&scheduler.config));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
fn test_valid_residents(mut schedule: MonthlySchedule, scheduler: Scheduler) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,6 +77,18 @@ impl Slot {
|
|||||||
self.day.greater_than(&Day(limit))
|
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 {
|
pub fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
day: Day(1),
|
day: Day(1),
|
||||||
|
|||||||
Reference in New Issue
Block a user