holiday_rs/
lib.rs

1//! holiday-rs: MVP library to get holidays and business-day utils.
2//!
3//! - Countries: FR (France) for v0.1.0
4//! - Exports: JSON/CSV helpers
5//! - Utils: `next_holiday`, `is_business_day`, `business_days_between`
6
7mod data;
8mod calc;
9pub mod export;
10
11use chrono::{NaiveDate, Datelike};
12use serde::{Serialize, Deserialize};
13
14#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
15pub struct Holiday {
16    pub name: String,
17    pub date: NaiveDate,
18    pub country: String,
19}
20
21/// Return the list of holidays for a given `year` and ISO-like country code (MVP: "FR").
22pub fn get_holidays(year: i32, country: &str) -> Vec<Holiday> {
23    match country.to_uppercase().as_str() {
24        "FR" => data::fr::holidays_fr(year),
25        _ => Vec::new(),
26    }
27}
28
29/// Return the next holiday strictly after `date` for `country`.
30pub fn next_holiday(date: NaiveDate, country: &str) -> Option<Holiday> {
31    let mut list = get_holidays(date.year(), country);
32    if date.month() == 12 && date.day() >= 20 {
33        list.extend(get_holidays(date.year() + 1, country));
34    }
35    list.into_iter().filter(|h| h.date > date).min_by_key(|h| h.date)
36}
37
38/// Returns true if the date is a business day (Mon-Fri and not a holiday for `country`).
39pub fn is_business_day(date: NaiveDate, country: &str) -> bool {
40    let weekday = date.weekday().number_from_monday(); // 1..=7
41    if weekday >= 6 {
42        return false;
43    }
44    let holidays = get_holidays(date.year(), country);
45    !holidays.iter().any(|h| h.date == date)
46}
47
48/// Count business days in [start, end). If start >= end -> 0.
49pub fn business_days_between(start: NaiveDate, end: NaiveDate, country: &str) -> u32 {
50    if start >= end { return 0; }
51    let mut count = 0u32;
52    let mut d = start;
53    while d < end {
54        if is_business_day(d, country) { count += 1; }
55        d = d.succ_opt().unwrap();
56    }
57    count
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63    use chrono::NaiveDate;
64
65    #[test]
66    fn test_fr_2025_contains_may1() {
67        let v = get_holidays(2025, "FR");
68        assert!(v.iter().any(|h| h.name.contains("FĂȘte du Travail") && h.date == NaiveDate::from_ymd_opt(2025,5,1).unwrap()));
69    }
70
71    #[test]
72    fn test_business_days_between_simple() {
73        let s = NaiveDate::from_ymd_opt(2025, 5, 2).unwrap(); // Fri
74        let e = NaiveDate::from_ymd_opt(2025, 5, 6).unwrap(); // Tue
75        assert_eq!(business_days_between(s, e, "FR"), 2); // Fri + Mon
76    }
77}
78