spacecell 0.1.0

Datetime library with ISO8601/RFC3339 parsing, calendar arithmetic, and business day calculations
Documentation
//! # **Components Module** - *Extract date/time components from timestamps*
//!
//! Provides component extraction and construction for timestamps in various units.

use super::civil::{days_to_ymd, ymd_to_days};
use crate::time_units::TimeUnit;

/// Date and time components.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DateTimeComponents {
    pub year: i32,
    pub month: u32,      // 1-12
    pub day: u32,        // 1-31
    pub hour: u32,       // 0-23
    pub minute: u32,     // 0-59
    pub second: u32,     // 0-59
    pub nanosecond: u32, // 0-999_999_999
}

/// Extracts date and time components from a timestamp.
///
/// # Arguments
/// - `timestamp`: Raw timestamp value
/// - `time_unit`: Unit of the timestamp
///
/// # Returns
/// DateTimeComponents with extracted values
#[inline]
pub fn extract_components(timestamp: i64, time_unit: TimeUnit) -> DateTimeComponents {
    // Convert to seconds and nanoseconds
    let (total_seconds, nanos) = match time_unit {
        TimeUnit::Seconds => (timestamp, 0),
        TimeUnit::Milliseconds => {
            let secs = timestamp / 1_000;
            let millis = (timestamp % 1_000) as i32;
            let nanos = if millis < 0 {
                ((millis + 1_000) * 1_000_000) as u32
            } else {
                (millis * 1_000_000) as u32
            };
            (secs, nanos)
        }
        TimeUnit::Microseconds => {
            let secs = timestamp / 1_000_000;
            let micros = (timestamp % 1_000_000) as i32;
            let nanos = if micros < 0 {
                ((micros + 1_000_000) * 1_000) as u32
            } else {
                (micros * 1_000) as u32
            };
            (secs, nanos)
        }
        TimeUnit::Nanoseconds => {
            let secs = timestamp / 1_000_000_000;
            let ns = (timestamp % 1_000_000_000) as i32;
            let nanos = if ns < 0 {
                (ns + 1_000_000_000) as u32
            } else {
                ns as u32
            };
            (secs, nanos)
        }
        TimeUnit::Days => {
            // Days are whole days, no time component
            return DateTimeComponents {
                year: 0,
                month: 0,
                day: 0,
                hour: 0,
                minute: 0,
                second: 0,
                nanosecond: 0,
            };
        }
    };

    // Extract date from days (use div_euclid for floor division)
    let days = total_seconds.div_euclid(86400);
    let (year, month, day) = days_to_ymd(days);

    // Extract time from seconds within day (use rem_euclid for positive remainder)
    let seconds_in_day = total_seconds.rem_euclid(86400) as u32;

    let hour = seconds_in_day / 3600;
    let minute = (seconds_in_day % 3600) / 60;
    let second = seconds_in_day % 60;

    DateTimeComponents {
        year,
        month,
        day,
        hour,
        minute,
        second,
        nanosecond: nanos,
    }
}

