rstime 0.1.0

A zero-dependency Rust time library providing date, time, datetime types with formatting, parsing, Unix timestamps, and clock functionality.
Documentation
//! Date module
//!
//! Provides `Date` and `Weekday` types with validation, weekday calculation
//! (Zeller's formula), and date arithmetic.

use crate::duration::TimeDelta;
use crate::time::Time;
use std::fmt;

/// Days of the week
///
/// # Examples
///
/// ```rust
/// use rstime::Weekday;
///
/// assert_eq!(Weekday::Monday.to_string(), "Monday");
/// assert_eq!(Weekday::from_u8(1), Some(Weekday::Monday));
/// assert_eq!(Weekday::Sunday.to_u8(), 7);
/// ```
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
pub enum Weekday {
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday,
    Sunday,
}

impl Weekday {
    /// Convert from a number (1=Monday, 7=Sunday)
    pub fn from_u8(n: u8) -> Option<Weekday> {
        match n {
            1 => Some(Weekday::Monday),
            2 => Some(Weekday::Tuesday),
            3 => Some(Weekday::Wednesday),
            4 => Some(Weekday::Thursday),
            5 => Some(Weekday::Friday),
            6 => Some(Weekday::Saturday),
            7 => Some(Weekday::Sunday),
            _ => None,
        }
    }

    /// Convert to a number (1=Monday, 7=Sunday)
    pub fn to_u8(self) -> u8 {
        match self {
            Weekday::Monday => 1,
            Weekday::Tuesday => 2,
            Weekday::Wednesday => 3,
            Weekday::Thursday => 4,
            Weekday::Friday => 5,
            Weekday::Saturday => 6,
            Weekday::Sunday => 7,
        }
    }
}

impl fmt::Display for Weekday {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let s = match self {
            Weekday::Monday => "Monday",
            Weekday::Tuesday => "Tuesday",
            Weekday::Wednesday => "Wednesday",
            Weekday::Thursday => "Thursday",
            Weekday::Friday => "Friday",
            Weekday::Saturday => "Saturday",
            Weekday::Sunday => "Sunday",
        };
        write!(f, "{}", s)
    }
}

/// A calendar date
///
/// Represents a date with year, month, and day. Supports validation,
/// weekday calculation, day-of-year, and arithmetic with [`TimeDelta`].
///
/// # Examples
///
/// ```rust
/// use rstime::Date;
///
/// let date = Date::new(2026, 5, 10);
/// assert!(date.is_valid());
/// assert_eq!(date.year(), 2026);
/// assert_eq!(date.month(), 5);
/// assert_eq!(date.day(), 10);
/// ```
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)]
pub struct Date {
    /// Year (supports negative values for BCE)
    pub year: i32,
    /// Month (1-12)
    pub month: u8,
    /// Day (1-31, depending on month)
    pub day: u8,
}

impl Date {
    /// Create a new date
    pub fn new(year: i32, month: u8, day: u8) -> Self {
        Date { year, month, day }
    }

    /// Create a new date (alias for `new`)
    pub fn from_ymd(year: i32, month: u8, day: u8) -> Self {
        Date::new(year, month, day)
    }

    /// Check if the date is valid
    ///
    /// Validates month range (1-12) and day range for the given month,
    /// accounting for leap years.
    pub fn is_valid(&self) -> bool {
        if self.month < 1 || self.month > 12 {
            return false;
        }
        if self.day < 1 || self.day > days_in_month(self.year, self.month) {
            return false;
        }
        true
    }

    /// Returns the year
    pub fn year(&self) -> i32 {
        self.year
    }

    /// Returns the month (1-12)
    pub fn month(&self) -> u8 {
        self.month
    }

    /// Returns the day (1-31)
    pub fn day(&self) -> u8 {
        self.day
    }

    /// Check if this is a leap year
    pub fn is_leap_year(&self) -> bool {
        is_leap_year(self.year)
    }

