use chrono::{Datelike, Duration, NaiveDate, Weekday};
use serde::{Deserialize, Serialize};
use std::{collections::HashSet, str::FromStr};
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct WorkCalendar {
work_days: HashSet<Weekday>,
holidays: HashSet<NaiveDate>,
}
impl FromStr for WorkCalendar {
type Err = Box<dyn std::error::Error>;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let config: WorkCalendarConfig = if s.trim_start().starts_with('{') {
serde_json::from_str(s)?
} else {
serde_yaml::from_str(s)?
};
Ok(Self::from(config))
}
}
impl WorkCalendar {
pub fn new() -> Self {
let work_days = [
Weekday::Mon,
Weekday::Tue,
Weekday::Wed,
Weekday::Thu,
Weekday::Fri,
]
.iter()
.cloned()
.collect();
WorkCalendar {
work_days,
holidays: HashSet::new(),
}
}
pub fn compute_end_date(
&self,
start_date: NaiveDate,
days_worked: i64,
) -> Result<(NaiveDate, Duration), String> {
if days_worked < 0 {
return Err("days_worked must be non-negative".to_string());
}
if self.work_days.is_empty() {
return Err("No work days defined".to_string());
}
let mut current_date = start_date;
let mut remaining_days = days_worked;
if self.is_work_day(¤t_date.weekday()) && !self.is_holiday(¤t_date) {
remaining_days -= 1;
}
while remaining_days > 0 {
current_date += Duration::days(1);
if self.is_work_day(¤t_date.weekday()) && !self.is_holiday(¤t_date) {
remaining_days -= 1;
}
}
let calendar_duration = current_date.signed_duration_since(start_date);
Ok((current_date, calendar_duration))
}
pub fn add_work_day(&mut self, day: Weekday) {
self.work_days.insert(day);
}
pub fn remove_work_day(&mut self, day: &Weekday) {
self.work_days.remove(day);
}
pub fn add_holiday(&mut self, date: NaiveDate) {
self.holidays.insert(date);
}
pub fn remove_holiday(&mut self, date: &NaiveDate) {
self.holidays.remove(date);
}
pub fn set_work_days(&mut self, days: &str) -> Result<(), String> {
let new_work_days: HashSet<Weekday> = days
.split(',')
.filter_map(|day| parse_weekday(day.trim()))
.collect();
if new_work_days.is_empty() {
return Err("No valid work days provided".to_string());
}
self.work_days = new_work_days;
Ok(())
}
pub fn is_work_day(&self, day: &Weekday) -> bool {
self.work_days.contains(day)
}
pub fn is_holiday(&self, date: &NaiveDate) -> bool {
self.holidays.contains(date)
}
pub fn work_days_between(&self, start_date: NaiveDate, end_date: NaiveDate) -> i64 {
let mut work_days = 0;
let mut current_date = start_date;
while current_date <= end_date {
if self.work_days.contains(¤t_date.weekday()) && !self.is_holiday(¤t_date) {
work_days += 1;
}
current_date += Duration::days(1);
}
work_days
}
}
#[derive(Debug, Serialize, Deserialize, Default)]
struct WorkCalendarConfig {
work_days: Option<Vec<String>>,
holidays: Option<Vec<String>>,
}
impl From<WorkCalendarConfig> for WorkCalendar {
fn from(config: WorkCalendarConfig) -> Self {
let mut calendar = WorkCalendar::new();
if let Some(days) = config.work_days {
calendar.work_days = days
.into_iter()
.filter_map(|day| parse_weekday(&day))
.collect();
}
if let Some(dates) = config.holidays {
calendar.holidays = dates
.into_iter()
.filter_map(|date_str| NaiveDate::parse_from_str(&date_str, "%Y-%m-%d").ok())
.collect();
}
calendar
}
}
pub fn parse_weekday(day: &str) -> Option<Weekday> {
match day.to_lowercase().as_str() {
"monday" | "mon" => Some(Weekday::Mon),
"tuesday" | "tue" => Some(Weekday::Tue),
"wednesday" | "wed" => Some(Weekday::Wed),
"thursday" | "thu" => Some(Weekday::Thu),
"friday" | "fri" => Some(Weekday::Fri),
"saturday" | "sat" => Some(Weekday::Sat),
"sunday" | "sun" => Some(Weekday::Sun),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_compute_end_date_standard_week() {
let calendar = WorkCalendar::new();
let start_date = NaiveDate::from_ymd_opt(2023, 8, 21).unwrap(); let (end_date, duration) = calendar.compute_end_date(start_date, 5).unwrap();
assert_eq!(end_date, NaiveDate::from_ymd_opt(2023, 8, 25).unwrap()); assert_eq!(duration.num_days(), 4);
}
#[test]
fn test_compute_end_date_with_weekend() {
let calendar = WorkCalendar::new();
let start_date = NaiveDate::from_ymd_opt(2023, 8, 18).unwrap(); let (end_date, duration) = calendar.compute_end_date(start_date, 5).unwrap();
assert_eq!(end_date, NaiveDate::from_ymd_opt(2023, 8, 24).unwrap()); assert_eq!(duration.num_days(), 6);
}
#[test]
fn test_compute_end_date_with_holiday() {
let mut calendar = WorkCalendar::new();
calendar.add_holiday(NaiveDate::from_ymd_opt(2023, 8, 23).unwrap()); let start_date = NaiveDate::from_ymd_opt(2023, 8, 21).unwrap(); let (end_date, duration) = calendar.compute_end_date(start_date, 5).unwrap();
assert_eq!(end_date, NaiveDate::from_ymd_opt(2023, 8, 28).unwrap()); assert_eq!(duration.num_days(), 7);
}
#[test]
fn test_compute_end_date_custom_work_days() {
let mut calendar = WorkCalendar::new();
calendar.set_work_days("Monday,Wednesday,Friday").unwrap();
let start_date = NaiveDate::from_ymd_opt(2023, 8, 21).unwrap(); let (end_date, duration) = calendar.compute_end_date(start_date, 5).unwrap();
assert_eq!(end_date, NaiveDate::from_ymd_opt(2023, 8, 30).unwrap()); assert_eq!(duration.num_days(), 9);
}
#[test]
fn test_compute_end_date_zero_days() {
let calendar = WorkCalendar::new();
let start_date = NaiveDate::from_ymd_opt(2023, 8, 21).unwrap(); let (end_date, duration) = calendar.compute_end_date(start_date, 0).unwrap();
assert_eq!(end_date, start_date);
assert_eq!(duration.num_days(), 0);
}
#[test]
fn test_compute_end_date_negative_days() {
let calendar = WorkCalendar::new();
let start_date = NaiveDate::from_ymd_opt(2023, 8, 21).unwrap(); assert!(calendar.compute_end_date(start_date, -1).is_err());
}
#[test]
fn test_set_work_days() {
let mut calendar = WorkCalendar::new();
assert!(calendar.set_work_days("Mon,Wed,Fri").is_ok());
assert!(calendar.is_work_day(&Weekday::Mon));
assert!(calendar.is_work_day(&Weekday::Wed));
assert!(calendar.is_work_day(&Weekday::Fri));
assert!(!calendar.is_work_day(&Weekday::Tue));
assert!(!calendar.is_work_day(&Weekday::Thu));
assert!(!calendar.is_work_day(&Weekday::Sat));
assert!(!calendar.is_work_day(&Weekday::Sun));
}
#[test]
fn test_add_and_remove_holiday() {
let mut calendar = WorkCalendar::new();
let holiday = NaiveDate::from_ymd_opt(2023, 12, 25).unwrap();
calendar.add_holiday(holiday);
assert!(calendar.is_holiday(&holiday));
calendar.remove_holiday(&holiday);
assert!(!calendar.is_holiday(&holiday));
}
#[test]
fn test_work_days_between() {
let calendar = WorkCalendar::new();
let start_date = NaiveDate::from_ymd_opt(2023, 8, 21).unwrap(); let end_date = NaiveDate::from_ymd_opt(2023, 8, 25).unwrap(); assert_eq!(calendar.work_days_between(start_date, end_date), 5);
let end_date = NaiveDate::from_ymd_opt(2023, 8, 27).unwrap(); assert_eq!(calendar.work_days_between(start_date, end_date), 5);
let mut calendar = WorkCalendar::new();
calendar.add_holiday(NaiveDate::from_ymd_opt(2023, 8, 23).unwrap()); assert_eq!(calendar.work_days_between(start_date, end_date), 4);
}
#[test]
fn test_from_str_yaml() {
let config = r#"
work_days:
- Monday
- Wednesday
- Friday
holidays:
- 2023-12-25
"#;
let calendar = WorkCalendar::from_str(config).unwrap();
assert!(calendar.is_work_day(&Weekday::Mon));
assert!(calendar.is_work_day(&Weekday::Wed));
assert!(calendar.is_work_day(&Weekday::Fri));
assert!(!calendar.is_work_day(&Weekday::Tue));
assert!(calendar.is_holiday(&NaiveDate::from_ymd_opt(2023, 12, 25).unwrap()));
}
#[test]
fn test_from_str_json() {
let config = r#"
{
"work_days": ["Monday", "Wednesday", "Friday"],
"holidays": ["2023-12-25"]
}
"#;
let calendar = WorkCalendar::from_str(config).unwrap();
assert!(calendar.is_work_day(&Weekday::Mon));
assert!(calendar.is_work_day(&Weekday::Wed));
assert!(calendar.is_work_day(&Weekday::Fri));
assert!(!calendar.is_work_day(&Weekday::Tue));
assert!(calendar.is_holiday(&NaiveDate::from_ymd_opt(2023, 12, 25).unwrap()));
}
}