Add fairness for each shift type count
This commit is contained in:
@@ -1,37 +1,18 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::{ser::SerializeMap, Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{
|
||||
config::UserConfig,
|
||||
resident::Resident,
|
||||
slot::{Day, ShiftPosition, Slot},
|
||||
slot::{weekday_to_greek, Day, ShiftPosition, Slot},
|
||||
};
|
||||
|
||||
use serde::Serializer;
|
||||
|
||||
impl Serialize for MonthlySchedule {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
use serde::ser::SerializeMap;
|
||||
let mut map = serializer.serialize_map(Some(self.0.len()))?;
|
||||
for (slot, name) in &self.0 {
|
||||
let pos_str = match slot.position {
|
||||
ShiftPosition::First => "First",
|
||||
ShiftPosition::Second => "Second",
|
||||
};
|
||||
let key = format!("{}-{}", slot.day.0, pos_str);
|
||||
map.serialize_entry(&key, name)?;
|
||||
}
|
||||
map.end()
|
||||
}
|
||||
}
|
||||
|
||||
// each slot has one resident
|
||||
// a day can span between 1 or 2 slots depending on if it is open(odd) or closed(even)
|
||||
/// each slot has one resident
|
||||
/// a day can span between 1 or 2 slots depending on if it is open(odd) or closed(even)
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct MonthlySchedule(HashMap<Slot, String>);
|
||||
pub struct MonthlySchedule(pub HashMap<Slot, String>);
|
||||
|
||||
impl MonthlySchedule {
|
||||
pub fn new() -> Self {
|
||||
@@ -66,6 +47,51 @@ impl MonthlySchedule {
|
||||
.count()
|
||||
}
|
||||
|
||||
pub fn count_shifts(&self, resident_id: &str, shift_type: Option<ShiftType>) -> usize {
|
||||
self.0
|
||||
.iter()
|
||||
.filter(|(slot, id)| {
|
||||
if *id != resident_id {
|
||||
return false;
|
||||
}
|
||||
|
||||
match &shift_type {
|
||||
None => true,
|
||||
Some(target) => {
|
||||
let actual_type = if slot.is_open_shift() {
|
||||
match slot.position {
|
||||
ShiftPosition::First => ShiftType::OpenFirst,
|
||||
ShiftPosition::Second => ShiftType::OpenSecond,
|
||||
}
|
||||
} else {
|
||||
ShiftType::Closed
|
||||
};
|
||||
actual_type == *target
|
||||
}
|
||||
}
|
||||
})
|
||||
.count()
|
||||
}
|
||||
|
||||
pub fn is_per_shift_threshold_met(&self, config: &UserConfig) -> bool {
|
||||
for res in &config.residents {
|
||||
for stype in [
|
||||
ShiftType::OpenFirst,
|
||||
ShiftType::OpenSecond,
|
||||
ShiftType::Closed,
|
||||
] {
|
||||
let count = self.count_shifts(&res.id, Some(stype.clone()));
|
||||
if let Some(&threshold) = config.shift_type_threshold.get(&(res.id.clone(), stype))
|
||||
{
|
||||
if count < threshold as usize {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, slot: Slot, resident: &Resident) {
|
||||
self.0.insert(slot, resident.id.clone());
|
||||
}
|
||||
@@ -90,6 +116,7 @@ impl MonthlySchedule {
|
||||
|| self.has_toxic_pair(slot, config)
|
||||
|| self.is_workload_unbalanced(slot, config)
|
||||
|| self.is_holiday_workload_imbalanced(slot, config)
|
||||
|| self.is_shift_type_distribution_unfair(slot, config)
|
||||
}
|
||||
|
||||
/// same_resident_in_consecutive_days
|
||||
@@ -98,20 +125,24 @@ impl MonthlySchedule {
|
||||
return false;
|
||||
}
|
||||
|
||||
let previous_slot = if slot.is_open_second() {
|
||||
slot.previous().previous()
|
||||
let previous_slots = if slot.is_open_second() {
|
||||
vec![slot.previous().previous()]
|
||||
} else if slot.is_open_first() {
|
||||
vec![slot.previous()]
|
||||
} else {
|
||||
slot.previous()
|
||||
// if current shift is closed, we need to check both residents in the previous day
|
||||
vec![slot.previous(), slot.previous().previous()]
|
||||
};
|
||||
|
||||
self.get_resident_id(&previous_slot) == self.get_resident_id(slot)
|
||||
previous_slots
|
||||
.iter()
|
||||
.any(|s| self.get_resident_id(s) == self.get_resident_id(slot))
|
||||
}
|
||||
|
||||
/// has_toxic_pair
|
||||
pub fn has_toxic_pair(&self, slot: &Slot, config: &UserConfig) -> bool {
|
||||
// if it is not an open shift and we only just added the first person in an open shift
|
||||
// then we couldn't have just created a new toxic pair
|
||||
if !slot.is_open_shift() || !slot.is_open_second() {
|
||||
// can only have caused a toxic pair violation if we just added a 2nd resident in an open shift
|
||||
if !slot.is_open_second() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -177,11 +208,39 @@ impl MonthlySchedule {
|
||||
false
|
||||
}
|
||||
|
||||
/// is_shift_type_distribution_unfair
|
||||
pub fn is_shift_type_distribution_unfair(&self, slot: &Slot, config: &UserConfig) -> bool {
|
||||
let resident_id = match self.get_resident_id(slot) {
|
||||
Some(id) => id,
|
||||
None => return false,
|
||||
};
|
||||
|
||||
let current_shift_type = if slot.is_open_shift() {
|
||||
match slot.position {
|
||||
ShiftPosition::First => ShiftType::OpenFirst,
|
||||
ShiftPosition::Second => ShiftType::OpenSecond,
|
||||
}
|
||||
} else {
|
||||
ShiftType::Closed
|
||||
};
|
||||
|
||||
let current_count = self.count_shifts(resident_id, Some(current_shift_type.clone()));
|
||||
|
||||
if let Some(&limit) = config
|
||||
.shift_type_limits
|
||||
.get(&(resident_id.clone(), current_shift_type.clone()))
|
||||
{
|
||||
return current_count > limit as usize;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub fn pretty_print(&self, config: &UserConfig) -> String {
|
||||
let mut sorted: Vec<_> = self.0.iter().collect();
|
||||
sorted.sort_by_key(|(slot, _)| (slot.day, slot.position));
|
||||
|
||||
let mut output = String::from("MonthlySchedule {\n");
|
||||
let mut output = String::from("Μηνιαίο Πρόγραμμα Εφημεριών\n");
|
||||
for (slot, res_id) in sorted {
|
||||
let res_name = config
|
||||
.residents
|
||||
@@ -191,16 +250,70 @@ impl MonthlySchedule {
|
||||
.unwrap();
|
||||
|
||||
output.push_str(&format!(
|
||||
"Day {:2} ({:?}): {},\n",
|
||||
slot.day.0, slot.position, res_name
|
||||
"Ημέρα {:2} - {:9} - {:11}: {},\n",
|
||||
slot.day.0,
|
||||
weekday_to_greek(
|
||||
slot.day
|
||||
.weekday(config.month.number_from_month(), config.year)
|
||||
),
|
||||
slot.shift_type_str(),
|
||||
res_name
|
||||
));
|
||||
}
|
||||
output.push('}');
|
||||
output
|
||||
}
|
||||
|
||||
pub fn report(&self, config: &UserConfig) -> String {
|
||||
let mut output = String::new();
|
||||
output.push_str("\n--- Αναφορά ---\n");
|
||||
// Using standard widths for Greek characters and alignment
|
||||
output.push_str(&format!(
|
||||
"{:<15} | {:<6} | {:<10} | {:<10} | {:<7} | {:<10}\n",
|
||||
"Ειδικευόμενος", "Σύνολο", "Ανοιχτή(1)", "Ανοιχτή(2)", "Κλειστή", "ΣΚ/Αργίες"
|
||||
));
|
||||
output.push_str("-".repeat(75).as_str());
|
||||
output.push('\n');
|
||||
|
||||
let mut residents: Vec<_> = config.residents.iter().collect();
|
||||
residents.sort_by_key(|r| &r.name);
|
||||
|
||||
for res in residents {
|
||||
let total = self.current_workload(res);
|
||||
let o1 = self.count_shifts(&res.id, Some(ShiftType::OpenFirst));
|
||||
let o2 = self.count_shifts(&res.id, Some(ShiftType::OpenSecond));
|
||||
let cl = self.count_shifts(&res.id, Some(ShiftType::Closed));
|
||||
let sun = self.current_holiday_workload(res, config);
|
||||
|
||||
output.push_str(&format!(
|
||||
"{:<15} | {:<6} | {:<10} | {:<10} | {:<7} | {:<10}\n",
|
||||
res.name, total, o1, o2, cl, sun
|
||||
));
|
||||
}
|
||||
output.push_str("-".repeat(75).as_str());
|
||||
output.push('\n');
|
||||
output
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
impl Serialize for MonthlySchedule {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut map = serializer.serialize_map(Some(self.0.len()))?;
|
||||
for (slot, name) in &self.0 {
|
||||
let pos_str = match slot.position {
|
||||
ShiftPosition::First => "First",
|
||||
ShiftPosition::Second => "Second",
|
||||
};
|
||||
let key = format!("{}-{}", slot.day.0, pos_str);
|
||||
map.serialize_entry(&key, name)?;
|
||||
}
|
||||
map.end()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum ShiftType {
|
||||
Closed,
|
||||
OpenFirst,
|
||||
|
||||
Reference in New Issue
Block a user