/// Constructs a timestamp from date/time components.
///
/// # Arguments
/// - `comp`: DateTimeComponents
/// - `time_unit`: Desired unit for the output timestamp
///
/// # Returns
/// Timestamp in the specified unit
#[inline]
pub fn components_to_timestamp(comp: &DateTimeComponents, time_unit: TimeUnit) -> i64 {
    // Convert date to days
    let days = ymd_to_days(comp.year, comp.month, comp.day);

    // Convert time to seconds within day
    let seconds_in_day = comp.hour * 3600 + comp.minute * 60 + comp.second;

    // Total seconds
    let total_seconds = days * 86400 + seconds_in_day as i64;

    // Convert to target unit
    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,
    }
}

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

    #[test]
    fn test_extract_components_seconds() {
        // 1970-01-01 00:00:00
        let comp = extract_components(0, TimeUnit::Seconds);
        assert_eq!(comp.year, 1970);
        assert_eq!(comp.month, 1);
        assert_eq!(comp.day, 1);
        assert_eq!(comp.hour, 0);
        assert_eq!(comp.minute, 0);
        assert_eq!(comp.second, 0);

        // 2000-01-01 12:34:56
        let ts = ymd_to_days(2000, 1, 1) * 86400 + 12 * 3600 + 34 * 60 + 56;
        let comp = extract_components(ts, TimeUnit::Seconds);
        assert_eq!(comp.year, 2000);
        assert_eq!(comp.month, 1);
        assert_eq!(comp.day, 1);
        assert_eq!(comp.hour, 12);
        assert_eq!(comp.minute, 34);
        assert_eq!(comp.second, 56);
    }

    #[test]
    fn test_extract_components_milliseconds() {
        // 1970-01-01 00:00:00.123
        let comp = extract_components(123, TimeUnit::Milliseconds);
        assert_eq!(comp.year, 1970);
        assert_eq!(comp.month, 1);
        assert_eq!(comp.day, 1);
        assert_eq!(comp.hour, 0);
        assert_eq!(comp.minute, 0);
        assert_eq!(comp.second, 0);
        assert_eq!(comp.nanosecond, 123_000_000);
    }

    #[test]
    fn test_extract_components_microseconds() {
        // 1970-01-01 00:00:00.000123
        let comp = extract_components(123, TimeUnit::Microseconds);
        assert_eq!(comp.year, 1970);
        assert_eq!(comp.month, 1);
        assert_eq!(comp.day, 1);
        assert_eq!(comp.hour, 0);
        assert_eq!(comp.minute, 0);
        assert_eq!(comp.second, 0);
        assert_eq!(comp.nanosecond, 123_000);
    }

    #[test]
    fn test_extract_components_nanoseconds() {
        // 1970-01-01 00:00:00.000000123
        let comp = extract_components(123, TimeUnit::Nanoseconds);
        assert_eq!(comp.year, 1970);
        assert_eq!(comp.month, 1);
        assert_eq!(comp.day, 1);
        assert_eq!(comp.hour, 0);
        assert_eq!(comp.minute, 0);
        assert_eq!(comp.second, 0);
        assert_eq!(comp.nanosecond, 123);
    }

    #[test]
    fn test_components_to_timestamp_roundtrip() {
        let test_cases = vec![
            (0i64, TimeUnit::Seconds),
            (1_700_000_000, TimeUnit::Seconds),
            (1_700_000_000_000, TimeUnit::Milliseconds),
            (1_700_000_000_000_000, TimeUnit::Microseconds),
            (1_700_000_000_000_000_000, TimeUnit::Nanoseconds),
        ];

        for (ts, unit) in test_cases {
            let comp = extract_components(ts, unit);
            let ts2 = components_to_timestamp(&comp, unit);
            assert_eq!(ts, ts2, "Round-trip failed for timestamp {} {:?}", ts, unit);
        }
    }

    #[test]
    fn test_components_to_timestamp() {
        let comp = DateTimeComponents {
            year: 2000,
            month: 1,
            day: 1,
            hour: 12,
            minute: 34,
            second: 56,
            nanosecond: 123_456_789,
        };

        // Test seconds (nanoseconds truncated)
        let ts_sec = components_to_timestamp(&comp, TimeUnit::Seconds);
        let expected_sec = ymd_to_days(2000, 1, 1) * 86400 + 12 * 3600 + 34 * 60 + 56;
        assert_eq!(ts_sec, expected_sec);

        // Test milliseconds
        let ts_ms = components_to_timestamp(&comp, TimeUnit::Milliseconds);
        let expected_ms = expected_sec * 1_000 + 123;
        assert_eq!(ts_ms, expected_ms);

        // Test microseconds
        let ts_us = components_to_timestamp(&comp, TimeUnit::Microseconds);
        let expected_us = expected_sec * 1_000_000 + 123_456;
        assert_eq!(ts_us, expected_us);

        // Test nanoseconds
        let ts_ns = components_to_timestamp(&comp, TimeUnit::Nanoseconds);
        let expected_ns = expected_sec * 1_000_000_000 + 123_456_789;
        assert_eq!(ts_ns, expected_ns);
    }

    #[test]
    fn test_negative_timestamps() {
        // 1969-12-31 23:59:59 (one second before epoch)
        let comp = extract_components(-1, TimeUnit::Seconds);
        assert_eq!(comp.year, 1969);
        assert_eq!(comp.month, 12);
        assert_eq!(comp.day, 31);
        assert_eq!(comp.hour, 23);
        assert_eq!(comp.minute, 59);
        assert_eq!(comp.second, 59);
    }
}