Skip to main content

sac13/
month.rs

1use core::fmt::Display;
2
3/// Represents a month on the SAC13 calendar.
4///
5/// Months are practically the same as in the Gregorian Calendar.
6/// They have the same names and order. The two main differences are that
7/// SAC13 starts its year with March (so March is the 1st month) and SAC13 has
8/// 13 months and this additional month is called "Addenduary" and placed after
9/// February.
10#[repr(u8)]
11#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord, Hash)]
12#[allow(missing_docs)]
13pub enum Month {
14    March = 0,
15    April = 1,
16    May = 2,
17    June = 3,
18    July = 4,
19    August = 5,
20    September = 6,
21    October = 7,
22    November = 8,
23    December = 9,
24    January = 10,
25    February = 11,
26    Addenduary = 12,
27}
28
29impl Month {
30    /// Month from its ordinal number _(valid are 1-13, both inclusive)_.
31    ///
32    /// Returns `None` for invalid ordinals.
33    #[must_use]
34    pub const fn new(m: u8) -> Option<Self> {
35        if m >= 1 && m <= 13 {
36            Some(Self::idx0_to_month(m - 1))
37        } else {
38            None
39        }
40    }
41
42    /// The ordinal number of the month.
43    ///
44    /// Note that those are different from the Gregorian Calendar.  
45    /// March = 1, April = 2, ... February = 12, Addenduary = 13
46    #[must_use]
47    pub const fn ord(self) -> u8 {
48        self.idx0() + 1
49    }
50
51    #[must_use]
52    /// Returns the next months. Effectively like calling [Month::next] `rhs` times.
53    /// 
54    /// `.add(0)` return the current month, `.add(1)` the next month,
55    /// `.add(2)` the one after that, and so on.
56    pub const fn add(self, rhs: u8) -> Self {
57        // we have to reduce (mod 13) two times.
58        // 1) we reduce the right hand side (rhs) to prevent overflow during addition
59        // 2) to reduce the sum to be between 0 and 12 (incl.)
60
61        let rhs = rhs.rem_euclid(13);
62        let sum = self.idx0() + rhs;
63        Self::idx0_to_month(sum.rem_euclid(13))
64    }
65
66    #[must_use]
67    /// Returns the previous months. Effectively like calling [Month::previous] `rhs` times.
68    /// 
69    /// `.sub(0)` return the current month, `.sub(1)` the previous month,
70    /// `.sub(2)` the one before that, and so on.
71    pub const fn sub(self, rhs: u8) -> Self {
72        // to prevent over/underflow we reduce the rhs and reuse add.
73        self.add(13 - rhs.rem_euclid(13))
74    }
75
76    #[must_use]
77    /// Returns the next month (including over year boundries).
78    pub const fn next(self) -> Self {
79        self.add(1)
80    }
81
82    #[must_use]
83    /// Returns the previous month (including over year boundries).
84    pub const fn previous(self) -> Self {
85        // Subtracting 1 is the same as adding 12!
86        self.add(12)
87    }
88
89    /// Full name of the month _(international, English)_.
90    ///
91    /// March, April, May, ...
92    #[must_use]
93    pub const fn name(self) -> &'static str {
94        use Month::*;
95
96        match self {
97            March => "March",
98            April => "April",
99            May => "May",
100            June => "June",
101            July => "July",
102            August => "August",
103            September => "September",
104            October => "October",
105            November => "November",
106            December => "December",
107            January => "January",
108            February => "February",
109            Addenduary => "Addenduary",
110        }
111    }
112
113    const fn idx0(self) -> u8 {
114        self as u8
115    }
116
117    const fn idx0_to_month(m: u8) -> Month {
118        use Month::*;
119
120        match m {
121            0 => March,
122            1 => April,
123            2 => May,
124            3 => June,
125            4 => July,
126            5 => August,
127            6 => September,
128            7 => October,
129            8 => November,
130            9 => December,
131            10 => January,
132            11 => February,
133            12 => Addenduary,
134            _ => panic!(),
135        }
136    }
137}
138
139impl Display for Month {
140    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
141        write!(f, "{}", self.name())
142    }
143}
144
145// Helper macro to implement traits for numeric data types.
146macro_rules! num_trait_impl {
147    ($type:ident) => {
148        impl From<Month> for $type {
149            fn from(value: Month) -> Self {
150                value.ord() as $type
151            }
152        }
153
154        impl TryFrom<$type> for Month {
155            type Error = ();
156
157            fn try_from(value: $type) -> Result<Self, Self::Error> {
158                if (1..=13).contains(&value) {
159                    return Ok(Month::new(value as u8).unwrap());
160                } else {
161                    Err(())
162                }
163            }
164        }
165
166        impl core::ops::Add<$type> for Month {
167            type Output = Month;
168
169            fn add(self, rhs: $type) -> Month {
170                let reduced = rhs.rem_euclid(13) as u8;
171                Self::add(self, reduced)
172            }
173        }
174
175        impl core::ops::Sub<$type> for Month {
176            type Output = Month;
177
178            fn sub(self, rhs: $type) -> Month {
179                let reduced = rhs.rem_euclid(13) as u8;
180                Self::sub(self, reduced)
181            }
182        }
183    };
184}
185
186num_trait_impl!(u8);
187num_trait_impl!(u16);
188num_trait_impl!(u32);
189num_trait_impl!(u64);
190num_trait_impl!(u128);
191
192num_trait_impl!(i8);
193num_trait_impl!(i16);
194num_trait_impl!(i32);
195num_trait_impl!(i64);
196num_trait_impl!(i128);
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201
202    #[test]
203    fn addition_tests() {
204        assert_eq!(Month::September + 4i8, Month::January);
205        assert_eq!(Month::September + 4u8, Month::January);
206        assert_eq!(Month::September + 4i16, Month::January);
207        assert_eq!(Month::September + 4u16, Month::January);
208        assert_eq!(Month::September + 4i32, Month::January);
209        assert_eq!(Month::September + 4u32, Month::January);
210        assert_eq!(Month::September + 4i64, Month::January);
211        assert_eq!(Month::September + 4u64, Month::January);
212        assert_eq!(Month::September + 4i128, Month::January);
213        assert_eq!(Month::September + 4u128, Month::January);
214    }
215
216    #[test]
217    fn subtraction_tests() {
218        assert_eq!(Month::September - 4i8, Month::May);
219        assert_eq!(Month::September - 4u8, Month::May);
220        assert_eq!(Month::September - 4i16, Month::May);
221        assert_eq!(Month::September - 4u16, Month::May);
222        assert_eq!(Month::September - 4i32, Month::May);
223        assert_eq!(Month::September - 4u32, Month::May);
224        assert_eq!(Month::September - 4i64, Month::May);
225        assert_eq!(Month::September - 4u64, Month::May);
226        assert_eq!(Month::September - 4i128, Month::May);
227        assert_eq!(Month::September - 4u128, Month::May);
228    }
229
230    #[test]
231    fn into_implementation_works() {
232        let m: u8 = Month::March.into();
233        assert_eq!(m, 1);
234
235        let m: i32 = Month::September.into();
236        assert_eq!(m, 7);
237    }
238
239    #[test]
240    fn from_into_round_trip_works() {
241        for m in 1..=13 {
242            let typed: Month = m.try_into().unwrap();
243            let num: i32 = typed.into();
244
245            assert_eq!(m, num);
246        }
247    }
248}