|
|
|
|
|
|
|
|
use chrono::{NaiveDate, NaiveDateTime}; |
|
|
use serde::{Deserialize, Serialize}; |
|
|
use solverforge::prelude::*; |
|
|
use std::collections::HashSet; |
|
|
|
|
|
|
|
|
#[problem_fact] |
|
|
#[derive(Serialize, Deserialize)] |
|
|
pub struct Employee { |
|
|
|
|
|
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>, |
|
|
|
|
|
|
|
|
#[serde(skip)] |
|
|
pub unavailable_days: Vec<NaiveDate>, |
|
|
|
|
|
#[serde(skip)] |
|
|
pub undesired_days: Vec<NaiveDate>, |
|
|
|
|
|
#[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(), |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
#[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, |
|
|
|
|
|
#[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, |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
pub fn date(&self) -> NaiveDate { |
|
|
self.start.date() |
|
|
} |
|
|
|
|
|
|
|
|
pub fn duration_hours(&self) -> f64 { |
|
|
(self.end - self.start).num_minutes() as f64 / 60.0 |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
#[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, |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
#[inline] |
|
|
pub fn get_employee(&self, idx: usize) -> Option<&Employee> { |
|
|
self.employees.get(idx) |
|
|
} |
|
|
|
|
|
|
|
|
#[inline] |
|
|
pub fn employee_count(&self) -> usize { |
|
|
self.employees.len() |
|
|
} |
|
|
} |
|
|
|