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::{Calendar, Date, YearMonth};

/// A **year** on a calendar.
///
/// This type does not store or represent a month, day, time, or time-zone.
///
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Year<C: Calendar = Iso> {
    calendar: C,
    // absolute year number
    // for ISO, “-15” would refer to “16 BCE”
    number: i32,
}

/// Obtains a [`Year`] from a statically known number.
///
/// Validation is done at compile-time.
///
/// Values may be used in a `const` or `static` declaration.
///
/// ```rust
/// # use ako::{Year, year};
/// # use ako::calendar::Iso;
/// #
/// const COPYRIGHT: Year<Iso> = year!(2024);
/// #
/// # assert_eq!(COPYRIGHT.number(), 2024);
/// ```
///
// #[macro_export]
macro_rules! year {
    ($number:expr) => {
        const {
            match $crate::Year::iso($number) {
                Ok(year) => year,
                Err(error) => error.panic(),
            }
        }
    };
}

// Construction: ISO
impl Year<Iso> {
    /// Obtains an ISO year.
    pub const fn iso(number: i32) -> crate::Result<Self> {
        Iso.year(number)
    }
}

// Construction
impl<C: Calendar> Year<C> {
    /// Obtains a year on the given calendar.
    pub fn of(calendar: C, number: i32) -> crate::Result<Self> {
        calendar.year(number)
    }

    /// Obtains a year on the given calendar, without any checks.
    #[allow(unsafe_code)]
    #[must_use]
    pub(crate) const unsafe fn unchecked_of(calendar: C, number: i32) -> Self {
        debug_assert!(number <= 5_000_000);
        debug_assert!(number >= -5_000_000);

        Self { calendar, number }
    }
}

// Components
impl<C: Calendar> Year<C> {
    /// Gets the calendar that this year is interpreted in.
    #[must_use]
    pub const fn calendar(self) -> C {
        self.calendar
    }

    /// Gets the year number.
    #[must_use]
    pub const fn number(self) -> i32 {
        self.number
    }

    // TODO: pub const fn era(self) -> Era { ... }
    // TODO: pub const fn era_relative(self) -> u32 { ... }
}

// Queries
impl<C: Calendar> Year<C> {
    // TODO: Return YearDayIter (ExactSize, BiDirectional)
    /// Gets the number of days in this year.
    #[must_use]
    pub fn days(self) -> u16 {
        self.calendar.year_days(self.number)
    }

    // TODO: Return YearMonthIter (ExactSize, BiDirectional)
    /// Gets the number of months in this year.
    #[must_use]
    pub fn months(self) -> u8 {
        self.calendar.year_months(self.number)
    }

    /// Determines if this year is a leap year.
    ///
    /// # Examples
    ///
    /// ```rust
    /// # use ako::Year;
    /// # fn main() -> ako::Result<()> {
    /// assert!(Year::iso(2024)?.is_leap());
    /// assert!(!Year::iso(2025)?.is_leap());
    /// assert!(Year::iso(2000)?.is_leap());
    /// # Ok(()) }
    /// ```
    ///
    #[must_use]
    pub fn is_leap(self) -> bool {
        self.calendar.year_is_leap(self.number)
    }
}

// Astronomy
#[cfg(feature = "astronomy")]
impl<C: Calendar> Year<C> {
    /// Gets the near-exact date-time of the [northward equinox] for this year.
    ///
    /// The [northward equinox] (or March equinox)
    /// is the moment when the Sun appears to cross the celestial equator, heading northward.
    ///
    /// [northward equinox]: https://en.wikipedia.org/wiki/March_equinox
    ///
    // TODO: Return ZonedDateTime to UTC
    #[allow(unsafe_code)]
    #[doc(alias = "march_equinox")]
    #[doc(alias = "march")]
    pub fn northward_equinox(self) -> crate::DateTime<C> {
        let jde = crate::astronomy::solstice::march(self.number);
        // SAFETY: all values returned from dates within supported ranges are guaranteed to be valid
        let moment = unsafe { crate::Moment::from_julian_ephemeris_day(jde).unwrap_unchecked() };

        moment.on(self.calendar)
    }

