Create struct scheduler to encapsulate the search logic
This commit is contained in:
131
src-tauri/src/scheduler.rs
Normal file
131
src-tauri/src/scheduler.rs
Normal file
@@ -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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user