spacecell 0.1.0

Datetime library with ISO8601/RFC3339 parsing, calendar arithmetic, and business day calculations
Documentation
//! # **Delta Module** - *Duration type for date arithmetic*
//!
//! Provides TimeDelta for representing durations and time differences.

/// Duration type for date/time arithmetic
///
/// Normalized representation:
/// - `days`: whole days
/// - `nanos`: nanoseconds within the day [0, 86_400_000_000_000)
///
/// Can represent both positive and negative durations.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct TimeDelta {
    days: i64,
    nanos: i64, // Always normalized to [0, 86_400_000_000_000)
}

const NANOS_PER_DAY: i64 = 86_400_000_000_000;

impl TimeDelta {
    /// Create a new TimeDelta with normalization
    #[inline]
    fn new_normalize(days: i64, nanos: i64) -> Self {
        let mut total_days = days;
        let mut total_nanos = nanos;

        // Normalize nanos to [0, NANOS_PER_DAY)
        if total_nanos < 0 {
            let overflow_days = (total_nanos.abs() + NANOS_PER_DAY - 1) / NANOS_PER_DAY;
            total_days -= overflow_days;
            total_nanos += overflow_days * NANOS_PER_DAY;
        }

        if total_nanos >= NANOS_PER_DAY {
            let overflow_days = total_nanos / NANOS_PER_DAY;
            total_days += overflow_days;
            total_nanos %= NANOS_PER_DAY;
        }

        Self {
            days: total_days,
            nanos: total_nanos,
        }
    }

    /// Create a delta from days
    #[inline]
    pub fn days(days: i64) -> Self {
        Self { days, nanos: 0 }
    }

    /// Create a delta from hours
    #[inline]
    pub fn hours(hours: i64) -> Self {
        Self::new_normalize(0, hours * 3_600_000_000_000)
    }

    /// Create a delta from minutes
    #[inline]
    pub fn minutes(minutes: i64) -> Self {
        Self::new_normalize(0, minutes * 60_000_000_000)
    }

    /// Create a delta from seconds
    #[inline]
    pub fn seconds(seconds: i64) -> Self {
        Self::new_normalize(0, seconds * 1_000_000_000)
    }

    /// Create a delta from milliseconds
    #[inline]
    pub fn milliseconds(millis: i64) -> Self {
        Self::new_normalize(0, millis * 1_000_000)
    }

    /// Create a delta from microseconds
    #[inline]
    pub fn microseconds(micros: i64) -> Self {
        Self::new_normalize(0, micros * 1_000)
    }

    /// Create a delta from nanoseconds
    #[inline]
    pub fn nanoseconds(nanos: i64) -> Self {
        Self::new_normalize(0, nanos)
    }

    /// Get total seconds (truncated)
    #[inline]
    pub fn total_seconds(&self) -> i64 {
        self.days * 86_400 + self.nanos / 1_000_000_000
    }

    /// Get total milliseconds (may overflow for very large durations)
    #[inline]
    pub fn total_milliseconds(&self) -> i64 {
        self.days
            .saturating_mul(86_400_000)
            .saturating_add(self.nanos / 1_000_000)
    }

    /// Get total nanoseconds (returns i128 to avoid overflow)
    #[inline]
    pub fn total_nanoseconds(&self) -> i128 {
        (self.days as i128) * (NANOS_PER_DAY as i128) + (self.nanos as i128)
    }

    /// Add two deltas
    #[inline]
    pub fn add(&self, other: &TimeDelta) -> TimeDelta {
        Self::new_normalize(self.days + other.days, self.nanos + other.nanos)
    }

    /// Subtract two deltas
    #[inline]
    pub fn sub(&self, other: &TimeDelta) -> TimeDelta {
        Self::new_normalize(self.days - other.days, self.nanos - other.nanos)
    }

    /// Negate a delta
    #[inline]
    pub fn neg(&self) -> TimeDelta {
        if self.nanos == 0 {
            Self {
                days: -self.days,
                nanos: 0,
            }
        } else {
            Self {
                days: -self.days - 1,
                nanos: NANOS_PER_DAY - self.nanos,
            }
        }
    }

    /// Get absolute value
    #[inline]
    pub fn abs(&self) -> TimeDelta {
        if self.days < 0 {
            self.neg()
        } else {
            *self
        }
    }

    /// Check if zero
    #[inline]
    pub fn is_zero(&self) -> bool {
        self.days == 0 && self.nanos == 0
    }
}

impl std::ops::Add for TimeDelta {
    type Output = TimeDelta;

    fn add(self, other: TimeDelta) -> TimeDelta {
        TimeDelta::add(&self, &other)
    }
}

impl std::ops::Sub for TimeDelta {
    type Output = TimeDelta;

    fn sub(self, other: TimeDelta) -> TimeDelta {
        TimeDelta::sub(&self, &other)
    }
}

impl std::ops::Neg for TimeDelta {
    type Output = TimeDelta;

    fn neg(self) -> TimeDelta {
        TimeDelta::neg(&self)
    }
}

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

    #[test]
    fn test_delta_days() {
        let d = TimeDelta::days(5);
        assert_eq!(d.total_seconds(), 5 * 86_400);
    }

    #[test]
    fn test_delta_hours() {
        let d = TimeDelta::hours(24);
        assert_eq!(d.total_seconds(), 86_400);
        assert_eq!(d.days, 1);
    }

    #[test]
    fn test_delta_minutes() {
        let d = TimeDelta::minutes(120);
        assert_eq!(d.total_seconds(), 7_200);
    }

    #[test]
    fn test_delta_seconds() {
        let d = TimeDelta::seconds(3600);
        assert_eq!(d.total_seconds(), 3600);
    }

    #[test]
    fn test_delta_milliseconds() {
        let d = TimeDelta::milliseconds(1500);
        assert_eq!(d.total_milliseconds(), 1500);
    }

    #[test]
    fn test_delta_add() {
        let d1 = TimeDelta::hours(12);
        let d2 = TimeDelta::hours(13);
        let sum = d1.add(&d2);
        assert_eq!(sum.total_seconds(), 25 * 3600);
    }

    #[test]
    fn test_delta_sub() {
        let d1 = TimeDelta::days(5);
        let d2 = TimeDelta::days(2);
        let diff = d1.sub(&d2);
        assert_eq!(diff.total_seconds(), 3 * 86_400);
    }

    #[test]
    fn test_delta_neg() {
        let d = TimeDelta::hours(5);
        let neg_d = d.neg();
        assert_eq!(neg_d.total_seconds(), -5 * 3600);
    }

    #[test]
    fn test_delta_normalization() {
        // 25 hours should normalize to 1 day + 1 hour
        let d = TimeDelta::hours(25);
        assert_eq!(d.days, 1);
        assert_eq!(d.total_seconds(), 25 * 3600);
    }

    #[test]
    fn test_delta_operators() {
        let d1 = TimeDelta::days(3);
        let d2 = TimeDelta::days(2);

        assert_eq!((d1 + d2).total_seconds(), 5 * 86_400);
        assert_eq!((d1 - d2).total_seconds(), 86_400);
        assert_eq!((-d1).total_seconds(), -3 * 86_400);
    }

    #[test]
    fn test_delta_zero() {
        let d = TimeDelta::days(0);
        assert!(d.is_zero());

        let d = TimeDelta::seconds(1);
        assert!(!d.is_zero());
    }
}