    /// Gets the near-exact date-time of the [northern solstice] for this year.
    ///
    /// The [northern solstice] (or June solstice)
    /// is the moment when the Sun is directly over the Tropic of Cancer,
    /// located in the Northern Hemisphere.
    ///
    /// [northern solstice]: https://en.wikipedia.org/wiki/June_solstice
    ///
    // TODO: Return ZonedDateTime to UTC
    #[allow(unsafe_code)]
    #[doc(alias = "june_solstice")]
    #[doc(alias = "june")]
    pub fn northern_solstice(self) -> crate::DateTime<C> {
        let jde = crate::astronomy::solstice::june(self.number);
        // SAFETY: all values returned from dates within supported ranges are guaranteed to be valid
        let moment = unsafe { crate::Moment::from_julian_ephemeris_day(jde).unwrap_unchecked() };

        moment.on(self.calendar)
    }

    /// Gets the near-exact date-time of the [southward equinox] for this year.
    ///
    /// The [southward equinox] (or September equinox)
    /// is the moment when the Sun appears to cross the celestial equator, heading southward.
    ///
    /// [southward equinox]: https://en.wikipedia.org/wiki/September_equinox
    ///
    // TODO: Return ZonedDateTime to UTC
    #[allow(unsafe_code)]
    #[doc(alias = "september_equinox")]
    #[doc(alias = "september")]
    pub fn southward_equinox(self) -> crate::DateTime<C> {
        let jde = crate::astronomy::solstice::september(self.number);
        // SAFETY: all values returned from dates within supported ranges are guaranteed to be valid
        let moment = unsafe { crate::Moment::from_julian_ephemeris_day(jde).unwrap_unchecked() };

        moment.on(self.calendar)
    }

    /// Gets the near-exact date-time of the [southern solstice] for this year.
    ///
    /// The [southern solstice] (or December solstice)
    /// is the moment when the Sun is directly over the Tropic of Capricorn,
    /// located in the Southern Hemisphere.
    ///
    /// [southern solstice]: https://en.wikipedia.org/wiki/December_solstice
    ///
    // TODO: Return ZonedDateTime to UTC
    #[allow(unsafe_code)]
    #[doc(alias = "december_solstice")]
    #[doc(alias = "december")]
    pub fn southern_solstice(self) -> crate::DateTime<C> {
        let jde = crate::astronomy::solstice::december(self.number);
        // SAFETY: all values returned from dates within supported ranges are guaranteed to be valid
        let moment = unsafe { crate::Moment::from_julian_ephemeris_day(jde).unwrap_unchecked() };

        moment.on(self.calendar)
    }
}

// Composition
impl<C: Calendar> Year<C> {
    /// Combines this year with the given month to create a [`YearMonth`].
    pub fn with_month(self, month: u8) -> crate::Result<YearMonth<C>> {
        YearMonth::of(self.calendar, self.number, month)
    }

    /// Combines this year with the given day of the year to create a [`Date`].
    pub fn with_day(self, day: u16) -> crate::Result<Date<C>> {
        Date::from_ordinal_date(self.calendar, self.number, day)
    }
}

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

#[cfg(test)]
mod tests {
    #[cfg(feature = "astronomy")]
    #[test]
    fn expect_year_solstice() {
        use crate::year;

        // TODO: the solstice functions should return a ZonedDateTime in UTC
        let southern_solstice_2024 = year!(2024).southern_solstice();
        let southern_solstice_2036 = year!(2036).southern_solstice();
        let southern_solstice_2020 = year!(2020).southern_solstice();

        // be careful when confirming these values
        // many online sources are +/- one second due to the confusion between UT1 and UTC

        // https://www.timeanddate.com/calendar/december-solstice.html

        assert_eq!(
            southern_solstice_2020.format_rfc3339(),
            "2020-12-21T10:02:16.414185047Z"
        );

        assert_eq!(
            southern_solstice_2024.format_rfc3339(),
            "2024-12-21T09:20:28.207420766Z"
        );

        assert_eq!(
            southern_solstice_2036.format_rfc3339(),
            "2036-12-21T07:12:38.636344254Z"
        );
    }
}