use std::cmp::Reverse; use super::employees::EmployeeBlueprint; use super::vocabulary::*; /// Running totals for one weekday-off cohort. /// /// We use this to distribute scarce specialties across the seven primary /// off-day groups instead of accidentally clustering too many similar people on /// the same day off. #[derive(Default, Clone, Copy)] struct CohortLoad { size: usize, doctors: usize, nurses: usize, ambulatory_doctors: usize, ambulatory_nurses: usize, neurology_doctors: usize, neurology_nurses: usize, critical_doctors: usize, critical_nurses: usize, pediatric_doctors: usize, pediatric_nurses: usize, surgery_doctors: usize, surgery_nurses: usize, outpatient_doctors: usize, outpatient_nurses: usize, radiology_day: usize, radiology_nurses: usize, radiology_call: usize, cardiology: usize, anaesthetics: usize, } impl CohortLoad { /// Updates the cohort totals after placing one blueprint into it. fn add(&mut self, blueprint: &EmployeeBlueprint) { self.size += 1; if blueprint.skills.contains(DOCTOR) { self.doctors += 1; } if blueprint.skills.contains(NURSE) { self.nurses += 1; } if blueprint.skills.contains(AMBULATORY_DOCTOR) { self.ambulatory_doctors += 1; } if blueprint.skills.contains(AMBULATORY_NURSE) { self.ambulatory_nurses += 1; } if blueprint.skills.contains(NEUROLOGY_DOCTOR) { self.neurology_doctors += 1; } if blueprint.skills.contains(NEUROLOGY_NURSE) { self.neurology_nurses += 1; } if blueprint.skills.contains(CRITICAL_DOCTOR) { self.critical_doctors += 1; } if blueprint.skills.contains(CRITICAL_NURSE) { self.critical_nurses += 1; } if blueprint.skills.contains(PEDIATRIC_DOCTOR) { self.pediatric_doctors += 1; } if blueprint.skills.contains(PEDIATRIC_NURSE) { self.pediatric_nurses += 1; } if blueprint.skills.contains(SURGERY_DOCTOR) { self.surgery_doctors += 1; } if blueprint.skills.contains(SURGERY_NURSE) { self.surgery_nurses += 1; } if blueprint.skills.contains(OUTPATIENT_DOCTOR) { self.outpatient_doctors += 1; } if blueprint.skills.contains(OUTPATIENT_NURSE) { self.outpatient_nurses += 1; } if blueprint.skills.contains(RADIOLOGY_DAY) { self.radiology_day += 1; } if blueprint.skills.contains(RADIOLOGY_NURSE) { self.radiology_nurses += 1; } if blueprint.skills.contains(RADIOLOGY_CALL) { self.radiology_call += 1; } if blueprint.skills.contains(CARDIOLOGY) { self.cardiology += 1; } if blueprint.skills.contains(ANAESTHETICS) { self.anaesthetics += 1; } } } /// Assigns each employee blueprint a stable primary off weekday. pub(super) fn assign_primary_off_days(blueprints: &mut [EmployeeBlueprint]) { let mut order: Vec = (0..blueprints.len()).collect(); order.sort_by_key(|&index| Reverse(blueprint_priority(&blueprints[index]))); let mut loads = [CohortLoad::default(); 7]; for employee_index in order { let cohort = (0..7) .filter(|&candidate| loads[candidate].size < PRIMARY_OFF_COHORT_SIZES[candidate]) .min_by_key(|&candidate| cohort_score(&loads[candidate], &blueprints[employee_index])) .expect("cohort should have spare capacity"); blueprints[employee_index].primary_off_weekday = cohort; loads[cohort].add(&blueprints[employee_index]); } } /// Scarcer or more specialized blueprints get placed first. fn blueprint_priority(blueprint: &EmployeeBlueprint) -> (usize, usize, usize, usize, usize) { ( blueprint.specialty_count(), usize::from(blueprint.skills.contains(DOCTOR)), usize::from(blueprint.skills.contains(CARDIOLOGY)), usize::from(blueprint.skills.contains(ANAESTHETICS)), usize::from(blueprint.skills.contains(RADIOLOGY_CALL)), ) } /// Lower scores mean "this cohort needs this blueprint more". fn cohort_score(load: &CohortLoad, blueprint: &EmployeeBlueprint) -> (usize, usize, usize, usize) { let line_pressure = weighted_line_load(load, blueprint); ( line_pressure, if blueprint.skills.contains(DOCTOR) { load.doctors } else { load.nurses }, load.size, blueprint.primary_off_weekday, ) } /// Applies weighted pressure so rare specialties dominate balancing decisions. fn weighted_line_load(load: &CohortLoad, blueprint: &EmployeeBlueprint) -> usize { let mut score = 0usize; for (skill, weight) in line_balance_weights() { if blueprint.has_skill(skill) { score += line_load_for_skill(load, skill) * weight; } } score } /// Manual weights that treat some specialties as harder to concentrate. fn line_balance_weights() -> &'static [(&'static str, usize)] { &[ (CARDIOLOGY, 8), (RADIOLOGY_CALL, 8), (ANAESTHETICS, 7), (SURGERY_NURSE, 6), (SURGERY_DOCTOR, 6), (NEUROLOGY_DOCTOR, 6), (RADIOLOGY_DAY, 5), (RADIOLOGY_NURSE, 5), (OUTPATIENT_DOCTOR, 5), (OUTPATIENT_NURSE, 5), (AMBULATORY_DOCTOR, 4), (AMBULATORY_NURSE, 4), (PEDIATRIC_DOCTOR, 4), (PEDIATRIC_NURSE, 4), (NEUROLOGY_NURSE, 4), (CRITICAL_DOCTOR, 3), (CRITICAL_NURSE, 3), ] } /// Reads the current cohort count for one specific skill. fn line_load_for_skill(load: &CohortLoad, skill: &'static str) -> usize { match skill { AMBULATORY_DOCTOR => load.ambulatory_doctors, AMBULATORY_NURSE => load.ambulatory_nurses, NEUROLOGY_DOCTOR => load.neurology_doctors, NEUROLOGY_NURSE => load.neurology_nurses, CRITICAL_DOCTOR => load.critical_doctors, CRITICAL_NURSE => load.critical_nurses, PEDIATRIC_DOCTOR => load.pediatric_doctors, PEDIATRIC_NURSE => load.pediatric_nurses, SURGERY_DOCTOR => load.surgery_doctors, SURGERY_NURSE => load.surgery_nurses, OUTPATIENT_DOCTOR => load.outpatient_doctors, OUTPATIENT_NURSE => load.outpatient_nurses, RADIOLOGY_DAY => load.radiology_day, RADIOLOGY_NURSE => load.radiology_nurses, RADIOLOGY_CALL => load.radiology_call, CARDIOLOGY => load.cardiology, ANAESTHETICS => load.anaesthetics, _ => 0, } }