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

View File

@@ -164,8 +164,8 @@ mod tests {
use rstest::{fixture, rstest};
use crate::{
bounds::WorkloadBounds, config::UserConfig, generator::backtracking, resident::Resident,
schedule::MonthlySchedule, slot::Slot,
bounds::WorkloadBounds, config::UserConfig, resident::Resident, schedule::MonthlySchedule,
scheduler::Scheduler, slot::Slot,
};
#[fixture]
@@ -190,13 +190,14 @@ mod tests {
WorkloadBounds::new_with_config(&config)
}
#[fixture]
fn scheduler(config: UserConfig, bounds: WorkloadBounds) -> Scheduler {
Scheduler::new(config, bounds)
}
#[rstest]
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);
pub fn test_export_as_doc(mut schedule: MonthlySchedule, scheduler: Scheduler) {
scheduler.search(&mut schedule, Slot::default());
schedule.export_as_doc(&scheduler.config);
}
}

View File

@@ -1,123 +0,0 @@
use rand::Rng;
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,
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, bounds) {
return false;
}
log::trace!("Solution found, exiting recursive algorithm");
return true;
}
if schedule.is_slot_manually_assigned(&slot) {
return backtracking(schedule, slot.next(), config, bounds);
}
// sort candidates by current workload, add rng for tie breakers
let mut candidates = 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 backtracking(schedule, slot.next(), config, bounds) {
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,
generator::backtracking,
resident::Resident,
schedule::MonthlySchedule,
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)
}
#[rstest]
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);
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 &config.residents {
let workload = schedule.current_workload(r);
let limit = *bounds.max_workloads.get(&r.id).unwrap();
assert!(workload <= limit as usize);
}
println!("{}", schedule.pretty_print(&config));
}
}

View File

@@ -1,22 +1,19 @@
use std::sync::Mutex;
use log::info;
use crate::{
bounds::WorkloadBounds,
config::{UserConfig, UserConfigDTO},
export::{Export, FileType},
generator::backtracking,
schedule::MonthlySchedule,
slot::Slot,
scheduler::Scheduler,
};
mod bounds;
mod config;
mod export;
mod generator;
mod resident;
mod schedule;
mod scheduler;
mod slot;
struct AppState {
@@ -28,18 +25,16 @@ struct AppState {
/// and the period of the schedule
#[tauri::command]
fn generate(config: UserConfigDTO, state: tauri::State<'_, AppState>) -> MonthlySchedule {
info!("{:?}", config);
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));
let bounds = WorkloadBounds::new_with_config(&config);
let scheduler = Scheduler::new(config, bounds);
scheduler.run(&mut schedule);
backtracking(&mut schedule, Slot::default(), &config, &bounds);
let mut internal_schedule = state.schedule.lock().unwrap();
*internal_schedule = schedule.clone();
info!("{}", schedule.pretty_print(&config));
schedule
}

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));
}
}