spacecell 0.1.0

Datetime library with ISO8601/RFC3339 parsing, calendar arithmetic, and business day calculations
Documentation
//! # **Construct Module** - *Build timestamps from components*
//!
//! Provides functions to construct timestamps from date/time components with validation.

use super::civil::{days_in_month, ymd_to_days};

/// Construction errors
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ConstructError {
    InvalidDate { year: i32, month: u32, day: u32 },
    InvalidTime {
        hour: u32,
        minute: u32,
        second: u32,
        nanosecond: u32,
    },
}

impl std::fmt::Display for ConstructError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ConstructError::InvalidDate { year, month, day } => {
                write!(f, "Invalid date: {}-{:02}-{:02}", year, month, day)
            }
            ConstructError::InvalidTime {
                hour,
                minute,
                second,
                nanosecond,
            } => write!(
                f,
                "Invalid time: {:02}:{:02}:{:02}.{:09}",
                hour, minute, second, nanosecond
            ),
        }
    }
}

impl std::error::Error for ConstructError {}

/// Validate date components
#[inline]
fn validate_date(year: i32, month: u32, day: u32) -> Result<(), ConstructError> {
    if month < 1 || month > 12 {
        return Err(ConstructError::InvalidDate { year, month, day });
    }

    let max_day = days_in_month(year, month);
    if day < 1 || day > max_day {
        return Err(ConstructError::InvalidDate { year, month, day });
    }

    Ok(())
}

/// Validate time components
#[inline]
fn validate_time(
    hour: u32,
    minute: u32,
    second: u32,
    nanosecond: u32,
) -> Result<(), ConstructError> {
    if hour >= 24 || minute >= 60 || second >= 60 || nanosecond >= 1_000_000_000 {
        return Err(ConstructError::InvalidTime {
            hour,
            minute,
            second,
            nanosecond,
        });
    }
    Ok(())
}

/// Construct timestamp from year, month, day (midnight UTC)
///
/// # Returns
/// Timestamp in seconds since Unix epoch
pub fn from_ymd(year: i32, month: u32, day: u32) -> Result<i64, ConstructError> {
    validate_date(year, month, day)?;
    let days = ymd_to_days(year, month, day);
    Ok(days * 86400)
}

/// Construct timestamp from year, month, day, hour, minute, second
///
/// # Returns
/// Timestamp in seconds since Unix epoch
pub fn from_ymd_hms(
    year: i32,
    month: u32,
    day: u32,
    hour: u32,
    minute: u32,
    second: u32,
) -> Result<i64, ConstructError> {
    validate_date(year, month, day)?;
    validate_time(hour, minute, second, 0)?;

    let days = ymd_to_days(year, month, day);
    let seconds_in_day = hour * 3600 + minute * 60 + second;

    Ok(days * 86400 + seconds_in_day as i64)
}

/// Construct timestamp from year, month, day, hour, minute, second, nanosecond
///
/// # Returns
/// Timestamp in nanoseconds since Unix epoch
pub fn from_ymd_hms_nano(
    year: i32,
    month: u32,
    day: u32,
    hour: u32,
    minute: u32,
    second: u32,
    nanosecond: u32,
) -> Result<i64, ConstructError> {
    validate_date(year, month, day)?;
    validate_time(hour, minute, second, nanosecond)?;

    let days = ymd_to_days(year, month, day);
    let seconds_in_day = hour * 3600 + minute * 60 + second;

    Ok(days * 86_400_000_000_000 + seconds_in_day as i64 * 1_000_000_000 + nanosecond as i64)
}

/// Construct timestamp from Unix timestamp in seconds
#[inline]
pub fn from_timestamp_seconds(secs: i64) -> i64 {
    secs
}

/// Construct timestamp from Unix timestamp in milliseconds
#[inline]
pub fn from_timestamp_millis(millis: i64) -> i64 {
    millis
}

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

    #[test]
    fn test_from_ymd_epoch() {
        let ts = from_ymd(1970, 1, 1).unwrap();
        assert_eq!(ts, 0);
    }

    #[test]
    fn test_from_ymd() {
        let ts = from_ymd(2000, 1, 1).unwrap();
        assert_eq!(ts, 946684800);
    }

    #[test]
    fn test_from_ymd_invalid_month() {
        assert!(from_ymd(2000, 13, 1).is_err());
        assert!(from_ymd(2000, 0, 1).is_err());
    }

    #[test]
    fn test_from_ymd_invalid_day() {
        assert!(from_ymd(2000, 2, 30).is_err()); // Feb only has 29 days in 2000
        assert!(from_ymd(2001, 2, 29).is_err()); // Not a leap year
        assert!(from_ymd(2000, 4, 31).is_err()); // April has 30 days
    }

    #[test]
    fn test_from_ymd_leap_day() {
        assert!(from_ymd(2000, 2, 29).is_ok());
        assert!(from_ymd(2004, 2, 29).is_ok());
    }

    #[test]
    fn test_from_ymd_hms() {
        let ts = from_ymd_hms(1970, 1, 1, 0, 0, 0).unwrap();
        assert_eq!(ts, 0);

        let ts = from_ymd_hms(2000, 1, 1, 12, 34, 56).unwrap();
        assert_eq!(ts, 946730096);
    }

    #[test]
    fn test_from_ymd_hms_invalid_time() {
        assert!(from_ymd_hms(2000, 1, 1, 24, 0, 0).is_err());
        assert!(from_ymd_hms(2000, 1, 1, 12, 60, 0).is_err());
        assert!(from_ymd_hms(2000, 1, 1, 12, 34, 60).is_err());
    }

    #[test]
    fn test_from_ymd_hms_nano() {
        let ts = from_ymd_hms_nano(1970, 1, 1, 0, 0, 0, 123456789).unwrap();
        assert_eq!(ts, 123456789);

        let ts = from_ymd_hms_nano(1970, 1, 1, 0, 0, 1, 0).unwrap();
        assert_eq!(ts, 1_000_000_000);
    }

    #[test]
    fn test_from_ymd_hms_nano_invalid() {
        assert!(from_ymd_hms_nano(2000, 1, 1, 12, 34, 56, 1_000_000_000).is_err());
    }
}