Sync progress
This commit is contained in:
@@ -2,45 +2,65 @@
|
||||
//!
|
||||
//! here lies the schedule generator which uses a simple backtracking algorithm
|
||||
|
||||
use crate::model::{MonthlySchedule, Resident};
|
||||
use crate::model::{Configurations, Day, MonthlySchedule, Slot};
|
||||
|
||||
// schedule param contains the schedule with maybe some manually input days
|
||||
// returns the complete schedule after applying all rules/restrictions/fairness
|
||||
fn generate(schedule: &MonthlySchedule) -> MonthlySchedule {
|
||||
todo!()
|
||||
}
|
||||
|
||||
// make part of Schedule
|
||||
// examines Schedule for validity, if no then backtrack
|
||||
fn is_state_valid() {
|
||||
todo!()
|
||||
}
|
||||
|
||||
// From https://en.wikipedia.org/wiki/Backtracking
|
||||
// Recursively fill a partially filled MonthlySchedule until shifts are set for all days of the month
|
||||
// Collection of Resident is immutable and only there for reference
|
||||
// TODO: we can create a struct `Generator` that contains the collection of Resident upon initialization and then call self.people inside self.backtracking
|
||||
// Returns error if there is no solution for the specific set of constaints
|
||||
fn backtracking(schedule: &mut MonthlySchedule, people: &Vec<Resident>, mut day: usize) {
|
||||
if schedule.restrictions_violated() {
|
||||
log::info!("restrictions_violated due to...");
|
||||
return;
|
||||
/// https://en.wikipedia.org/wiki/Backtracking
|
||||
/// https://en.wikipedia.org/wiki/Nurse_scheduling_problem
|
||||
///
|
||||
/// DFS where maximum depth is calculated by total_days_of_month + odd_days_of_month (open shifts)
|
||||
/// Starts with schedule partially completed from the user interface
|
||||
/// Ends with a full schedule following restrictions and fairness
|
||||
pub fn backtracking(schedule: &mut MonthlySchedule, slot: Slot, config: &Configurations) -> bool {
|
||||
// check if schedule was fully filled in the previous iteration
|
||||
if slot.out_of_range(config.total_days()) {
|
||||
log::info!("Solution found, exiting recursive algorithm");
|
||||
return true;
|
||||
}
|
||||
|
||||
if schedule.is_filled() {
|
||||
log::info!("..?");
|
||||
return;
|
||||
// TODO:check if slot is already assigned with manual shifts
|
||||
|
||||
// check if any of the rules is violated before continuing
|
||||
if schedule.restrictions_violated(&slot) {
|
||||
// log::info!("Cutting branch due to restriction violation");
|
||||
return false;
|
||||
}
|
||||
|
||||
for resident in people {}
|
||||
// get all candidate options for current step/slot/depth
|
||||
let candidates = config.candidates(slot, schedule);
|
||||
|
||||
todo!()
|
||||
// TODO: sort the candidates by workload left in the month helping the algorithm
|
||||
|
||||
for (resident, shift_type) in candidates {
|
||||
schedule.insert(slot, resident, shift_type);
|
||||
|
||||
if backtracking(schedule, slot.increment(), config) {
|
||||
log::info!("Solution found...");
|
||||
return true;
|
||||
}
|
||||
|
||||
schedule.remove(slot, resident, shift_type);
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rstest::rstest;
|
||||
|
||||
use crate::{
|
||||
generator::backtracking,
|
||||
model::{Configurations, MonthlySchedule, Slot},
|
||||
};
|
||||
|
||||
#[rstest]
|
||||
pub fn xxxt() {}
|
||||
fn test_backtracking() {
|
||||
const JAN: usize = 0;
|
||||
let config = Configurations::new(JAN);
|
||||
let mut schedule = MonthlySchedule::new();
|
||||
|
||||
backtracking(&mut schedule, Slot::default(), &config);
|
||||
|
||||
// assert!(schedule.is_filled(config.total_days()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use log::info;
|
||||
|
||||
use crate::{
|
||||
export::{Export, FileType},
|
||||
model::{MonthlySchedule, Resident},
|
||||
generator::backtracking,
|
||||
model::{Configurations, Day, MonthlySchedule, Resident, Slot},
|
||||
};
|
||||
|
||||
mod export;
|
||||
@@ -24,9 +27,31 @@ fn add_resident(resident: Resident) -> String {
|
||||
format!("{:?}", resident)
|
||||
}
|
||||
|
||||
/// argument to this must be the rota state including all
|
||||
/// information for the residents, the forbidden pairs the holidays
|
||||
/// and the period of the schedule
|
||||
#[tauri::command]
|
||||
fn generate() -> String {
|
||||
"Generating new rota".to_string()
|
||||
const JAN: usize = 0;
|
||||
const DAYS_IN_JAN: usize = 31;
|
||||
let config = Configurations::new(JAN);
|
||||
let mut schedule = MonthlySchedule::new();
|
||||
|
||||
// find total number of slots MEANING
|
||||
|
||||
//TODO: return a result instead of ()
|
||||
backtracking(&mut schedule, Slot::default(), &config);
|
||||
|
||||
info!(
|
||||
"Generated rota for month {}, final schedule {:?}",
|
||||
JAN, schedule
|
||||
);
|
||||
|
||||
//TODO: this should in fact return a JSON of the schedule instance solution
|
||||
format!(
|
||||
"Generated rota for month {}, final schedule {:?}",
|
||||
JAN, schedule
|
||||
)
|
||||
}
|
||||
|
||||
// takes the list of active residents and their configurations
|
||||
@@ -42,7 +67,7 @@ fn possible_swap_locations(people: Vec<Resident>) -> Vec<Resident> {
|
||||
fn export() -> String {
|
||||
// param must have filetype as string from svelte
|
||||
// somehow get the current schedule
|
||||
let rota = MonthlySchedule::new(10);
|
||||
let rota = MonthlySchedule::new();
|
||||
let _ = rota.export(FileType::Json);
|
||||
|
||||
// locally store the _?
|
||||
|
||||
@@ -2,28 +2,51 @@ use std::collections::HashMap;
|
||||
|
||||
use chrono::Month;
|
||||
use itertools::Itertools;
|
||||
use log::warn;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// We're always talking about entities that are created for the single month of the rota generation
|
||||
// here add all info that we will need when we unmake the move after recursing...
|
||||
pub struct CalendarState {
|
||||
// here make unmake stuff
|
||||
}
|
||||
|
||||
pub struct Restriction {}
|
||||
|
||||
pub struct Rule {}
|
||||
|
||||
const YEAR: i32 = 2026;
|
||||
|
||||
pub struct Configurations {
|
||||
month: Month,
|
||||
holidays: Vec<usize>,
|
||||
residents: Vec<Resident>,
|
||||
}
|
||||
|
||||
impl Configurations {
|
||||
pub fn new(month: usize) -> Self {
|
||||
Self {
|
||||
month: Month::try_from(month as u8).unwrap(),
|
||||
holidays: vec![],
|
||||
residents: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default() -> Self {
|
||||
Self {
|
||||
month: Month::try_from(0).unwrap(),
|
||||
holidays: vec![],
|
||||
residents: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add(&mut self, resident: Resident) {
|
||||
self.residents.push(resident);
|
||||
}
|
||||
|
||||
pub fn total_days(&self) -> u8 {
|
||||
self.month.num_days(YEAR).unwrap()
|
||||
}
|
||||
|
||||
// open shfits -> 2 people per night
|
||||
pub fn total_open_shifts(&self) -> u8 {
|
||||
let mut total_open_shifts = 0;
|
||||
for i in 1..=self.total_days() {
|
||||
@@ -34,31 +57,170 @@ impl Configurations {
|
||||
total_open_shifts
|
||||
}
|
||||
|
||||
// closed shifts -> 1 resident per night
|
||||
pub fn total_closed_shifts(&self) -> u8 {
|
||||
self.total_days() - self.total_open_shifts()
|
||||
}
|
||||
|
||||
/// Return all possible candidates for the next slot
|
||||
pub fn candidates(
|
||||
&self,
|
||||
slot: Slot,
|
||||
schedule: &MonthlySchedule,
|
||||
) -> Vec<(&Resident, &ShiftType)> {
|
||||
let mut candidates = vec![];
|
||||
let is_open = slot.is_open_shift();
|
||||
|
||||
for resident in &self.residents {
|
||||
if resident.negative_shifts.contains(&slot.day) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if schedule.is_already_assigned_in_slot(&slot) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for shift_type in &resident.allowed_types {
|
||||
match shift_type {
|
||||
ShiftType::OpenFirst => {
|
||||
if is_open {
|
||||
candidates.push((resident, &ShiftType::OpenFirst))
|
||||
}
|
||||
}
|
||||
ShiftType::OpenSecond => {
|
||||
if is_open {
|
||||
candidates.push((resident, &ShiftType::OpenSecond))
|
||||
}
|
||||
}
|
||||
ShiftType::Closed => {
|
||||
if !is_open {
|
||||
candidates.push((resident, &ShiftType::Closed))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
candidates
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MonthlySchedule {
|
||||
rotation: Vec<Shift>,
|
||||
#[derive(Serialize, Deserialize, PartialEq, PartialOrd, Ord, Eq, Debug, Hash, Clone, Copy)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Slot {
|
||||
day: Day,
|
||||
index: usize,
|
||||
}
|
||||
|
||||
impl Slot {
|
||||
pub fn new(day: Day, index: usize) -> Self {
|
||||
Self { day, index }
|
||||
}
|
||||
|
||||
pub fn is_open_shift(&self) -> bool {
|
||||
self.day.is_open_shift()
|
||||
}
|
||||
|
||||
pub fn increment(&self) -> Self {
|
||||
if self.index == 0 {
|
||||
Self {
|
||||
day: self.day,
|
||||
index: 1,
|
||||
}
|
||||
} else {
|
||||
Self {
|
||||
day: self.day.increment(),
|
||||
index: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn out_of_range(&self, limit: u8) -> bool {
|
||||
self.day.0 > limit
|
||||
}
|
||||
|
||||
pub fn default() -> Self {
|
||||
Self {
|
||||
day: Day(0),
|
||||
index: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MonthlySchedule(HashMap<Slot, Shift>);
|
||||
|
||||
impl MonthlySchedule {
|
||||
pub fn new(days_of_month: usize) -> Self {
|
||||
Self { rotation: vec![] }
|
||||
pub fn new() -> Self {
|
||||
Self(HashMap::new())
|
||||
}
|
||||
|
||||
pub fn restrictions_violated(&self) -> bool {
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub fn is_filled(&self) -> bool {
|
||||
pub fn get_resident(&self, slot: &Slot) -> Option<String> {
|
||||
let shift = self.0.get(slot);
|
||||
|
||||
if let Some(shift) = shift {
|
||||
if slot.index == 0 {
|
||||
return shift.resident_1_id.clone();
|
||||
} else {
|
||||
return shift.resident_1_id.clone();
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, slot: Slot, resident: &Resident, shift_type: &ShiftType) {
|
||||
match self.0.get_mut(&slot) {
|
||||
Some(shift) => {
|
||||
warn!("one shift already set for today");
|
||||
|
||||
if shift.resident_2_id.is_none() && slot.is_open_shift() {
|
||||
shift.resident_2_id = Some(resident.id.clone())
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let new_shift = Shift::new(resident.id.clone());
|
||||
self.0.insert(slot, new_shift);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, slot: Slot, resident: &Resident, shift_type: &ShiftType) {
|
||||
if let Some(shift) = self.0.get_mut(&slot) {
|
||||
if let Some(res_1_id) = &shift.resident_1_id {
|
||||
if &resident.id == res_1_id {
|
||||
shift.resident_1_id = None;
|
||||
}
|
||||
} else if let Some(res_1_id) = &shift.resident_2_id {
|
||||
if &resident.id == res_1_id {
|
||||
shift.resident_1_id = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_already_assigned_in_slot(&self, slot: &Slot) -> bool {
|
||||
if let Some(shift) = self.0.get(slot) {
|
||||
if shift.resident_1_id.is_some() || shift.resident_2_id.is_some() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// here we check for the validity of all environment restrictions
|
||||
/// if any is violated we return true (leading to pruning in the backtracking algorithm)
|
||||
pub fn restrictions_violated(&self, slot: &Slot) -> bool {
|
||||
// self.contains_any_consecutive_shifts()
|
||||
|
||||
// see if the spot in the self calendar for the slot has the same person like the previous?
|
||||
// this would take out both back to back days, but also same day 2 times the same resident
|
||||
let resident = self.get_resident(slot);
|
||||
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// return true if
|
||||
pub fn contains_any_consecutive_shifts(&self) -> bool {
|
||||
// self.rotation
|
||||
// self.0
|
||||
// .into_iter()
|
||||
// .sorted_unstable_by_key(|shift| shift.day)
|
||||
// .tuple_windows()
|
||||
@@ -83,18 +245,20 @@ impl MonthlySchedule {
|
||||
// });
|
||||
todo!()
|
||||
}
|
||||
|
||||
// pub fn
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, PartialOrd, Ord, Eq, Debug)]
|
||||
#[derive(Serialize, Deserialize, PartialEq, PartialOrd, Ord, Eq, Debug, Hash, Clone, Copy)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
||||
pub struct Day(usize);
|
||||
pub struct Day(pub u8);
|
||||
|
||||
impl Day {
|
||||
pub fn is_open_shift(&self) -> bool {
|
||||
self.0.is_multiple_of(2)
|
||||
!self.0.is_multiple_of(2)
|
||||
}
|
||||
|
||||
pub fn increment(&self) -> Self {
|
||||
Self(self.0 + 1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,13 +301,40 @@ impl WeeklySchedule {
|
||||
// todo
|
||||
}
|
||||
|
||||
// this is agnostic to what specific types the resident shave
|
||||
// we cant add here shift_Type except if we just do OPEN/CLOSED.
|
||||
// also there's a discussion to be had regardng the Option of resident 1 id since a shift should always have one..
|
||||
// (But due to the backtracking algorithm maybe its ok to work without any residents in a shift? cauise we add/remove all the time)
|
||||
#[derive(Debug)]
|
||||
pub struct Shift {
|
||||
day: Day,
|
||||
resident_a: Resident,
|
||||
resident_b: Option<Resident>,
|
||||
resident_1_id: Option<String>,
|
||||
// should be filled in odd days, in other words in open shifts (OpenFirst, OpenSecond)
|
||||
resident_2_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
impl Shift {
|
||||
pub fn new_empty() -> Self {
|
||||
Self {
|
||||
resident_1_id: None,
|
||||
resident_2_id: None,
|
||||
}
|
||||
}
|
||||
pub fn new(resident_1_id: String) -> Self {
|
||||
Self {
|
||||
resident_1_id: Some(resident_1_id),
|
||||
resident_2_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_open(resident_1_id: String, resident_2_id: String) -> Self {
|
||||
Self {
|
||||
resident_1_id: Some(resident_1_id),
|
||||
resident_2_id: Some(resident_2_id),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
|
||||
pub enum ShiftType {
|
||||
Closed,
|
||||
OpenFirst,
|
||||
|
||||
Reference in New Issue
Block a user