File size: 5,309 Bytes
195a426 |
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 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 |
//! Domain model for Employee Scheduling Problem.
use chrono::{NaiveDate, NaiveDateTime};
use serde::{Deserialize, Serialize};
use solverforge::prelude::*;
use std::collections::HashSet;
/// An employee who can be assigned to shifts.
#[problem_fact]
#[derive(Serialize, Deserialize)]
pub struct Employee {
/// Index of this employee in `EmployeeSchedule.employees` for O(1) join matching.
pub index: usize,
pub name: String,
pub skills: HashSet<String>,
#[serde(rename = "unavailableDates", default)]
pub unavailable_dates: HashSet<NaiveDate>,
#[serde(rename = "undesiredDates", default)]
pub undesired_dates: HashSet<NaiveDate>,
#[serde(rename = "desiredDates", default)]
pub desired_dates: HashSet<NaiveDate>,
/// Sorted unavailable dates for `flatten_last` compatibility.
/// Populated by `finalize()` from `unavailable_dates` HashSet.
#[serde(skip)]
pub unavailable_days: Vec<NaiveDate>,
/// Sorted undesired dates for `flatten_last` compatibility.
#[serde(skip)]
pub undesired_days: Vec<NaiveDate>,
/// Sorted desired dates for `flatten_last` compatibility.
#[serde(skip)]
pub desired_days: Vec<NaiveDate>,
}
impl Employee {
pub fn new(index: usize, name: impl Into<String>) -> Self {
Self {
index,
name: name.into(),
skills: HashSet::new(),
unavailable_dates: HashSet::new(),
undesired_dates: HashSet::new(),
desired_dates: HashSet::new(),
unavailable_days: Vec::new(),
undesired_days: Vec::new(),
desired_days: Vec::new(),
}
}
/// Populates derived Vec fields from HashSets for zero-erasure stream compatibility.
/// Must be called after all dates have been added to HashSets.
pub fn finalize(&mut self) {
self.unavailable_days = self.unavailable_dates.iter().copied().collect();
self.unavailable_days.sort();
self.undesired_days = self.undesired_dates.iter().copied().collect();
self.undesired_days.sort();
self.desired_days = self.desired_dates.iter().copied().collect();
self.desired_days.sort();
}
pub fn with_skill(mut self, skill: impl Into<String>) -> Self {
self.skills.insert(skill.into());
self
}
pub fn with_skills(mut self, skills: impl IntoIterator<Item = impl Into<String>>) -> Self {
for skill in skills {
self.skills.insert(skill.into());
}
self
}
pub fn with_unavailable_date(mut self, date: NaiveDate) -> Self {
self.unavailable_dates.insert(date);
self
}
pub fn with_undesired_date(mut self, date: NaiveDate) -> Self {
self.undesired_dates.insert(date);
self
}
pub fn with_desired_date(mut self, date: NaiveDate) -> Self {
self.desired_dates.insert(date);
self
}
}
/// A shift that needs to be staffed by an employee.
#[planning_entity]
#[derive(Serialize, Deserialize)]
pub struct Shift {
#[planning_id]
pub id: String,
pub start: NaiveDateTime,
pub end: NaiveDateTime,
pub location: String,
#[serde(rename = "requiredSkill")]
pub required_skill: String,
/// Index into `EmployeeSchedule.employees` (O(1) lookup, no String cloning).
#[planning_variable(allows_unassigned = true)]
pub employee_idx: Option<usize>,
}
impl Shift {
pub fn new(
id: impl Into<String>,
start: NaiveDateTime,
end: NaiveDateTime,
location: impl Into<String>,
required_skill: impl Into<String>,
) -> Self {
Self {
id: id.into(),
start,
end,
location: location.into(),
required_skill: required_skill.into(),
employee_idx: None,
}
}
/// Returns the date of the shift start.
pub fn date(&self) -> NaiveDate {
self.start.date()
}
/// Returns the duration in hours.
pub fn duration_hours(&self) -> f64 {
(self.end - self.start).num_minutes() as f64 / 60.0
}
}
/// The employee scheduling solution.
#[planning_solution]
#[basic_variable_config(
entity_collection = "shifts",
variable_field = "employee_idx",
variable_type = "usize",
value_range = "employees"
)]
#[solverforge_constraints_path = "crate::constraints::create_fluent_constraints"]
#[derive(Serialize, Deserialize)]
pub struct EmployeeSchedule {
#[problem_fact_collection]
pub employees: Vec<Employee>,
#[planning_entity_collection]
pub shifts: Vec<Shift>,
#[planning_score]
pub score: Option<HardSoftDecimalScore>,
#[serde(rename = "solverStatus", skip_serializing_if = "Option::is_none")]
pub solver_status: Option<String>,
}
impl EmployeeSchedule {
pub fn new(employees: Vec<Employee>, shifts: Vec<Shift>) -> Self {
Self {
employees,
shifts,
score: None,
solver_status: None,
}
}
/// Gets an Employee by index (O(1)).
#[inline]
pub fn get_employee(&self, idx: usize) -> Option<&Employee> {
self.employees.get(idx)
}
/// Returns the number of employees.
#[inline]
pub fn employee_count(&self) -> usize {
self.employees.len()
}
}
|