1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
use crate::{Date, InvalidDayOfMonth, Month, Year};

/// A month of a specific year.
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct YearMonth {
	year: Year,
	month: Month,
}

impl YearMonth {
	/// Create a new year-month.
	pub fn new(year: impl Into<Year>, month: Month) -> Self {
		let year = year.into();
		Self { year, month }
	}

	/// Create a new year-month at compile time.
	///
	/// Unline, [`Self::new()`], this takes a [`Year`] instead of an `impl Into<Year>`,
	/// since the conversion can not be done in a `const fn`.
	pub const fn new_const(year: Year, month: Month) -> Self {
		Self { year, month }
	}

	/// Get the year.
	pub const fn year(self) -> Year {
		self.year
	}

	/// Get the month as [`Month`].
	pub const fn month(self) -> Month {
		self.month
	}

	/// Get the total number of days in the month.
	///
	/// This function accounts for leap-days,
	/// so it reports 29 days for February of leap-years,
	/// and 28 days for other years.
	pub const fn total_days(self) -> u8 {
		crate::raw::days_in_month(self.month, self.year.has_leap_day())
	}

	/// Get the day-of-year on which the month starts.
	///
	/// Day-of-year numbers are 1-based.
	pub const fn day_of_year(self) -> u16 {
		crate::raw::start_day_of_year(self.month, self.year.has_leap_day())
	}

	/// Get the next month as [`YearMonth`].
	///
	/// After December, this function returns January of the next year.
	pub const fn next(self) -> Self {
		if let Month::December = self.month {
			Self::new_const(self.year.next(), Month::January)
		} else {
			Self::new_const(self.year, self.month.wrapping_next())
		}
	}

	/// Get the previous month as [`YearMonth`].
	///
	/// After January, this function returns December of the previous year.
	pub const fn prev(self) -> Self {
		if let Month::January = self.month {
			Self::new_const(self.year.prev(), Month::December)
		} else {
			Self::new_const(self.year, self.month.wrapping_prev())
		}
	}

	/// Get a new [`YearMonth`] by adding a number of years.
	pub const fn add_years(self, years: i16) -> Self {
		let year = Year::new(self.year.to_number() + years);
		year.with_month(self.month())
	}

	/// Get a new [`YearMonth`] by subtracting a number of years.
	pub const fn sub_years(self, years: i16) -> Self {
		let year = Year::new(self.year.to_number() - years);
		year.with_month(self.month())
	}

	/// Get a new [`YearMonth`] by adding a number of months.
	pub const fn add_months(self, months: i32) -> Self {
		// Split calculation for years and months.
		let months = (self.month().to_number() - 1) as i32 + months;
		let mut year = self.year().to_number() + (months / 12) as i16;
		let month = Month::January.wrapping_add((months % 12) as i8);

		// If we subtract months, we must decrease the year too.
		if months % 12 < 0 {
			year -= 1;
		}

		Year::new(year).with_month(month)
	}

	/// Get a new [`YearMonth`] by subtracting a number of months.
	pub const fn sub_months(self, months: i32) -> Self {
		// This breaks for i32::MIN, but that would overflow the year counter anyway.
		self.add_months(-months)
	}

	/// Combine the year and month with a day, to create a full [`Date`].
	pub const fn with_day(self, day: u8) -> Result<Date, InvalidDayOfMonth> {
		if let Err(e) = InvalidDayOfMonth::check(self.year, self.month, day) {
			return Err(e);
		}
		unsafe { Ok(Date::new_unchecked(self.year, self.month, day)) }
	}

	/// Combine the year and month with a day, without checking for validity.
	///
	/// # Safety
	/// Although this is currently not the case, future implementations may rely on date validity for memory safety
	pub const unsafe fn with_day_unchecked(self, day: u8) -> Date {
		Date::new_unchecked(self.year, self.month, day)
	}

	/// Get the first day of the month as [`Date`].
	pub const fn first_day(self) -> Date {
		Date {
			year: self.year,
			month: self.month,
			day: 1,
		}
	}

	/// Get the last day of the month as [`Date`].
	pub const fn last_day(self) -> Date {
		Date {
			year: self.year,
			month: self.month,
			day: self.total_days(),
		}
	}
}

impl core::fmt::Display for YearMonth {
	fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
		write!(f, "{:04}-{:02}", self.year.to_number(), self.month().to_number())
	}
}

impl core::fmt::Debug for YearMonth {
	fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
		write!(f, "YearMonth({})", self)
	}
}

#[cfg(test)]
mod test {
	use crate::*;
	use assert2::{assert, let_assert};

