spacecell 0.1.0

Datetime library with ISO8601/RFC3339 parsing, calendar arithmetic, and business day calculations
Documentation
//! # **Business Module** - *Business day calculations*
//!
//! Business day operations with weekend and holiday support.

use super::civil::{day_of_week, days_to_ymd, ymd_to_days};
use super::config::CalendarConfig;

/// Day of week enumeration
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Weekday {
    Monday = 0,
    Tuesday = 1,
    Wednesday = 2,
    Thursday = 3,
    Friday = 4,
    Saturday = 5,
    Sunday = 6,
}

impl Weekday {
    /// Convert from day number (0=Monday, 6=Sunday)
    pub fn from_u32(day: u32) -> Self {
        match day % 7 {
            0 => Weekday::Monday,
            1 => Weekday::Tuesday,
            2 => Weekday::Wednesday,
            3 => Weekday::Thursday,
            4 => Weekday::Friday,
            5 => Weekday::Saturday,
            6 => Weekday::Sunday,
            _ => unreachable!(),
        }
    }

    /// Check if this is a weekend day
    #[inline]
    pub fn is_weekend(&self) -> bool {
        matches!(self, Weekday::Saturday | Weekday::Sunday)
    }
}

/// Check if a date is a weekend
#[inline]
pub fn is_weekend(year: i32, month: u32, day: u32) -> bool {
    let days = ymd_to_days(year, month, day);
    let weekday = day_of_week(days);
    Weekday::from_u32(weekday).is_weekend()
}

/// Check if a date is a business day (not weekend, not holiday)
#[inline]
pub fn is_business_day(year: i32, month: u32, day: u32, config: &CalendarConfig) -> bool {
    !is_weekend(year, month, day) && !config.is_holiday(year, month, day)
}

/// Count business days in a date range (inclusive)
///
/// # Arguments
/// - Date range from (from_year, from_month, from_day) to (to_year, to_month, to_day)
/// - `config`: Calendar configuration for holidays
///
/// # Returns
/// Number of business days (non-weekend, non-holiday days)
///
/// # TODO
/// Parallelize for large date ranges by splitting into chunks
pub fn count_business_days(
    from_year: i32,
    from_month: u32,
    from_day: u32,
    to_year: i32,
    to_month: u32,
    to_day: u32,
    config: &CalendarConfig,
) -> i64 {
    // TODO: Parallelize with rayon for large ranges
    // Split into monthly chunks and process in parallel

    let start_days = ymd_to_days(from_year, from_month, from_day);
    let end_days = ymd_to_days(to_year, to_month, to_day);

    if start_days > end_days {
        return 0;
    }

    let mut count = 0i64;
    for days in start_days..=end_days {
        let (y, m, d) = days_to_ymd(days);
        if is_business_day(y, m, d, config) {
            count += 1;
        }
    }
    count
}

/// Add business days to a date
///
/// Skips weekends and holidays when counting.
///
/// # Arguments
/// - Starting date (year, month, day)
/// - `business_days`: Number of business days to add (can be negative)
/// - `config`: Calendar configuration for holidays
///
/// # Returns
/// New date (year, month, day)
///
/// # TODO
/// Optimize for large N by skipping full weeks in bulk
pub fn add_business_days(
    year: i32,
    month: u32,
    day: u32,
    business_days: i64,
    config: &CalendarConfig,
) -> (i32, u32, u32) {
    // TODO: Optimize by skipping full weeks when |business_days| is large
    // E.g., 5 business days per week, so 50 business days ≈ 10 weeks

    if business_days == 0 {
        return (year, month, day);
    }

    let mut current_days = ymd_to_days(year, month, day);
    let mut remaining = business_days.abs();
    let direction = if business_days >= 0 { 1 } else { -1 };

    while remaining > 0 {
        current_days += direction;
        let (y, m, d) = days_to_ymd(current_days);
        if is_business_day(y, m, d, config) {
            remaining -= 1;
        }
    }

    days_to_ymd(current_days)
}

/// Get next business day after the given date
///
/// # Returns
/// Next business day (year, month, day)
pub fn next_business_day(
    year: i32,
    month: u32,
    day: u32,
    config: &CalendarConfig,
) -> (i32, u32, u32) {
    let mut days = ymd_to_days(year, month, day) + 1;
    loop {
        let (y, m, d) = days_to_ymd(days);
        if is_business_day(y, m, d, config) {
            return (y, m, d);
        }
        days += 1;
    }
}

