use chrono::Timelike; use std::cmp::Reverse; use crate::domain::{Employee, Shift}; use super::support::{ can_mark_preference_date, date_pressure_for_shift, mark_preference_date, preferred_shift_date, shift_prefers_doctor_family, shift_same_shape, PreferenceKind, }; use super::PreferenceAnalysis; use crate::data::data_seed::employees::EmployeeBlueprint; use crate::data::data_seed::skills::is_specialty_skill; use crate::data::data_seed::vocabulary::DOCTOR; use crate::data::data_seed::witness::{shift_priority_rank, WitnessRoster}; /// Adds a small number of witness-relative preference swaps. /// /// This is the sharpest source of local-search signal in the dataset: one /// employee is marked as disliking a date while another feasible employee is /// marked as preferring it. pub(super) fn assign_exchange_preferences( employees: &mut [Employee], blueprints: &[EmployeeBlueprint], shifts: &[Shift], witness: &WitnessRoster, analysis: &PreferenceAnalysis, max_exchange_marks_per_employee: usize, ) { let mut exchange_marks_by_employee = vec![0usize; employees.len()]; let mut shift_order: Vec = (0..shifts.len()).collect(); shift_order.sort_by_key(|&shift_index| { let shift = &shifts[shift_index]; ( Reverse(analysis.candidate_counts[shift_index]), Reverse(date_pressure_for_shift(shift, &analysis.date_pressure)), shift_priority_rank(shift), shift.start, shift_index, ) }); for shift_index in shift_order { let shift = &shifts[shift_index]; if analysis.candidate_counts[shift_index] < 6 || is_specialty_skill(&shift.required_skill) || shift.start.time().hour() == 22 { continue; } let holder = witness.assignments[shift_index]; let date = preferred_shift_date(shift, &analysis.date_pressure); let Some(alternative) = analysis.candidate_lists[shift_index] .iter() .copied() .filter(|&candidate| candidate != holder) .filter(|&candidate| { exchange_marks_by_employee[candidate] < max_exchange_marks_per_employee }) .filter(|&candidate| { can_mark_preference_date(&employees[candidate], date, PreferenceKind::Desired) }) .min_by_key(|&candidate| { exchange_alternative_key(candidate, shift, shifts, blueprints, witness, analysis) }) else { continue; }; if mark_preference_date(&mut employees[alternative], date, PreferenceKind::Desired) { exchange_marks_by_employee[alternative] += 1; } } } /// Lower keys mean "better alternative employee for this exchange mark". fn exchange_alternative_key( candidate: usize, shift: &Shift, shifts: &[Shift], blueprints: &[EmployeeBlueprint], witness: &WitnessRoster, analysis: &PreferenceAnalysis, ) -> (usize, usize, usize, usize, usize, usize) { let date = preferred_shift_date(shift, &analysis.date_pressure); let same_date_in_witness = usize::from(witness.employee_touched_dates[candidate].contains(&date)); let same_shape_load = analysis.witness_shifts_by_employee[candidate] .iter() .filter(|&&other_shift_index| shift_same_shape(shift, &shifts[other_shift_index])) .count(); ( same_date_in_witness, usize::from( blueprints[candidate].skills.contains(DOCTOR) != shift_prefers_doctor_family(shift), ), same_shape_load, witness.employee_touched_dates[candidate].len(), usize::MAX - *analysis.date_pressure.get(&date).unwrap_or(&0), candidate, ) }