use anyhow::Context; use chrono::Month; use serde::{Deserialize, Serialize}; use crate::{ resident::{Resident, ResidentDTO, ResidentId}, slot::Day, }; const MONTH: u8 = 2; const YEAR: i32 = 2026; #[derive(Debug, Clone)] pub struct ToxicPair((ResidentId, ResidentId)); impl ToxicPair { pub fn new(res_id_1: u8, res_id_2: u8) -> Self { Self((ResidentId(res_id_1), ResidentId(res_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, residents: Vec, toxic_pairs: Vec<(u8, u8)>, } #[derive(Debug, Clone)] pub struct UserConfig { pub month: Month, pub year: i32, pub holidays: Vec, pub residents: Vec, pub toxic_pairs: Vec, pub total_days: u8, pub total_slots: u8, pub total_holiday_slots: u8, } impl UserConfig { pub fn with_holidays(mut self, holidays: Vec) -> Self { self.holidays = holidays; self.total_holiday_slots = self.total_holiday_slots(); self } pub fn with_residents(mut self, residents: Vec) -> 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) -> Self { self.toxic_pairs = toxic_pairs; self } fn total_holiday_slots(&self) -> u8 { (1..=self.total_days) .filter(|&d| self.is_holiday_or_weekend_slot(d)) .map(|d| if Day(d).is_open_shift() { 2 } else { 1 }) .sum() } pub fn is_holiday_or_weekend_slot(&self, day: u8) -> bool { let day = Day(day); day.is_weekend(self.month.number_from_month(), self.year) || self.holidays.contains(&(day.0)) } } 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 for UserConfig { type Error = anyhow::Error; fn try_from(value: UserConfigDTO) -> Result { 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, }) } }