ako 0.0.3

Ako is a Rust crate that offers a practical and human-friendly approach to creating, manipulating, formatting and converting dates, times and timestamps.
Documentation
use core::fmt::{self, Debug, Formatter};

use crate::calendar::Iso;
use crate::{AsDate, Calendar, Month, PlainDateTime, PlainTime, Year};

// shift between the epoch base from Unix (1970-01-01) to Ako (0000-03-01)
const UNIX_TO_AKO: i32 = 719_468;

/// A **date** on a calendar, independent of any time zone.
///
/// This type can be used to refer to an event that spans an entire day, irrespective of the time zone.
/// An example of such an event is Christmas.
///
/// Supports dates before the year 5,869,411 and after the year -5,865,471.
///
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Date<C: Calendar = Iso> {
    calendar: C,
    // the number of days before, or after, 0000-03-01
    // the epoch was chosen to better align the algorithms used in this module
    // this is exactly 719,468 days from the Unix epoch of 1970-01-01
    days: i32,
}

/// Obtains a [`Date`] from a statically known `year`, `month`, and `day`.
///
/// Validation is done at compile-time.
///
/// Values may be used in a `const` or `static` declaration.
///
/// ```rust
/// # use ako::{Date, date};
/// # use ako::calendar::Iso;
/// #
/// const Y2K38: Date<Iso> = date!(2038, 1, 1);
/// #
/// # assert_eq!(Y2K38.format_rfc3339(), "2038-01-01");
/// ```
///
// #[macro_export]
macro_rules! date {
    ($year:expr, $month:expr, $day:expr) => {
        const {
            match $crate::Date::iso($year, $month, $day) {
                Ok(date) => date,
                Err(error) => error.panic(),
            }
        }
    };
}

// Construction: ISO
impl Date<Iso> {
    /// Obtains a [`Date`] for the `year`, `month`, and `day` on the ISO calendar.
    ///
    pub const fn iso(year: i32, month: u8, day: u8) -> crate::Result<Self> {
        Iso.date(year, month, day)
    }
}

// Construction
impl<C: Calendar> Date<C> {
    /// Obtains a [`Date`] for the `year`, `month`, and `day` on the `calendar`.
    ///
    pub fn of(calendar: C, year: i32, month: u8, day: u8) -> crate::Result<Self> {
        calendar.date(year, month, day)
    }

    /// Obtains a [`Date`] from the number of days since the Ako epoch, without any checks.
    #[allow(unsafe_code)]
    #[must_use]
    pub(crate) const unsafe fn unchecked_of(calendar: C, days: i32) -> Self {
        Self { calendar, days }
    }
}

// Conversion From
impl<C: Calendar> Date<C> {
    /// Creates a [`Date`] from a given year and day of the year (ordinal number)
    /// on the given `calendar`.
    pub fn from_ordinal_date(calendar: C, year: i32, day: u16) -> crate::Result<Self> {
        calendar.date_from_ordinal(year, day)
    }

    /// Creates a [`Date`] from the given number of days since January 1st, 1970 (the Unix epoch),
    /// on the given `calendar`.
    ///
    /// # Examples
    ///
    /// ```rust
    /// # use ako::Date;
    /// # use ako::calendar::Iso;
    /// #
    /// let date = Date::from_days_since_unix_epoch(Iso, 5);
    ///
    /// assert_eq!(date.year().number(), 1970);
    /// assert_eq!(date.month().number(), 1); // January
    /// assert_eq!(date.day(), 6);
    /// ```
    ///
    #[must_use]
    pub const fn from_days_since_unix_epoch(calendar: C, days: i32) -> Self {
        Self {
            calendar,
            days: days + UNIX_TO_AKO,
        }
    }

    /// Creates a [`Date`] from the given number of seconds since the Unix epoch,
    /// on the given `calendar`.
    ///
    #[must_use]
    pub const fn from_unix_timestamp(calendar: C, seconds: i64) -> Self {
        Self::from_days_since_unix_epoch(calendar, seconds.div_euclid(86_400) as i32)
    }
}

// Components
impl<C: Calendar> Date<C> {
    /// Gets the year, month, and day of this date.
    #[must_use]
    pub fn components(self) -> (Year<C>, Month<C>, u8) {
        self.calendar.date_components(self.days)
    }

    /// Gets the calendar associated with this date.
    #[must_use]
    pub const fn calendar(self) -> C {
        self.calendar
    }

