spacecell 0.1.0

Datetime library with ISO8601/RFC3339 parsing, calendar arithmetic, and business day calculations
Documentation
//! # **Arithmetic Module** - *Date/time arithmetic operations*
//!
//! Calendar-aware arithmetic with delta addition and timestamp differencing.

use super::civil::{days_in_month, ymd_to_days};
use super::components::{components_to_timestamp, extract_components};
use super::delta::TimeDelta;
use crate::time_units::TimeUnit;

/// Add a TimeDelta to a timestamp
///
/// # Arguments
/// - `timestamp`: Base timestamp
/// - `delta`: TimeDelta to add
/// - `time_unit`: Unit of the timestamp
///
/// # Returns
/// New timestamp with delta applied
pub fn add_delta(timestamp: i64, delta: &TimeDelta, time_unit: TimeUnit) -> i64 {
    let delta_nanos = delta.total_nanoseconds();

    match time_unit {
        TimeUnit::Seconds => timestamp + (delta_nanos / 1_000_000_000) as i64,
        TimeUnit::Milliseconds => timestamp + (delta_nanos / 1_000_000) as i64,
        TimeUnit::Microseconds => timestamp + (delta_nanos / 1_000) as i64,
        TimeUnit::Nanoseconds => timestamp + delta_nanos as i64,
        TimeUnit::Days => timestamp + delta.total_seconds() / 86_400,
    }
}

/// Calculate difference between two timestamps
///
/// # Arguments
/// - `lhs`: First timestamp
/// - `rhs`: Second timestamp
/// - `time_unit`: Unit of the timestamps
///
/// # Returns
/// TimeDelta representing lhs - rhs
pub fn diff_timestamps(lhs: i64, rhs: i64, time_unit: TimeUnit) -> TimeDelta {
    let diff = lhs - rhs;

    match time_unit {
        TimeUnit::Seconds => TimeDelta::seconds(diff),
        TimeUnit::Milliseconds => TimeDelta::milliseconds(diff),
        TimeUnit::Microseconds => TimeDelta::microseconds(diff),
        TimeUnit::Nanoseconds => TimeDelta::nanoseconds(diff),
        TimeUnit::Days => TimeDelta::days(diff),
    }
}

/// Add months to a timestamp (calendar-aware)
///
/// Handles month-end edge cases:
/// - Jan 31 + 1 month = Feb 28/29 (last day of Feb)
/// - Aug 31 + 1 month = Sep 30 (last day of Sep)
///
/// # Arguments
/// - `timestamp`: Base timestamp
/// - `months`: Number of months to add (can be negative)
/// - `time_unit`: Unit of the timestamp
///
/// # Returns
/// New timestamp with months added
pub fn add_months(timestamp: i64, months: i64, time_unit: TimeUnit) -> i64 {
    let comp = extract_components(timestamp, time_unit);

    // Calculate target month and year
    let total_months = comp.month as i64 + months - 1; // 0-indexed
    let year_offset = if total_months >= 0 {
        total_months / 12
    } else {
        (total_months - 11) / 12
    };

    let target_year = comp.year + year_offset as i32;
    let target_month = ((total_months % 12 + 12) % 12 + 1) as u32;

    // Clamp day to valid range for target month
    let max_day = days_in_month(target_year, target_month);
    let target_day = comp.day.min(max_day);

    // Reconstruct timestamp
    let days = ymd_to_days(target_year, target_month, target_day);
    let seconds_in_day = comp.hour * 3600 + comp.minute * 60 + comp.second;
    let total_seconds = days * 86400 + seconds_in_day as i64;

    match time_unit {
        TimeUnit::Seconds => total_seconds,
        TimeUnit::Milliseconds => {
            total_seconds * 1_000 + (comp.nanosecond / 1_000_000) as i64
        }
        TimeUnit::Microseconds => total_seconds * 1_000_000 + (comp.nanosecond / 1_000) as i64,
        TimeUnit::Nanoseconds => total_seconds * 1_000_000_000 + comp.nanosecond as i64,
        TimeUnit::Days => days,
    }
}

/// Add years to a timestamp (calendar-aware)
///
/// Handles leap year edge case:
/// - 2020-02-29 + 1 year = 2021-02-28
///
/// # Arguments
/// - `timestamp`: Base timestamp
/// - `years`: Number of years to add (can be negative)
/// - `time_unit`: Unit of the timestamp
///
/// # Returns
/// New timestamp with years added
pub fn add_years(timestamp: i64, years: i64, time_unit: TimeUnit) -> i64 {
    add_months(timestamp, years * 12, time_unit)
}

