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, Month, Year};

/// A **year and month** on the calendar.
///
/// This type doesn't store or represent a day, time, or time-zone.
///
#[derive(Copy, Clone)]
pub struct YearMonth<C: Calendar = Iso> {
    pub(crate) calendar: C,
    pub(crate) year: i32,
    pub(crate) month: u8,
}

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

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

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

    /// Gets the year.
    #[allow(unsafe_code)]
    #[must_use]
    pub const fn year(self) -> Year<C> {
        // SAFETY: year is pre-validated
        unsafe { Year::unchecked_of(self.calendar, self.year) }
    }

    // TODO: fn era() -> Era<C>
    // TODO: fn year_of_era() -> u32

    /// Gets the month.
    #[allow(unsafe_code)]
    #[must_use]
    pub const fn month(self) -> Month<C> {
        // SAFETY: month is pre-validated
        unsafe { Month::unchecked_of(self.calendar, self.month) }
    }
}

// Getters
impl<C: Calendar> YearMonth<C> {
    // TODO: Return YearMonthDayIter (ExactSize, BiDirectional)
    /// Gets the number of days in this year/month.
    #[must_use]
    pub fn days(self) -> u8 {
        self.calendar.year_month_days(self.year, self.month)
    }
}

// Composition
impl<C: Calendar> YearMonth<C> {
    /// Combines this year/month with the day to create a date.
    pub fn with_day(self, day: u8) -> crate::Result<Date<C>> {
        Date::of(self.calendar, self.year, self.month, day)
    }
}

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

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

    use test_case::test_case;

    use crate::Year;

    #[test_case(2000)]
    #[test_case(2024)]
    #[test_case(2025)]
    fn expect_year_month_days(year: i32) -> crate::Result<()> {
        let year = Year::iso(year)?;

        let days = [
            31,
            if year.is_leap() { 29 } else { 28 },
            31,
            30,
            31,
            30,
            31,
            31,
            30,
            31,
            30,
            31,
        ];

        for month in 1..=12 {
            let ym = year.with_month(month)?;

            assert_eq!(ym.year(), year);
            assert_eq!(ym.month().number(), month);
            assert_eq!(ym.format_rfc3339(), format!("{:?}-{:02}", year, month));
            assert_eq!(ym.days(), days[month as usize - 1]);
        }

        Ok(())
    }
}