Add fairness for each shift type count

This commit is contained in:
2026-01-13 21:08:03 +02:00
parent db7623528c
commit e9ca378099
6 changed files with 297 additions and 44 deletions

View File

@@ -22,8 +22,8 @@ pub struct UserConfigDTO {
#[derive(Debug)]
pub struct UserConfig {
month: Month,
year: i32,
pub month: Month,
pub year: i32,
pub holidays: Vec<usize>,
pub residents: Vec<Resident>,
pub toxic_pairs: Vec<(String, String)>,
@@ -31,6 +31,8 @@ pub struct UserConfig {
// calculated from inputs
pub workload_limits: HashMap<String, u8>,
pub holiday_limits: HashMap<String, u8>,
pub shift_type_limits: HashMap<(String, ShiftType), u8>,
pub shift_type_threshold: HashMap<(String, ShiftType), u8>,
}
impl UserConfig {
@@ -43,6 +45,8 @@ impl UserConfig {
toxic_pairs: vec![],
workload_limits: HashMap::new(),
holiday_limits: HashMap::new(),
shift_type_limits: HashMap::new(),
shift_type_threshold: HashMap::new(),
}
}
@@ -55,6 +59,8 @@ impl UserConfig {
toxic_pairs: dto.toxic_pairs,
workload_limits: HashMap::new(),
holiday_limits: HashMap::new(),
shift_type_limits: HashMap::new(),
shift_type_threshold: HashMap::new(),
}
}
@@ -100,6 +106,22 @@ impl UserConfig {
|| self.holidays.contains(&(day.0 as usize))
}
/// get map with total amount of slots in a month for each type of shift
pub fn get_initial_supply(&self) -> HashMap<ShiftType, u8> {
let mut supply = HashMap::new();
let total_days = self.total_days();
for d in 1..=total_days {
if Day(d).is_open_shift() {
*supply.entry(ShiftType::OpenFirst).or_insert(0) += 1;
*supply.entry(ShiftType::OpenSecond).or_insert(0) += 1;
} else {
*supply.entry(ShiftType::Closed).or_insert(0) += 1;
}
}
supply
}
/// this is called after the user config params have been initialized, can be done with the builder (lite) pattern
/// initialize a hashmap for O(1) search calls for the residents' max workload
pub fn calculate_workload_limits(&mut self) {
@@ -162,6 +184,82 @@ impl UserConfig {
}
}
/// shift type count fairness
pub fn calculate_shift_type_fairness(&mut self) {
let mut global_supply = self.get_initial_supply();
let mut local_limits = HashMap::new();
let mut local_thresholds = HashMap::new();
let all_shift_types = [
ShiftType::OpenFirst,
ShiftType::OpenSecond,
ShiftType::Closed,
];
// residents with 1 available shift types
for res in self.residents.iter().filter(|r| r.allowed_types.len() == 1) {
let stype = &res.allowed_types[0];
let total_limit = *self.workload_limits.get(&res.id).unwrap_or(&0);
local_limits.insert((res.id.clone(), stype.clone()), total_limit);
local_thresholds.insert((res.id.clone(), stype.clone()), total_limit - 1);
for other_type in &all_shift_types {
if other_type != stype {
local_limits.insert((res.id.clone(), other_type.clone()), 0);
local_thresholds.insert((res.id.clone(), other_type.clone()), 0);
}
}
if let Some(s) = global_supply.get_mut(stype) {
*s = s.saturating_sub(total_limit)
}
}
// residents with 2 available shift types
for res in self.residents.iter().filter(|r| r.allowed_types.len() == 2) {
let total_limit = *self.workload_limits.get(&res.id).unwrap_or(&0) as f32;
let per_type = (total_limit / 2.0).ceil() as u8;
for stype in &all_shift_types {
if res.allowed_types.contains(stype) {
local_limits.insert((res.id.clone(), stype.clone()), per_type);
local_thresholds.insert((res.id.clone(), stype.clone()), per_type - 1);
if let Some(s) = global_supply.get_mut(stype) {
*s = s.saturating_sub(per_type)
}
} else {
local_limits.insert((res.id.clone(), stype.clone()), 0);
local_thresholds.insert((res.id.clone(), stype.clone()), 0);
}
}
}
// residents with 3 available shift types
let generalists: Vec<&Resident> = self
.residents
.iter()
.filter(|r| r.allowed_types.len() == 3)
.collect();
if !generalists.is_empty() {
for stype in &all_shift_types {
let remaining = *global_supply.get(stype).unwrap_or(&0);
let fair_slice = (remaining as f32 / generalists.len() as f32)
.ceil()
.max(0.0) as u8;
for res in &generalists {
local_limits.insert((res.id.clone(), stype.clone()), fair_slice);
local_thresholds.insert((res.id.clone(), stype.clone()), fair_slice - 1);
}
}
}
self.shift_type_limits = local_limits;
self.shift_type_threshold = local_thresholds;
}
/// Return all possible candidates for the next slot
/// TODO: move this to another file, UserConfig should only hold the info set from GUI
///
@@ -219,6 +317,8 @@ impl UserConfig {
toxic_pairs: vec![],
workload_limits: HashMap::new(),
holiday_limits: HashMap::new(),
shift_type_limits: HashMap::new(),
shift_type_threshold: HashMap::new(),
}
}
}
@@ -284,7 +384,7 @@ mod tests {
#[rstest]
fn test_set_limits_fair_distribution() {
let mut config = UserConfig::new(1).with_residents(vec![
let mut config = UserConfig::default().with_residents(vec![
Resident::new("1", "Stefanos").with_max_shifts(2),
Resident::new("2", "Iordanis").with_max_shifts(2),
Resident::new("3", "Maria").with_reduced_load(),
@@ -296,9 +396,9 @@ mod tests {
assert_eq!(config.workload_limits["1"], 2);
assert_eq!(config.workload_limits["2"], 2);
assert_eq!(config.workload_limits["3"], 14);
assert_eq!(config.workload_limits["4"], 15);
assert_eq!(config.workload_limits["5"], 15);
assert_eq!(config.workload_limits["3"], 12);
assert_eq!(config.workload_limits["4"], 13);
assert_eq!(config.workload_limits["5"], 13);
}
#[rstest]
@@ -317,6 +417,8 @@ mod tests {
Resident::new("5", "Takis"),
],
holiday_limits: HashMap::new(),
shift_type_limits: HashMap::new(),
shift_type_threshold: HashMap::new(),
};
config.calculate_workload_limits();