	#[test]
	fn add_months() {
		for i in -200..=200 {
			assert!(Year::new(2000).with_month(January).add_months(i * 12 + 1) == Year::new(2000 + i as i16).with_month(February));
			assert!(Year::new(2000).with_month(January).add_months(i * 12 + 2) == Year::new(2000 + i as i16).with_month(March));
			assert!(Year::new(2000).with_month(January).add_months(i * 12 + 3) == Year::new(2000 + i as i16).with_month(April));
			assert!(Year::new(2000).with_month(January).add_months(i * 12 + 4) == Year::new(2000 + i as i16).with_month(May));
			assert!(Year::new(2000).with_month(January).add_months(i * 12 + 5) == Year::new(2000 + i as i16).with_month(June));
			assert!(Year::new(2000).with_month(January).add_months(i * 12 + 6) == Year::new(2000 + i as i16).with_month(July));
			assert!(Year::new(2000).with_month(January).add_months(i * 12 + 7) == Year::new(2000 + i as i16).with_month(August));
			assert!(Year::new(2000).with_month(January).add_months(i * 12 + 8) == Year::new(2000 + i as i16).with_month(September));
			assert!(Year::new(2000).with_month(January).add_months(i * 12 + 9) == Year::new(2000 + i as i16).with_month(October));
			assert!(Year::new(2000).with_month(January).add_months(i * 12 + 10) == Year::new(2000 + i as i16).with_month(November));
			assert!(Year::new(2000).with_month(January).add_months(i * 12 + 11) == Year::new(2000 + i as i16).with_month(December));
			assert!(Year::new(2000).with_month(January).add_months(i * 12 + 12) == Year::new(2001 + i as i16).with_month(January));

			assert!(Year::new(2000).with_month(January).add_months(i * 12 + -1) == Year::new(1999 + i as i16).with_month(December));
			assert!(Year::new(2000).with_month(January).add_months(i * 12 + -2) == Year::new(1999 + i as i16).with_month(November));
			assert!(Year::new(2000).with_month(January).add_months(i * 12 + -3) == Year::new(1999 + i as i16).with_month(October));
			assert!(Year::new(2000).with_month(January).add_months(i * 12 + -4) == Year::new(1999 + i as i16).with_month(September));
			assert!(Year::new(2000).with_month(January).add_months(i * 12 + -5) == Year::new(1999 + i as i16).with_month(August));
			assert!(Year::new(2000).with_month(January).add_months(i * 12 + -6) == Year::new(1999 + i as i16).with_month(July));
			assert!(Year::new(2000).with_month(January).add_months(i * 12 + -7) == Year::new(1999 + i as i16).with_month(June));
			assert!(Year::new(2000).with_month(January).add_months(i * 12 + -8) == Year::new(1999 + i as i16).with_month(May));
			assert!(Year::new(2000).with_month(January).add_months(i * 12 + -9) == Year::new(1999 + i as i16).with_month(April));
			assert!(Year::new(2000).with_month(January).add_months(i * 12 + -10) == Year::new(1999 + i as i16).with_month(March));
			assert!(Year::new(2000).with_month(January).add_months(i * 12 + -11) == Year::new(1999 + i as i16).with_month(February));
			assert!(Year::new(2000).with_month(January).add_months(i * 12 + -12) == Year::new(1999 + i as i16).with_month(January));
		}
	}

	#[test]
	fn total_days() {
		assert!(Year::new(2020).with_month(January).total_days() == 31);
		assert!(Year::new(2020).with_month(February).total_days() == 29);
		assert!(Year::new(2020).with_month(March).total_days() == 31);
		assert!(Year::new(2020).with_month(April).total_days() == 30);
		assert!(Year::new(2020).with_month(May).total_days() == 31);
		assert!(Year::new(2020).with_month(June).total_days() == 30);
		assert!(Year::new(2020).with_month(July).total_days() == 31);
		assert!(Year::new(2020).with_month(August).total_days() == 31);
		assert!(Year::new(2020).with_month(September).total_days() == 30);
		assert!(Year::new(2020).with_month(October).total_days() == 31);
		assert!(Year::new(2020).with_month(November).total_days() == 30);
		assert!(Year::new(2020).with_month(December).total_days() == 31);

		assert!(Year::new(2021).with_month(January).total_days() == 31);
		assert!(Year::new(2021).with_month(February).total_days() == 28);
		assert!(Year::new(2021).with_month(March).total_days() == 31);
		assert!(Year::new(2021).with_month(April).total_days() == 30);
		assert!(Year::new(2021).with_month(May).total_days() == 31);
		assert!(Year::new(2021).with_month(June).total_days() == 30);
		assert!(Year::new(2021).with_month(July).total_days() == 31);
		assert!(Year::new(2021).with_month(August).total_days() == 31);
		assert!(Year::new(2021).with_month(September).total_days() == 30);
		assert!(Year::new(2021).with_month(October).total_days() == 31);
		assert!(Year::new(2021).with_month(November).total_days() == 30);
		assert!(Year::new(2021).with_month(December).total_days() == 31);
	}

	#[test]
	fn start_day_of_year() {
		for year in -400..=400 {
			// We assume total_days() works, since it has it's own unit test,
			// then cross check start days against that.
			let mut start_day = 1;
			for month in &Year::new(year).months() {
				assert!(month.day_of_year() == start_day);
				start_day += u16::from(month.total_days());
			}
		}
	}

	#[test]
	#[cfg(feature = "std")]
	fn format() {
		assert!(format!("{}", Year::new(2020).with_month(January)) == "2020-01");
		assert!(format!("{:?}", Year::new(2020).with_month(January)) == "YearMonth(2020-01)");
	}

	#[test]
	fn serde() {
		let_assert!(Ok(serialized) = serde_yaml::to_string(&YearMonth::new(2020, Month::January)));
		assert!(serialized == "year: 2020\nmonth: 1\n");

		let_assert!(Ok(parsed) = serde_yaml::from_str::<YearMonth>("year: 2020\nmonth: 1\n"));
		assert!(parsed.year == 2020);
		assert!(parsed.month == Month::January);
	}
}