    /// Calculate the day of the week using Zeller's formula
    ///
    /// ```rust
    /// use rstime::{Date, Weekday};
    ///
    /// let date = Date::new(2026, 5, 10);
    /// assert_eq!(date.weekday(), Weekday::Sunday);
    /// ```
    pub fn weekday(&self) -> Weekday {
        let (mut y, m) = if self.month <= 2 {
            (self.year - 1, self.month + 12)
        } else {
            (self.year, self.month)
        };
        let c = y / 100;
        y %= 100;
        let w = (self.day as i32 + (13 * (m as i32 + 1)) / 5 + y + y / 4 + c / 4 - 2 * c) % 7;
        let w = ((w + 7) % 7) as u8;
        match w {
            0 => Weekday::Saturday,
            1 => Weekday::Sunday,
            2 => Weekday::Monday,
            3 => Weekday::Tuesday,
            4 => Weekday::Wednesday,
            5 => Weekday::Thursday,
            6 => Weekday::Friday,
            _ => Weekday::Monday,
        }
    }

    /// Day of the year (1-366)
    ///
    /// ```rust
    /// use rstime::Date;
    ///
    /// let d = Date::new(2025, 3, 1);
    /// assert_eq!(d.day_of_year(), 60);
    /// ```
    pub fn day_of_year(&self) -> u16 {
        let mut days = 0u16;
        for m in 1..self.month {
            days += days_in_month(self.year, m) as u16;
        }
        days + self.day as u16
    }

    /// Number of days in the current month
    pub fn days_in_month(&self) -> u8 {
        days_in_month(self.year, self.month)
    }

    /// Combine with a [`Time`] to create a [`DateTime`](crate::datetime::DateTime)
    pub fn at_time(&self, time: Time) -> crate::datetime::DateTime {
        crate::datetime::DateTime::new(*self, time)
    }
}

/// Returns `true` if the given year is a leap year
pub fn is_leap_year(year: i32) -> bool {
    (year % 4 == 0 && year % 100 != 0) || year % 400 == 0
}

/// Returns the number of days in the given month
pub fn days_in_month(year: i32, month: u8) -> u8 {
    match month {
        1 => 31,
        2 => {
            if is_leap_year(year) {
                29
            } else {
                28
            }
        }
        3 => 31,
        4 => 30,
        5 => 31,
        6 => 30,
        7 => 31,
        8 => 31,
        9 => 30,
        10 => 31,
        11 => 30,
        12 => 31,
        _ => 0,
    }
}

/// Number of days before the start of the given year
pub fn days_before_year(year: i32) -> i64 {
    let y = year as i64 - 1;
    y * 365 + y / 4 - y / 100 + y / 400
}

/// Days from epoch (Jan 1, 0001) to the given date
pub fn days_from_epoch(date: Date) -> i64 {
    let mut days = days_before_year(date.year);
    for m in 1..date.month {
        days += days_in_month(date.year, m) as i64;
    }
    days + date.day as i64 - 1
}

/// Convert days from epoch back to a [`Date`]
pub fn date_from_days(days: i64) -> Date {
    let mut y = (days / 146097 * 400 + 1) as i64;
    while y > i32::MAX as i64 {
        y -= 1;
    }
    let mut yi = y as i32;
    while days_before_year(yi) > days {
        yi -= 1;
    }
    while days_before_year(yi + 1) <= days {
        yi += 1;
    }
    let mut remaining = (days - days_before_year(yi)) as u16;
    let mut m: u8 = 1;
    while m <= 12 {
        let dm = days_in_month(yi, m) as u16;
        if remaining < dm {
            break;
        }
        remaining -= dm;
        m += 1;
    }
    Date {
        year: yi,
        month: m,
        day: (remaining + 1) as u8,
    }
}

impl fmt::Display for Date {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
    }
}

impl std::ops::Add<TimeDelta> for Date {
    type Output = Date;
    fn add(self, delta: TimeDelta) -> Date {
        date_from_days(days_from_epoch(self) + delta.total_seconds() as i64 / 86400)
    }
}

impl std::ops::Sub<TimeDelta> for Date {
    type Output = Date;
    fn sub(self, delta: TimeDelta) -> Date {
        date_from_days(days_from_epoch(self) - delta.total_seconds() as i64 / 86400)
    }
}

impl std::ops::Sub<Date> for Date {
    type Output = TimeDelta;
    fn sub(self, rhs: Date) -> TimeDelta {
        let days = days_from_epoch(self) - days_from_epoch(rhs);
        TimeDelta::new(days * 86400, 0)
    }
}