Create struct scheduler to encapsulate the search logic

This commit is contained in:
2026-01-14 00:34:48 +02:00
parent d7d4b109c3
commit 2febcb7344
4 changed files with 147 additions and 143 deletions

131
src-tauri/src/scheduler.rs Normal file
View 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));
}
}