File size: 2,678 Bytes
b7e7f16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
use chrono::NaiveDate;
use std::cmp::Reverse;
use std::collections::BTreeSet;

use crate::domain::{Employee, Shift};

use super::coverage::{candidate_redundancy_is_valid, public_candidate_counts};
use super::time_utils::horizon_dates;
use super::vocabulary::EXTRA_UNAVAILABLE_COUNT;

/// Adds a small amount of extra unavailability without breaking public feasibility.
///
/// The goal is not to make the dataset impossible. The goal is to remove some
/// trivial interchangeable assignments so local search has a clearer signal.
pub(super) fn add_extra_unavailability(
    employees: &mut [Employee],
    shifts: &[Shift],
    witness_dates: &[BTreeSet<NaiveDate>],
) {
    let horizon_dates = horizon_dates(shifts);

    for _ in 0..EXTRA_UNAVAILABLE_COUNT {
        let best_candidate = (0..employees.len())
            .flat_map(|employee_index| {
                horizon_dates
                    .iter()
                    .copied()
                    .map(move |date| (employee_index, date))
            })
            .filter(|&(employee_index, date)| {
                !employees[employee_index].unavailable_dates.contains(&date)
                    && !witness_dates[employee_index].contains(&date)
            })
            .filter_map(|(employee_index, date)| {
                let score = extra_unavailability_score(employees, shifts, employee_index, date)?;
                Some((score, employee_index, date))
            })
            .max_by_key(|&(score, employee_index, date)| {
                (score, Reverse(employee_index), Reverse(date))
            });

        let Some((_, employee_index, date)) = best_candidate else {
            break;
        };
        employees[employee_index].unavailable_dates.insert(date);
    }
}

/// Scores one candidate "employee unavailable on date" mutation.
fn extra_unavailability_score(
    employees: &[Employee],
    shifts: &[Shift],
    employee_index: usize,
    date: NaiveDate,
) -> Option<(usize, usize, usize)> {
    let mut cloned: Vec<Employee> = employees.to_vec();
    cloned[employee_index].unavailable_dates.insert(date);
    if !candidate_redundancy_is_valid(&cloned, shifts) {
        return None;
    }

    let counts = public_candidate_counts(&cloned, shifts);
    let affected: Vec<usize> = shifts
        .iter()
        .enumerate()
        .filter(|(_, shift)| shift.touched_dates.contains(&date))
        .map(|(index, _)| counts[index])
        .collect();
    let min_affected = affected.into_iter().min().unwrap_or(usize::MAX);
    let shifts_with_three_plus = counts.iter().filter(|&&count| count >= 3).count();
    Some((min_affected, shifts_with_three_plus, counts.iter().sum()))
}