gregorian/
year_month.rs

1use crate::{Date, InvalidDayOfMonth, Month, Year};
2
3/// A month of a specific year.
4#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
5#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
6pub struct YearMonth {
7	year: Year,
8	month: Month,
9}
10
11impl YearMonth {
12	/// Create a new year-month.
13	pub fn new(year: impl Into<Year>, month: Month) -> Self {
14		let year = year.into();
15		Self { year, month }
16	}
17
18	/// Create a new year-month at compile time.
19	///
20	/// Unline, [`Self::new()`], this takes a [`Year`] instead of an `impl Into<Year>`,
21	/// since the conversion can not be done in a `const fn`.
22	pub const fn new_const(year: Year, month: Month) -> Self {
23		Self { year, month }
24	}
25
26	/// Get the year.
27	pub const fn year(self) -> Year {
28		self.year
29	}
30
31	/// Get the month as [`Month`].
32	pub const fn month(self) -> Month {
33		self.month
34	}
35
36	/// Get the total number of days in the month.
37	///
38	/// This function accounts for leap-days,
39	/// so it reports 29 days for February of leap-years,
40	/// and 28 days for other years.
41	pub const fn total_days(self) -> u8 {
42		crate::raw::days_in_month(self.month, self.year.has_leap_day())
43	}
44
45	/// Get the day-of-year on which the month starts.
46	///
47	/// Day-of-year numbers are 1-based.
48	pub const fn day_of_year(self) -> u16 {
49		crate::raw::start_day_of_year(self.month, self.year.has_leap_day())
50	}
51
52	/// Get the next month as [`YearMonth`].
53	///
54	/// After December, this function returns January of the next year.
55	pub const fn next(self) -> Self {
56		if let Month::December = self.month {
57			Self::new_const(self.year.next(), Month::January)
58		} else {
59			Self::new_const(self.year, self.month.wrapping_next())
60		}
61	}
62
63	/// Get the previous month as [`YearMonth`].
64	///
65	/// After January, this function returns December of the previous year.
66	pub const fn prev(self) -> Self {
67		if let Month::January = self.month {
68			Self::new_const(self.year.prev(), Month::December)
69		} else {
70			Self::new_const(self.year, self.month.wrapping_prev())
71		}
72	}
73
74	/// Get a new [`YearMonth`] by adding a number of years.
75	pub const fn add_years(self, years: i16) -> Self {
76		let year = Year::new(self.year.to_number() + years);
77		year.with_month(self.month())
78	}
79
80	/// Get a new [`YearMonth`] by subtracting a number of years.
81	pub const fn sub_years(self, years: i16) -> Self {
82		let year = Year::new(self.year.to_number() - years);
83		year.with_month(self.month())
84	}
85
86	/// Get a new [`YearMonth`] by adding a number of months.
87	pub const fn add_months(self, months: i32) -> Self {
88		// Split calculation for years and months.
89		let months = (self.month().to_number() - 1) as i32 + months;
90		let mut year = self.year().to_number() + (months / 12) as i16;
91		let month = Month::January.wrapping_add((months % 12) as i8);
92
93		// If we subtract months, we must decrease the year too.
94		if months % 12 < 0 {
95			year -= 1;
96		}
97
98		Year::new(year).with_month(month)
99	}
100
101	/// Get a new [`YearMonth`] by subtracting a number of months.
102	pub const fn sub_months(self, months: i32) -> Self {
103		// This breaks for i32::MIN, but that would overflow the year counter anyway.
104		self.add_months(-months)
105	}
106
107	/// Combine the year and month with a day, to create a full [`Date`].
108	pub const fn with_day(self, day: u8) -> Result<Date, InvalidDayOfMonth> {
109		if let Err(e) = InvalidDayOfMonth::check(self.year, self.month, day) {
110			return Err(e);
111		}
112		unsafe { Ok(Date::new_unchecked(self.year, self.month, day)) }
113	}
114
115	/// Combine the year and month with a day, without checking for validity.
116	///
117	/// # Safety
118	/// Although this is currently not the case, future implementations may rely on date validity for memory safety
119	pub const unsafe fn with_day_unchecked(self, day: u8) -> Date {
120		Date::new_unchecked(self.year, self.month, day)
121	}
122
123	/// Get the first day of the month as [`Date`].
124	pub const fn first_day(self) -> Date {
125		Date {
126			year: self.year,
127			month: self.month,
128			day: 1,
129		}
130	}
131
132	/// Get the last day of the month as [`Date`].
133	pub const fn last_day(self) -> Date {
134		Date {
135			year: self.year,
136			month: self.month,
137			day: self.total_days(),
138		}
139	}
140}
141
142impl core::fmt::Display for YearMonth {
143	fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
144		write!(f, "{:04}-{:02}", self.year.to_number(), self.month().to_number())
145	}
146}
147
148impl core::fmt::Debug for YearMonth {
149	fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
150		write!(f, "YearMonth({})", self)
151	}
152}
153
154#[cfg(test)]
155mod test {
156	use crate::*;
157	use assert2::{assert, let_assert};
158
159	#[test]
160	fn add_months() {
161		for i in -200..=200 {
162			assert!(Year::new(2000).with_month(January).add_months(i * 12 + 1) == Year::new(2000 + i as i16).with_month(February));
163			assert!(Year::new(2000).with_month(January).add_months(i * 12 + 2) == Year::new(2000 + i as i16).with_month(March));
164			assert!(Year::new(2000).with_month(January).add_months(i * 12 + 3) == Year::new(2000 + i as i16).with_month(April));
165			assert!(Year::new(2000).with_month(January).add_months(i * 12 + 4) == Year::new(2000 + i as i16).with_month(May));
166			assert!(Year::new(2000).with_month(January).add_months(i * 12 + 5) == Year::new(2000 + i as i16).with_month(June));
167			assert!(Year::new(2000).with_month(January).add_months(i * 12 + 6) == Year::new(2000 + i as i16).with_month(July));
168			assert!(Year::new(2000).with_month(January).add_months(i * 12 + 7) == Year::new(2000 + i as i16).with_month(August));
169			assert!(Year::new(2000).with_month(January).add_months(i * 12 + 8) == Year::new(2000 + i as i16).with_month(September));
170			assert!(Year::new(2000).with_month(January).add_months(i * 12 + 9) == Year::new(2000 + i as i16).with_month(October));
171			assert!(Year::new(2000).with_month(January).add_months(i * 12 + 10) == Year::new(2000 + i as i16).with_month(November));
172			assert!(Year::new(2000).with_month(January).add_months(i * 12 + 11) == Year::new(2000 + i as i16).with_month(December));
173			assert!(Year::new(2000).with_month(January).add_months(i * 12 + 12) == Year::new(2001 + i as i16).with_month(January));
174
175			assert!(Year::new(2000).with_month(January).add_months(i * 12 + -1) == Year::new(1999 + i as i16).with_month(December));
176			assert!(Year::new(2000).with_month(January).add_months(i * 12 + -2) == Year::new(1999 + i as i16).with_month(November));
177			assert!(Year::new(2000).with_month(January).add_months(i * 12 + -3) == Year::new(1999 + i as i16).with_month(October));
178			assert!(Year::new(2000).with_month(January).add_months(i * 12 + -4) == Year::new(1999 + i as i16).with_month(September));
179			assert!(Year::new(2000).with_month(January).add_months(i * 12 + -5) == Year::new(1999 + i as i16).with_month(August));
180			assert!(Year::new(2000).with_month(January).add_months(i * 12 + -6) == Year::new(1999 + i as i16).with_month(July));
181			assert!(Year::new(2000).with_month(January).add_months(i * 12 + -7) == Year::new(1999 + i as i16).with_month(June));
182			assert!(Year::new(2000).with_month(January).add_months(i * 12 + -8) == Year::new(1999 + i as i16).with_month(May));
183			assert!(Year::new(2000).with_month(January).add_months(i * 12 + -9) == Year::new(1999 + i as i16).with_month(April));
184			assert!(Year::new(2000).with_month(January).add_months(i * 12 + -10) == Year::new(1999 + i as i16).with_month(March));
185			assert!(Year::new(2000).with_month(January).add_months(i * 12 + -11) == Year::new(1999 + i as i16).with_month(February));
186			assert!(Year::new(2000).with_month(January).add_months(i * 12 + -12) == Year::new(1999 + i as i16).with_month(January));
187		}
188	}
189
190	#[test]
191	fn total_days() {
192		assert!(Year::new(2020).with_month(January).total_days() == 31);
193		assert!(Year::new(2020).with_month(February).total_days() == 29);
194		assert!(Year::new(2020).with_month(March).total_days() == 31);
195		assert!(Year::new(2020).with_month(April).total_days() == 30);
196		assert!(Year::new(2020).with_month(May).total_days() == 31);
197		assert!(Year::new(2020).with_month(June).total_days() == 30);
198		assert!(Year::new(2020).with_month(July).total_days() == 31);
199		assert!(Year::new(2020).with_month(August).total_days() == 31);
200		assert!(Year::new(2020).with_month(September).total_days() == 30);
201		assert!(Year::new(2020).with_month(October).total_days() == 31);
202		assert!(Year::new(2020).with_month(November).total_days() == 30);
203		assert!(Year::new(2020).with_month(December).total_days() == 31);
204
205		assert!(Year::new(2021).with_month(January).total_days() == 31);
206		assert!(Year::new(2021).with_month(February).total_days() == 28);
207		assert!(Year::new(2021).with_month(March).total_days() == 31);
208		assert!(Year::new(2021).with_month(April).total_days() == 30);
209		assert!(Year::new(2021).with_month(May).total_days() == 31);
210		assert!(Year::new(2021).with_month(June).total_days() == 30);
211		assert!(Year::new(2021).with_month(July).total_days() == 31);
212		assert!(Year::new(2021).with_month(August).total_days() == 31);
213		assert!(Year::new(2021).with_month(September).total_days() == 30);
214		assert!(Year::new(2021).with_month(October).total_days() == 31);
215		assert!(Year::new(2021).with_month(November).total_days() == 30);
216		assert!(Year::new(2021).with_month(December).total_days() == 31);
217	}
218
219	#[test]
220	fn start_day_of_year() {
221		for year in -400..=400 {
222			// We assume total_days() works, since it has it's own unit test,
223			// then cross check start days against that.
224			let mut start_day = 1;
225			for month in &Year::new(year).months() {
226				assert!(month.day_of_year() == start_day);
227				start_day += u16::from(month.total_days());
228			}
229		}
230	}
231
232	#[test]
233	#[cfg(feature = "std")]
234	fn format() {
235		assert!(format!("{}", Year::new(2020).with_month(January)) == "2020-01");
236		assert!(format!("{:?}", Year::new(2020).with_month(January)) == "YearMonth(2020-01)");
237	}
238
239	#[test]
240	fn serde() {
241		let_assert!(Ok(serialized) = serde_yaml::to_string(&YearMonth::new(2020, Month::January)));
242		assert!(serialized == "year: 2020\nmonth: 1\n");
243
244		let_assert!(Ok(parsed) = serde_yaml::from_str::<YearMonth>("year: 2020\nmonth: 1\n"));
245		assert!(parsed.year == 2020);
246		assert!(parsed.month == Month::January);
247	}
248}