1#![forbid(unsafe_code)]
2#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum Month {
20 January,
21 February,
22 March,
23 April,
24 May,
25 June,
26 July,
27 August,
28 September,
29 October,
30 November,
31 December,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum MonthError {
36 InvalidMonth,
37}
38
39fn is_leap_year(year: i32) -> bool {
40 year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
41}
42
43impl Month {
44 #[must_use]
45 pub fn number(&self) -> u8 {
46 match self {
47 Self::January => 1,
48 Self::February => 2,
49 Self::March => 3,
50 Self::April => 4,
51 Self::May => 5,
52 Self::June => 6,
53 Self::July => 7,
54 Self::August => 8,
55 Self::September => 9,
56 Self::October => 10,
57 Self::November => 11,
58 Self::December => 12,
59 }
60 }
61
62 #[must_use]
63 pub fn name(&self) -> &'static str {
64 match self {
65 Self::January => "January",
66 Self::February => "February",
67 Self::March => "March",
68 Self::April => "April",
69 Self::May => "May",
70 Self::June => "June",
71 Self::July => "July",
72 Self::August => "August",
73 Self::September => "September",
74 Self::October => "October",
75 Self::November => "November",
76 Self::December => "December",
77 }
78 }
79
80 #[must_use]
81 pub fn short_name(&self) -> &'static str {
82 match self {
83 Self::January => "Jan",
84 Self::February => "Feb",
85 Self::March => "Mar",
86 Self::April => "Apr",
87 Self::May => "May",
88 Self::June => "Jun",
89 Self::July => "Jul",
90 Self::August => "Aug",
91 Self::September => "Sep",
92 Self::October => "Oct",
93 Self::November => "Nov",
94 Self::December => "Dec",
95 }
96 }
97}
98
99#[must_use]
100pub fn month_from_number(month: u8) -> Option<Month> {
101 Some(match month {
102 1 => Month::January,
103 2 => Month::February,
104 3 => Month::March,
105 4 => Month::April,
106 5 => Month::May,
107 6 => Month::June,
108 7 => Month::July,
109 8 => Month::August,
110 9 => Month::September,
111 10 => Month::October,
112 11 => Month::November,
113 12 => Month::December,
114 _ => return None,
115 })
116}
117
118pub fn days_in_month(year: i32, month: u8) -> Result<u8, MonthError> {
119 Ok(match month {
120 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
121 4 | 6 | 9 | 11 => 30,
122 2 => {
123 if is_leap_year(year) {
124 29
125 } else {
126 28
127 }
128 }
129 _ => return Err(MonthError::InvalidMonth),
130 })
131}
132
133#[must_use]
134pub fn is_valid_month(month: u8) -> bool {
135 month_from_number(month).is_some()
136}
137
138#[cfg(test)]
139mod tests {
140 use super::{days_in_month, is_valid_month, month_from_number, Month, MonthError};
141
142 #[test]
143 fn maps_month_numbers_and_names() {
144 let march = month_from_number(3).unwrap();
145
146 assert_eq!(march, Month::March);
147 assert_eq!(march.number(), 3);
148 assert_eq!(march.name(), "March");
149 assert_eq!(march.short_name(), "Mar");
150 assert!(is_valid_month(12));
151 assert!(!is_valid_month(13));
152 }
153
154 #[test]
155 fn counts_days_in_month() {
156 assert_eq!(days_in_month(2024, 2).unwrap(), 29);
157 assert_eq!(days_in_month(2023, 2).unwrap(), 28);
158 assert_eq!(days_in_month(2024, 4).unwrap(), 30);
159 assert_eq!(days_in_month(2024, 12).unwrap(), 31);
160 }
161
162 #[test]
163 fn rejects_invalid_month_values() {
164 assert_eq!(days_in_month(2024, 0), Err(MonthError::InvalidMonth));
165 assert_eq!(days_in_month(2024, 13), Err(MonthError::InvalidMonth));
166 }
167}