/// Truncate timestamp to a coarser time unit
///
/// # Examples
/// - Truncate to day: 2000-01-15 12:34:56 → 2000-01-15 00:00:00
/// - Truncate to hour: 2000-01-15 12:34:56 → 2000-01-15 12:00:00
///
/// # Arguments
/// - `timestamp`: Timestamp to truncate
/// - `from_unit`: Current unit of the timestamp
/// - `to_unit`: Unit to truncate to
///
/// # Returns
/// Truncated timestamp in original unit
pub fn truncate_to_unit(timestamp: i64, from_unit: TimeUnit, to_unit: TimeUnit) -> i64 {
    let comp = extract_components(timestamp, from_unit);

    let (hour, minute, second, nanosecond) = match to_unit {
        TimeUnit::Days => (0, 0, 0, 0),
        TimeUnit::Seconds => (comp.hour, comp.minute, comp.second, 0),
        TimeUnit::Milliseconds => (comp.hour, comp.minute, comp.second, comp.nanosecond),
        TimeUnit::Microseconds => (comp.hour, comp.minute, comp.second, comp.nanosecond),
        TimeUnit::Nanoseconds => (comp.hour, comp.minute, comp.second, comp.nanosecond),
    };

    let truncated_comp = super::components::DateTimeComponents {
        year: comp.year,
        month: comp.month,
        day: comp.day,
        hour,
        minute,
        second,
        nanosecond,
    };

    components_to_timestamp(&truncated_comp, from_unit)
}

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

    #[test]
    fn test_add_delta() {
        let delta = TimeDelta::days(1);
        let result = add_delta(0, &delta, TimeUnit::Seconds);
        assert_eq!(result, 86_400);
    }

    #[test]
    fn test_diff_timestamps() {
        let delta = diff_timestamps(86_400, 0, TimeUnit::Seconds);
        assert_eq!(delta.total_seconds(), 86_400);
    }

    #[test]
    fn test_add_months_simple() {
        // 2000-01-15 + 1 month = 2000-02-15
        let ts = ymd_to_days(2000, 1, 15) * 86400;
        let result = add_months(ts, 1, TimeUnit::Seconds);
        let expected = ymd_to_days(2000, 2, 15) * 86400;
        assert_eq!(result, expected);
    }

    #[test]
    fn test_add_months_end_of_month() {
        // 2000-01-31 + 1 month = 2000-02-29 (leap year)
        let ts = ymd_to_days(2000, 1, 31) * 86400;
        let result = add_months(ts, 1, TimeUnit::Seconds);
        let expected = ymd_to_days(2000, 2, 29) * 86400;
        assert_eq!(result, expected);

        // 2001-01-31 + 1 month = 2001-02-28 (non-leap year)
        let ts = ymd_to_days(2001, 1, 31) * 86400;
        let result = add_months(ts, 1, TimeUnit::Seconds);
        let expected = ymd_to_days(2001, 2, 28) * 86400;
        assert_eq!(result, expected);
    }

    #[test]
    fn test_add_months_year_boundary() {
        // 2000-12-15 + 1 month = 2001-01-15
        let ts = ymd_to_days(2000, 12, 15) * 86400;
        let result = add_months(ts, 1, TimeUnit::Seconds);
        let expected = ymd_to_days(2001, 1, 15) * 86400;
        assert_eq!(result, expected);
    }

    #[test]
    fn test_add_months_negative() {
        // 2000-03-15 - 1 month = 2000-02-15
        let ts = ymd_to_days(2000, 3, 15) * 86400;
        let result = add_months(ts, -1, TimeUnit::Seconds);
        let expected = ymd_to_days(2000, 2, 15) * 86400;
        assert_eq!(result, expected);
    }

    #[test]
    fn test_add_years() {
        // 2000-01-15 + 1 year = 2001-01-15
        let ts = ymd_to_days(2000, 1, 15) * 86400;
        let result = add_years(ts, 1, TimeUnit::Seconds);
        let expected = ymd_to_days(2001, 1, 15) * 86400;
        assert_eq!(result, expected);
    }

    #[test]
    fn test_add_years_leap_day() {
        // 2020-02-29 + 1 year = 2021-02-28
        let ts = ymd_to_days(2020, 2, 29) * 86400;
        let result = add_years(ts, 1, TimeUnit::Seconds);
        let expected = ymd_to_days(2021, 2, 28) * 86400;
        assert_eq!(result, expected);
    }

    #[test]
    fn test_truncate_to_day() {
        // 2000-01-15 12:34:56 → 2000-01-15 00:00:00
        let ts = ymd_to_days(2000, 1, 15) * 86400 + 12 * 3600 + 34 * 60 + 56;
        let result = truncate_to_unit(ts, TimeUnit::Seconds, TimeUnit::Days);
        let expected = ymd_to_days(2000, 1, 15) * 86400;
        assert_eq!(result, expected);
    }

    #[test]
    fn test_truncate_milliseconds() {
        let ts = 1_700_000_123; // Includes milliseconds
        let result = truncate_to_unit(ts, TimeUnit::Milliseconds, TimeUnit::Seconds);
        assert_eq!(result, 1_700_000_000);
    }
}