Files
rota/src-tauri/src/config.rs

278 lines
7.9 KiB
Rust

use std::collections::HashMap;
use anyhow::Context;
use chrono::Month;
use serde::{Deserialize, Serialize};
use crate::{
resident::{Resident, ResidentDTO, ResidentId},
schedule::ShiftType,
slot::{Day, Slot},
};
const MONTH: u8 = 4;
const YEAR: i32 = 2026;
#[derive(Debug, Clone)]
pub struct ToxicPair((ResidentId, ResidentId));
impl ToxicPair {
pub fn new(r_id_1: u8, r_id_2: u8) -> Self {
Self((ResidentId(r_id_1), ResidentId(r_id_2)))
}
pub fn matches(&self, other: &ToxicPair) -> bool {
let p1 = &self.0;
let p2 = &other.0;
(p1.0 == p2.0 && p1.1 == p2.1) || (p1.0 == p2.1 && p1.1 == p2.0)
}
}
impl From<(ResidentId, ResidentId)> for ToxicPair {
fn from(value: (ResidentId, ResidentId)) -> Self {
Self((value.0, value.1))
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct UserConfigDTO {
month: u8,
year: i32,
holidays: Vec<u8>,
residents: Vec<ResidentDTO>,
toxic_pairs: Vec<(u8, u8)>,
}
#[derive(Debug, Clone)]
pub struct UserConfig {
pub month: Month,
pub year: i32,
pub holidays: Vec<u8>,
pub residents: Vec<Resident>,
pub toxic_pairs: Vec<ToxicPair>,
pub total_days: u8,
pub total_slots: u8,
pub total_holiday_slots: u8,
}
impl UserConfig {
pub fn new(month: u8, year: i32) -> Self {
let month = Month::try_from(month).unwrap();
let total_days = month.num_days(year).unwrap();
let total_slots = (1..=total_days)
.map(|d| if Day(d).is_open_shift() { 2 } else { 1 })
.sum();
let total_holiday_slots = (1..=total_days)
.filter(|&d| Day(d).is_weekend(month.number_from_month(), year))
.map(|d| if Day(d).is_open_shift() { 2 } else { 1 })
.sum();
Self {
month,
year,
holidays: vec![],
residents: vec![],
toxic_pairs: vec![],
total_days,
total_slots,
total_holiday_slots,
}
}
pub fn with_holidays(mut self, holidays: Vec<u8>) -> Self {
self.holidays = holidays;
self.total_holiday_slots = self.total_holiday_slots();
self
}
pub fn with_residents(mut self, residents: Vec<Resident>) -> Self {
self.residents = residents;
self
}
pub fn add(&mut self, resident: Resident) {
self.residents.push(resident);
}
pub fn with_toxic_pairs(mut self, toxic_pairs: Vec<ToxicPair>) -> Self {
self.toxic_pairs = toxic_pairs;
self
}
pub fn update_month(&mut self, month: u8) {
self.month = Month::try_from(month).unwrap();
self.total_days = self.month.num_days(self.year).unwrap();
self.total_slots = (1..=self.total_days)
.map(|d| if Day(d).is_open_shift() { 2 } else { 1 })
.sum();
self.total_holiday_slots = self.total_holiday_slots()
}
fn total_holiday_slots(&self) -> u8 {
(1..=self.total_days)
.filter(|&d| self.is_holiday_or_weekend(Day(d)))
.map(|d| if Day(d).is_open_shift() { 2 } else { 1 })
.sum()
}
pub fn is_holiday_or_weekend(&self, day: Day) -> bool {
let month = self.month.number_from_month();
day.is_weekend(month, self.year) || self.holidays.contains(&(day.0))
}
pub fn is_holiday_or_weekend_slot(&self, slot: Slot) -> bool {
self.is_holiday_or_weekend(slot.day)
}
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
}
pub fn flexibility_map(&self) -> HashMap<ResidentId, u8> {
let mut map = HashMap::new();
for r in &self.residents {
map.insert(r.id, r.allowed_types.len() as u8);
}
map
}
}
impl Default for UserConfig {
fn default() -> Self {
let month = Month::try_from(MONTH).unwrap();
let total_days = month.num_days(YEAR).unwrap();
let total_slots = (1..=total_days)
.map(|d| if Day(d).is_open_shift() { 2 } else { 1 })
.sum();
let total_holiday_slots = (1..=total_days)
.filter(|&d| Day(d).is_weekend(month.number_from_month(), YEAR))
.map(|d| if Day(d).is_open_shift() { 2 } else { 1 })
.sum();
Self {
month,
year: YEAR,
holidays: vec![],
residents: vec![],
toxic_pairs: vec![],
total_days,
total_slots,
total_holiday_slots,
}
}
}
impl TryFrom<UserConfigDTO> for UserConfig {
type Error = anyhow::Error;
fn try_from(value: UserConfigDTO) -> Result<Self, Self::Error> {
let month = Month::try_from(value.month)?;
let total_days = month.num_days(value.year).context("Failed to parse")?;
let total_slots = (1..=total_days)
.map(|d| if Day(d).is_open_shift() { 2 } else { 1 })
.sum();
let total_holiday_slots = (1..=total_days)
.filter(|&d| {
Day(d).is_weekend(month.number_from_month(), value.year)
|| value.holidays.contains(&d)
})
.map(|d| if Day(d).is_open_shift() { 2 } else { 1 })
.sum();
Ok(Self {
month,
year: value.year,
holidays: value.holidays,
residents: value.residents.into_iter().map(Resident::from).collect(),
toxic_pairs: value
.toxic_pairs
.into_iter()
.map(|p| ToxicPair::new(p.0, p.1))
.collect(),
total_days,
total_slots,
total_holiday_slots,
})
}
}
#[cfg(test)]
mod tests {
use crate::{
config::{ToxicPair, UserConfig},
fixtures::complex_config,
schedule::ShiftType,
slot::{Day, ShiftPosition, Slot},
};
use rstest::rstest;
#[rstest]
fn test_get_initial_supply(complex_config: UserConfig) {
let supply = complex_config.get_initial_supply();
assert_eq!(15, *supply.get(&ShiftType::OpenFirst).unwrap());
assert_eq!(15, *supply.get(&ShiftType::OpenSecond).unwrap());
assert_eq!(15, *supply.get(&ShiftType::Closed).unwrap());
}
#[rstest]
fn test_is_holiday_or_weekend(complex_config: UserConfig) {
assert!(!complex_config.is_holiday_or_weekend(Day(1)));
assert!(complex_config.is_holiday_or_weekend(Day(4)));
assert!(complex_config.is_holiday_or_weekend(Day(5)));
assert!(complex_config.is_holiday_or_weekend(Day(10)));
}
#[rstest]
fn test_is_holiday_or_weekend_slot(complex_config: UserConfig) {
let weekday = Slot::new(Day(1), ShiftPosition::First);
let sat = Slot::new(Day(4), ShiftPosition::First);
let sun = Slot::new(Day(5), ShiftPosition::First);
let manual_holiday = Slot::new(Day(10), ShiftPosition::First);
assert!(!complex_config.is_holiday_or_weekend_slot(weekday));
assert!(complex_config.is_holiday_or_weekend_slot(sat));
assert!(complex_config.is_holiday_or_weekend_slot(sun));
assert!(complex_config.is_holiday_or_weekend_slot(manual_holiday));
}
#[rstest]
fn test_toxic_pair_matches() {
let pair_1 = ToxicPair::new(1, 2);
let pair_1_r = ToxicPair::new(2, 1);
let pair_2 = ToxicPair::new(3, 1);
assert!(pair_1.matches(&pair_1_r));
assert!(pair_1_r.matches(&pair_1));
assert!(!pair_1.matches(&pair_2));
assert!(!pair_2.matches(&pair_1));
assert!(!pair_1_r.matches(&pair_2));
assert!(!pair_2.matches(&pair_1_r));
}
}