    /// Gets the absolute year of this date.
    #[must_use]
    pub fn year(self) -> Year<C> {
        self.components().0
    }

    /// Gets the month within the year of this date.
    #[must_use]
    pub fn month(self) -> Month<C> {
        self.components().1
    }

    /// Gets the day within the month of this date.
    #[must_use]
    pub fn day(self) -> u8 {
        self.components().2
    }

    /// Gets the day within the year of this date.
    #[must_use]
    pub fn day_of_year(self) -> u16 {
        self.calendar.date_to_ordinal(self.days).1
    }
}

// Composition
impl<C: Calendar> Date<C> {
    /// Creates a new [`Date`] representing the same physical date,
    /// projected into the given `calendar`.
    ///
    #[must_use]
    pub const fn on<C2: Calendar>(self, calendar: C2) -> Date<C2> {
        Date::<C2> {
            calendar,
            days: self.days,
        }
    }

    /// Combines this date with the given time to create a [`PlainDateTime`].
    ///
    /// # Examples
    ///
    /// ```rust
    /// # use ako::{Date, PlainTime, PlainDateTime, AsDate, AsTime};
    /// # fn main() -> ako::Result<()> {
    /// let date = Date::iso(2020, 10, 2)?; // 2020-10-02
    /// let time = PlainTime::of(10, 1, 2); // 10:01:02
    /// let dt = date.with_time(time);
    ///
    /// assert_eq!(dt.as_date(), date);
    /// assert_eq!(dt.as_time(), time);
    /// # Ok(()) }
    /// ```
    ///
    #[must_use]
    pub fn with_time<T: Into<PlainTime>>(self, time: T) -> PlainDateTime<C> {
        PlainDateTime {
            date: self,
            time: time.into(),
        }
    }
}

// Conversion To
impl<C: Calendar> Date<C> {
    /// Gets the year and day of the year (ordinal number) of this date.
    ///
    /// # Examples
    ///
    /// ```rust
    /// # use ako::Date;
    /// # fn main() -> ako::Result<()> {
    /// let date = Date::iso(2030, 9, 1)?;
    /// let (year, ordinal) = date.to_ordinal_date();
    ///
    /// assert_eq!(year.number(), 2030);
    /// assert_eq!(ordinal, 244);
    /// # Ok(()) }
    /// ```
    ///
    #[must_use]
    pub fn to_ordinal_date(self) -> (Year<C>, u16) {
        self.calendar.date_to_ordinal(self.days)
    }

    /// Gets the number of days since January 1st, 1970 (the Unix epoch).
    ///
    /// # Examples
    ///
    /// ```rust
    /// # use ako::Date;
    /// # fn main() -> ako::Result<()> {
    /// let date = Date::iso(2024, 9, 1)?;
    ///
    /// assert_eq!(date.as_days_since_unix_epoch(), 19_967);
    /// # Ok(()) }
    /// ```
    ///
    #[must_use]
    pub const fn as_days_since_unix_epoch(self) -> i32 {
        self.days - UNIX_TO_AKO
    }

    /// Gets the number of days since March 1st, 3000 (the Ako epoch).
    #[must_use]
    pub(crate) const fn as_days_since_ako_epoch(self) -> i32 {
        self.days
    }
}

impl<C: Calendar> Debug for Date<C> {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        f.pad(&self.format_rfc3339())
    }
}

impl<C: Calendar> AsDate<C> for Date<C> {
    fn as_date(&self) -> Self {
        *self
    }
}

#[cfg(test)]
mod tests {
    use alloc::format;
    use alloc::string::ToString;

    use crate::{Date, YearMonth};

    #[test]
    fn expect_calendar_valid() -> crate::Result<()> {
        // check most valid permutations of the date to ensure we can create
        for year in -9999..=9999 {
            for month in 1..=12 {
                let year_month = YearMonth::iso(year, month)?;

                for day in 1..=year_month.days() {
                    let date = year_month.with_day(day)?;

                    // assert components

                    assert_eq!(date.year().number(), year);
                    assert_eq!(date.month().number(), month);
                    assert_eq!(date.day(), day);

                    // assert an RFC-3339 format

                    let year = if year >= 0 {
                        format!("{year:04}")
                    } else {
                        year.to_string()
                    };

                    let expected = format!("{}-{:02}-{:02}", year, month, day);

                    assert_eq!(date.format_rfc3339(), expected);
                    assert_eq!(Date::parse_rfc3339(&expected)?, date);
                }
            }
        }

        Ok(())
    }
}