initial commit

This commit is contained in:
swaits committed 2024-08-21 18:59:43 -06:00
commit 982f859668
6 files changed
+1082

No files matched your search

+1
View File
@@ -0,0 +1 @@
/target
Generated
+395
View File
@@ -0,0 +1,395 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "autocfg"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
[[package]]
name = "bumpalo"
version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]]
name = "cc"
version = "1.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72db2f7947ecee9b03b510377e8bb9077afa27176fdbff55c51027e976fdcc48"
dependencies = [
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"wasm-bindgen",
"windows-targets",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
[[package]]
name = "iana-time-zone"
version = "0.1.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "indexmap"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "itoa"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
[[package]]
name = "js-sys"
version = "0.3.70"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.158"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439"
[[package]]
name = "log"
version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "proc-macro2"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
dependencies = [
"proc-macro2",
]
[[package]]
name = "ryu"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[package]]
name = "serde"
version = "1.0.208"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cff085d2cb684faa248efb494c39b68e522822ac0de72ccf08109abde717cfb2"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.208"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.125"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
]
[[package]]
name = "serde_yaml"
version = "0.9.34+deprecated"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
dependencies = [
"indexmap",
"itoa",
"ryu",
"serde",
"unsafe-libyaml",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "syn"
version = "2.0.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6af063034fc1935ede7be0122941bafa9bacb949334d090b77ca98b5817c7d9"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "unsafe-libyaml"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
[[package]]
name = "wasm-bindgen"
version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5"
dependencies = [
"cfg-if",
"once_cell",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484"
[[package]]
name = "windows-core"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "workdays"
version = "0.1.0"
dependencies = [
"chrono",
"serde",
"serde_json",
"serde_yaml",
]
+20
View File
@@ -0,0 +1,20 @@
[package]
name = "workdays"
version = "0.1.0"
edition = "2021"
authors = ["Stephen Waits <steve@waits.net>"]
description = "A Rust library for computing work days and handling work calendars, inspired by the WORKDAY function in Excel."
license = "MIT"
repository = "https://github.com/swaits/workdays"
documentation = "https://docs.rs/workdays"
readme = "README.md"
keywords = ["calendar", "workday", "date", "business", "excel"]
categories = ["date-and-time", "algorithms"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
chrono = { version = "0.4.38", features = ["serde"] }
serde = { version = "1.0.208", features = ["derive"] }
serde_json = "1.0.125"
serde_yaml = "0.9.34"
+18
View File
@@ -0,0 +1,18 @@
Copyright 2024 Stephen Waits <steve@waits.net>
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the “Software”), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+68
View File
@@ -0,0 +1,68 @@
# Workdays
`workdays` is a Rust library for computing work days and handling work calendars, inspired by the WORKDAY function in Excel. It provides functionality to define custom work weeks, add holidays, and compute dates based on a given number of work days.
## Features
- Compute end dates based on work days
- Calculate the number of work days between two dates
- Parse and handle work calendar configurations (YAML or JSON)
- Support for custom work days and holidays
- Flexible weekday parsing
## Installation
Add `workdays` as a depend in your `Cargo.toml`:
```bash
cargo add workdays
```
## Usage
Here's a quick example:
```rust
use workdays::WorkCalendar;
use chrono::NaiveDate;
use std::str::FromStr;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut calendar = WorkCalendar::new();
calendar.add_holiday(NaiveDate::from_ymd_opt(2023, 12, 25).unwrap());
calendar.set_work_days("Mon,Tue,Wed,Thu,Fri")?;
let start_date = NaiveDate::from_ymd_opt(2023, 8, 21).unwrap();
let days_worked = 20;
let (end_date, calendar_duration) = calendar.compute_end_date(start_date, days_worked)?;
println!("End date: {}", end_date);
println!("Calendar duration: {} days", calendar_duration.num_days());
// Using FromStr to create a WorkCalendar from YAML or JSON
let config = r#"
work_days:
- Monday
- Tuesday
- Wednesday
holidays:
- 2023-12-25
"#;
let custom_calendar = WorkCalendar::from_str(config)?;
println!("Is Monday a work day? {}", custom_calendar.is_work_day(&chrono::Weekday::Mon));
Ok(())
}
```
## License
This project is licensed under the MIT License - see the [LICENSE](./LICENSE) file for details.
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
## Acknowledgments
- Inspired by the WORKDAY function in Excel
+580
View File
@@ -0,0 +1,580 @@
//! # Workdays
//!
//! `workdays` is a Rust library for computing work days and handling work calendars.
//! It provides functionality to define custom work weeks, add holidays, and compute
//! dates based on a given number of work days.
//!
//! ## Features
//!
//! - Compute end dates based on work days
//! - Calculate the number of work days between two dates
//! - Parse and handle work calendar configurations (YAML or JSON)
//! - Support for custom work days and holidays
//! - Flexible weekday parsing
//!
//! ## Usage
//!
//! Here's a quick example:
//!
//! ```
//! use workdays::WorkCalendar;
//! use chrono::NaiveDate;
//!
//! let mut calendar = WorkCalendar::new();
//! calendar.add_holiday(NaiveDate::from_ymd_opt(2023, 12, 25).unwrap());
//! calendar.set_work_days("Mon,Tue,Wed,Thu,Fri").unwrap();
//!
//! let start_date = NaiveDate::from_ymd_opt(2023, 8, 21).unwrap();
//! let days_worked = 20;
//!
//! let (end_date, calendar_duration) = calendar.compute_end_date(start_date, days_worked).unwrap();
//! println!("End date: {}", end_date);
//! println!("Calendar duration: {} days", calendar_duration.num_days());
//! ```
use chrono::{Datelike, Duration, NaiveDate, Weekday};
use serde::{Deserialize, Serialize};
use std::{collections::HashSet, str::FromStr};
/// Represents a work calendar with customizable work days and holidays.
#[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>;
/// Creates a `WorkCalendar` from a YAML or JSON string.
///
/// # Arguments
///
/// * `s` - A string slice that holds the configuration in YAML or JSON format.
///
/// # Examples
///
/// ```
/// use std::str::FromStr;
/// use workdays::WorkCalendar;
///
/// let config = r#"
/// work_days:
/// - Monday
/// - Tuesday
/// - Wednesday
/// holidays:
/// - 2023-12-25
/// "#;
///
/// let calendar = WorkCalendar::from_str(config).unwrap();
/// assert!(calendar.is_work_day(&chrono::Weekday::Mon));
/// assert!(!calendar.is_work_day(&chrono::Weekday::Thu));
/// ```
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 {
/// Creates a new `WorkCalendar` with default work days (Monday to Friday) and no holidays.
///
/// # Examples
///
/// ```
/// use workdays::WorkCalendar;
/// use chrono::Weekday;
///
/// let calendar = WorkCalendar::new();
/// assert!(calendar.is_work_day(&Weekday::Mon));
/// assert!(!calendar.is_work_day(&Weekday::Sat));
/// ```
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(),
}
}
/// Computes the end date and calendar duration given a start date and number of work days.
///
/// # Arguments
///
/// * `start_date` - The starting date.
/// * `days_worked` - Number of work days to add.
///
/// # Returns
///
/// A tuple containing the end date and the calendar duration, or an error if the input is invalid.
///
/// # Examples
///
/// ```
/// use workdays::WorkCalendar;
/// use chrono::NaiveDate;
///
/// 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);
/// ```
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 the start date is a work day, count it
if self.is_work_day(&current_date.weekday()) && !self.is_holiday(&current_date) {
remaining_days -= 1;
}
while remaining_days > 0 {
current_date += Duration::days(1);
if self.is_work_day(&current_date.weekday()) && !self.is_holiday(&current_date) {
remaining_days -= 1;
}
}
let calendar_duration = current_date.signed_duration_since(start_date);
Ok((current_date, calendar_duration))
}
/// Adds a work day to the calendar.
///
/// # Arguments
///
/// * `day` - The `Weekday` to add as a work day.
///
/// # Examples
///
/// ```
/// use workdays::WorkCalendar;
/// use chrono::Weekday;
///
/// let mut calendar = WorkCalendar::new();
/// calendar.add_work_day(Weekday::Sat);
/// assert!(calendar.is_work_day(&Weekday::Sat));
/// ```
pub fn add_work_day(&mut self, day: Weekday) {
self.work_days.insert(day);
}
/// Removes a work day from the calendar.
///
/// # Arguments
///
/// * `day` - The `Weekday` to remove from work days.
///
/// # Examples
///
/// ```
/// use workdays::WorkCalendar;
/// use chrono::Weekday;
///
/// let mut calendar = WorkCalendar::new();
/// calendar.remove_work_day(&Weekday::Fri);
/// assert!(!calendar.is_work_day(&Weekday::Fri));
/// ```
pub fn remove_work_day(&mut self, day: &Weekday) {
self.work_days.remove(day);
}
/// Adds a holiday to the calendar.
///
/// # Arguments
///
/// * `date` - The `NaiveDate` to add as a holiday.
///
/// # Examples
///
/// ```
/// use workdays::WorkCalendar;
/// use chrono::NaiveDate;
///
/// let mut calendar = WorkCalendar::new();
/// let holiday = NaiveDate::from_ymd_opt(2023, 12, 25).unwrap();
/// calendar.add_holiday(holiday);
/// assert!(calendar.is_holiday(&holiday));
/// ```
pub fn add_holiday(&mut self, date: NaiveDate) {
self.holidays.insert(date);
}
/// Removes a holiday from the calendar.
///
/// # Arguments
///
/// * `date` - The `NaiveDate` to remove from holidays.
///
/// # Examples
///
/// ```
/// use workdays::WorkCalendar;
/// use chrono::NaiveDate;
///
/// let mut calendar = WorkCalendar::new();
/// let holiday = NaiveDate::from_ymd_opt(2023, 12, 25).unwrap();
/// calendar.add_holiday(holiday);
/// calendar.remove_holiday(&holiday);
/// assert!(!calendar.is_holiday(&holiday));
/// ```
pub fn remove_holiday(&mut self, date: &NaiveDate) {
self.holidays.remove(date);
}
/// Set work days from a comma-separated string.
///
/// This method overrides all existing work days with the ones specified in the input string.
/// Day names are case-insensitive and can be full names (e.g., "Monday") or abbreviations (e.g., "Mon").
///
/// # Arguments
///
/// * `days` - A comma-separated string of day names (e.g., "mon,tue,Wednesday,thu,friday")
///
/// # Returns
///
/// * `Ok(())` if successful
/// * `Err(String)` if the input string contains invalid day names
///
/// # Examples
///
/// ```
/// use workdays::WorkCalendar;
/// use chrono::Weekday;
///
/// let mut calendar = WorkCalendar::new();
/// calendar.set_work_days("Mon,Wed,Fri").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));
/// ```
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(())
}
/// Checks if a given day is a work day.
///
/// # Arguments
///
/// * `day` - The `Weekday` to check.
///
/// # Returns
///
/// `true` if the day is a work day, `false` otherwise.
///
/// # Examples
///
/// ```
/// use workdays::WorkCalendar;
/// use chrono::Weekday;
///
/// let calendar = WorkCalendar::new();
/// assert!(calendar.is_work_day(&Weekday::Mon));
/// assert!(!calendar.is_work_day(&Weekday::Sat));
/// ```
pub fn is_work_day(&self, day: &Weekday) -> bool {
self.work_days.contains(day)
}
/// Checks if a given date is a holiday.
///
/// # Arguments
///
/// * `date` - The `NaiveDate` to check.
///
/// # Returns
///
/// `true` if the date is a holiday, `false` otherwise.
///
/// # Examples
///
/// ```
/// use workdays::WorkCalendar;
/// use chrono::NaiveDate;
///
/// let mut calendar = WorkCalendar::new();
/// let holiday = NaiveDate::from_ymd_opt(2023, 12, 25).unwrap();
/// calendar.add_holiday(holiday);
/// assert!(calendar.is_holiday(&holiday));
/// ```
pub fn is_holiday(&self, date: &NaiveDate) -> bool {
self.holidays.contains(date)
}
/// Calculates the number of work days between two dates (inclusive).
///
/// # Arguments
///
/// * `start_date` - The starting date.
/// * `end_date` - The ending date.
///
/// # Returns
///
/// The number of work days between the two dates (inclusive).
///
/// # Examples
///
/// ```
/// use workdays::WorkCalendar;
/// use chrono::NaiveDate;
///
/// let calendar = WorkCalendar::new();
/// let start_date = NaiveDate::from_ymd_opt(2023, 8, 21).unwrap(); // Monday
/// let end_date = NaiveDate::from_ymd_opt(2023, 8, 25).unwrap(); // Friday
/// assert_eq!(calendar.work_days_between(start_date, end_date), 5);
/// ```
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(&current_date.weekday()) && !self.is_holiday(&current_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
}
}
/// Parses a weekday string into a `Weekday` enum.
///
/// This function is case-insensitive and accepts both full names (e.g., "Monday")
/// and abbreviations (e.g., "Mon").
///
/// # Arguments
///
/// * `day` - A string slice that holds the name of the day.
///
/// # Returns
///
/// * `Some(Weekday)` if the day name is valid.
/// * `None` if the day name is invalid.
///
/// # Examples
///
/// ```
/// use workdays::parse_weekday;
/// use chrono::Weekday;
///
/// assert_eq!(parse_weekday("Monday"), Some(Weekday::Mon));
/// assert_eq!(parse_weekday("tue"), Some(Weekday::Tue));
/// assert_eq!(parse_weekday("Invalid"), None);
/// ```
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(); // Monday
let (end_date, duration) = calendar.compute_end_date(start_date, 5).unwrap();
assert_eq!(end_date, NaiveDate::from_ymd_opt(2023, 8, 25).unwrap()); // Friday
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(); // Friday
let (end_date, duration) = calendar.compute_end_date(start_date, 5).unwrap();
assert_eq!(end_date, NaiveDate::from_ymd_opt(2023, 8, 24).unwrap()); // Next Thursday
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()); // Wednesday
let start_date = NaiveDate::from_ymd_opt(2023, 8, 21).unwrap(); // Monday
let (end_date, duration) = calendar.compute_end_date(start_date, 5).unwrap();
assert_eq!(end_date, NaiveDate::from_ymd_opt(2023, 8, 28).unwrap()); // Next Monday
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(); // Monday
let (end_date, duration) = calendar.compute_end_date(start_date, 5).unwrap();
assert_eq!(end_date, NaiveDate::from_ymd_opt(2023, 8, 30).unwrap()); // Wednesday
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(); // Monday
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(); // Monday
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(); // Monday
let end_date = NaiveDate::from_ymd_opt(2023, 8, 25).unwrap(); // Friday
assert_eq!(calendar.work_days_between(start_date, end_date), 5);
let end_date = NaiveDate::from_ymd_opt(2023, 8, 27).unwrap(); // Sunday
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()); // Wednesday
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()));
}
}