/// Get previous business day before the given date
///
/// # Returns
/// Previous business day (year, month, day)
pub fn prev_business_day(
    year: i32,
    month: u32,
    day: u32,
    config: &CalendarConfig,
) -> (i32, u32, u32) {
    let mut days = ymd_to_days(year, month, day) - 1;
    loop {
        let (y, m, d) = days_to_ymd(days);
        if is_business_day(y, m, d, config) {
            return (y, m, d);
        }
        days -= 1;
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_weekday_from_u32() {
        assert_eq!(Weekday::from_u32(0), Weekday::Monday);
        assert_eq!(Weekday::from_u32(6), Weekday::Sunday);
        assert_eq!(Weekday::from_u32(7), Weekday::Monday); // Wraps
    }

    #[test]
    fn test_weekday_is_weekend() {
        assert!(!Weekday::Monday.is_weekend());
        assert!(!Weekday::Friday.is_weekend());
        assert!(Weekday::Saturday.is_weekend());
        assert!(Weekday::Sunday.is_weekend());
    }

    #[test]
    fn test_is_weekend() {
        // 2024-01-06 is Saturday
        assert!(is_weekend(2024, 1, 6));
        // 2024-01-07 is Sunday
        assert!(is_weekend(2024, 1, 7));
        // 2024-01-08 is Monday
        assert!(!is_weekend(2024, 1, 8));
    }

    #[test]
    fn test_is_business_day() {
        let config = CalendarConfig::new();

        // Monday should be a business day
        assert!(is_business_day(2024, 1, 8, &config));

        // Saturday should not be a business day
        assert!(!is_business_day(2024, 1, 6, &config));

        // Holiday should not be a business day
        let config = config.add_holiday(2024, 12, 25);
        assert!(!is_business_day(2024, 12, 25, &config));
    }

    #[test]
    fn test_count_business_days() {
        let config = CalendarConfig::new();

        // Monday Jan 8 to Friday Jan 12 (inclusive) = 5 business days
        let count = count_business_days(2024, 1, 8, 2024, 1, 12, &config);
        assert_eq!(count, 5);

        // Friday Jan 12 to Monday Jan 15 (inclusive, skips weekend) = 2 business days
        let count = count_business_days(2024, 1, 12, 2024, 1, 15, &config);
        assert_eq!(count, 2);
    }

    #[test]
    fn test_count_business_days_with_holiday() {
        // Add Christmas as holiday
        let config = CalendarConfig::new().add_holiday(2024, 12, 25);

        // Dec 23 (Mon) to Dec 27 (Fri), excluding Dec 25 (Wed holiday) and weekend
        let count = count_business_days(2024, 12, 23, 2024, 12, 27, &config);
        // Mon 23, Tue 24, Thu 26, Fri 27 = 4 days (Wed 25 is holiday, Sat-Sun skipped)
        assert_eq!(count, 4);
    }

    #[test]
    fn test_add_business_days() {
        let config = CalendarConfig::new();

        // Monday Jan 8 + 5 business days = Monday Jan 15
        let (y, m, d) = add_business_days(2024, 1, 8, 5, &config);
        assert_eq!((y, m, d), (2024, 1, 15));

        // Friday Jan 12 + 1 business day = Monday Jan 15 (skips weekend)
        let (y, m, d) = add_business_days(2024, 1, 12, 1, &config);
        assert_eq!((y, m, d), (2024, 1, 15));
    }

    #[test]
    fn test_add_business_days_negative() {
        let config = CalendarConfig::new();

        // Monday Jan 15 - 1 business day = Friday Jan 12
        let (y, m, d) = add_business_days(2024, 1, 15, -1, &config);
        assert_eq!((y, m, d), (2024, 1, 12));

        // Monday Jan 15 - 5 business days = Monday Jan 8
        let (y, m, d) = add_business_days(2024, 1, 15, -5, &config);
        assert_eq!((y, m, d), (2024, 1, 8));
    }

    #[test]
    fn test_next_business_day() {
        let config = CalendarConfig::new();

        // Friday → next business day is Monday
        let (y, m, d) = next_business_day(2024, 1, 12, &config);
        assert_eq!((y, m, d), (2024, 1, 15));

        // Monday → next business day is Tuesday
        let (y, m, d) = next_business_day(2024, 1, 8, &config);
        assert_eq!((y, m, d), (2024, 1, 9));
    }

    #[test]
    fn test_prev_business_day() {
        let config = CalendarConfig::new();

        // Monday → previous business day is Friday
        let (y, m, d) = prev_business_day(2024, 1, 15, &config);
        assert_eq!((y, m, d), (2024, 1, 12));

        // Tuesday → previous business day is Monday
        let (y, m, d) = prev_business_day(2024, 1, 9, &config);
        assert_eq!((y, m, d), (2024, 1, 8));
    }

    #[test]
    fn test_next_business_day_with_holiday() {
        // Add Monday Jan 15 as holiday
        let config = CalendarConfig::new().add_holiday(2024, 1, 15);

        // Friday Jan 12 → next business day is Tuesday Jan 16 (skips weekend and holiday)
        let (y, m, d) = next_business_day(2024, 1, 12, &config);
        assert_eq!((y, m, d), (2024, 1, 16));
    }
}