sac13 0.1.1

The reference implementation for the SAC13 calendar system.
Documentation
use core::fmt::Display;

/// Represents a month on the SAC13 calendar.
///
/// Months are practically the same as in the Gregorian Calendar.
/// They have the same names and order. The two main differences are that
/// SAC13 starts its year with March (so March is the 1st month) and SAC13 has
/// 13 months and this additional month is called "Addenduary" and placed after
/// February.
#[repr(u8)]
#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord, Hash)]
#[allow(missing_docs)]
pub enum Month {
    March = 0,
    April = 1,
    May = 2,
    June = 3,
    July = 4,
    August = 5,
    September = 6,
    October = 7,
    November = 8,
    December = 9,
    January = 10,
    February = 11,
    Addenduary = 12,
}

impl Month {
    /// Month from its ordinal number _(valid are 1-13, both inclusive)_.
    ///
    /// Returns `None` for invalid ordinals.
    #[must_use]
    pub const fn new(m: u8) -> Option<Self> {
        if m >= 1 && m <= 13 {
            Some(Self::idx0_to_month(m - 1))
        } else {
            None
        }
    }

    /// The ordinal number of the month.
    ///
    /// Note that those are different from the Gregorian Calendar.  
    /// March = 1, April = 2, ... February = 12, Addenduary = 13
    #[must_use]
    pub const fn ord(self) -> u8 {
        self.idx0() + 1
    }

    #[must_use]
    /// Returns the next months. Effectively like calling [Month::next] `rhs` times.
    /// 
    /// `.add(0)` return the current month, `.add(1)` the next month,
    /// `.add(2)` the one after that, and so on.
    pub const fn add(self, rhs: u8) -> Self {
        // we have to reduce (mod 13) two times.
        // 1) we reduce the right hand side (rhs) to prevent overflow during addition
        // 2) to reduce the sum to be between 0 and 12 (incl.)

        let rhs = rhs.rem_euclid(13);
        let sum = self.idx0() + rhs;
        Self::idx0_to_month(sum.rem_euclid(13))
    }

    #[must_use]
    /// Returns the previous months. Effectively like calling [Month::previous] `rhs` times.
    /// 
    /// `.sub(0)` return the current month, `.sub(1)` the previous month,
    /// `.sub(2)` the one before that, and so on.
    pub const fn sub(self, rhs: u8) -> Self {
        // to prevent over/underflow we reduce the rhs and reuse add.
        self.add(13 - rhs.rem_euclid(13))
    }

    #[must_use]
    /// Returns the next month (including over year boundries).
    pub const fn next(self) -> Self {
        self.add(1)
    }

    #[must_use]
    /// Returns the previous month (including over year boundries).
    pub const fn previous(self) -> Self {
        // Subtracting 1 is the same as adding 12!
        self.add(12)
    }

    /// Full name of the month _(international, English)_.
    ///
    /// March, April, May, ...
    #[must_use]
    pub const fn name(self) -> &'static str {
        use Month::*;

        match self {
            March => "March",
            April => "April",
            May => "May",
            June => "June",
            July => "July",
            August => "August",
            September => "September",
            October => "October",
            November => "November",
            December => "December",
            January => "January",
            February => "February",
            Addenduary => "Addenduary",
        }
    }

    const fn idx0(self) -> u8 {
        self as u8
    }

    const fn idx0_to_month(m: u8) -> Month {
        use Month::*;

        match m {
            0 => March,
            1 => April,
            2 => May,
            3 => June,
            4 => July,
            5 => August,
            6 => September,
            7 => October,
            8 => November,
            9 => December,
            10 => January,
            11 => February,
            12 => Addenduary,
            _ => panic!(),
        }
    }
}

impl Display for Month {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write!(f, "{}", self.name())
    }
}

// Helper macro to implement traits for numeric data types.
macro_rules! num_trait_impl {
    ($type:ident) => {
        impl From<Month> for $type {
            fn from(value: Month) -> Self {
                value.ord() as $type
            }
        }

        impl TryFrom<$type> for Month {
            type Error = ();

            fn try_from(value: $type) -> Result<Self, Self::Error> {
                if (1..=13).contains(&value) {
                    return Ok(Month::new(value as u8).unwrap());
                } else {
                    Err(())
                }
            }
        }

        impl core::ops::Add<$type> for Month {
            type Output = Month;

            fn add(self, rhs: $type) -> Month {
                let reduced = rhs.rem_euclid(13) as u8;
                Self::add(self, reduced)
            }
        }

        impl core::ops::Sub<$type> for Month {
            type Output = Month;

            fn sub(self, rhs: $type) -> Month {
                let reduced = rhs.rem_euclid(13) as u8;
                Self::sub(self, reduced)
            }
        }
    };
}

num_trait_impl!(u8);
num_trait_impl!(u16);
num_trait_impl!(u32);
num_trait_impl!(u64);
num_trait_impl!(u128);

num_trait_impl!(i8);
num_trait_impl!(i16);
num_trait_impl!(i32);
num_trait_impl!(i64);
num_trait_impl!(i128);

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

    #[test]
    fn addition_tests() {
        assert_eq!(Month::September + 4i8, Month::January);
        assert_eq!(Month::September + 4u8, Month::January);
        assert_eq!(Month::September + 4i16, Month::January);
        assert_eq!(Month::September + 4u16, Month::January);
        assert_eq!(Month::September + 4i32, Month::January);
        assert_eq!(Month::September + 4u32, Month::January);
        assert_eq!(Month::September + 4i64, Month::January);
        assert_eq!(Month::September + 4u64, Month::January);
        assert_eq!(Month::September + 4i128, Month::January);
        assert_eq!(Month::September + 4u128, Month::January);
    }

    #[test]
    fn subtraction_tests() {
        assert_eq!(Month::September - 4i8, Month::May);
        assert_eq!(Month::September - 4u8, Month::May);
        assert_eq!(Month::September - 4i16, Month::May);
        assert_eq!(Month::September - 4u16, Month::May);
        assert_eq!(Month::September - 4i32, Month::May);
        assert_eq!(Month::September - 4u32, Month::May);
        assert_eq!(Month::September - 4i64, Month::May);
        assert_eq!(Month::September - 4u64, Month::May);
        assert_eq!(Month::September - 4i128, Month::May);
        assert_eq!(Month::September - 4u128, Month::May);
    }

    #[test]
    fn into_implementation_works() {
        let m: u8 = Month::March.into();
        assert_eq!(m, 1);

        let m: i32 = Month::September.into();
        assert_eq!(m, 7);
    }

    #[test]
    fn from_into_round_trip_works() {
        for m in 1..=13 {
            let typed: Month = m.try_into().unwrap();
            let num: i32 = typed.into();

            assert_eq!(m, num);
        }
    }
}