whatwg_datetime/components/
month.rs

1use crate::tokens::TOKEN_HYPHEN;
2use crate::utils::{collect_ascii_digits, is_valid_month};
3use crate::{collect_month_and_validate, parse_format};
4
5/// A [proleptic-Gregorian date][proleptic-greg] consisting of a year and a month,
6/// with no time-zone or date information.
7///
8/// # Examples
9/// ```
10/// use whatwg_datetime::{parse_month, YearMonth};
11///
12/// assert_eq!(parse_month("2011-11"), YearMonth::new_opt(2011, 11));
13/// ```
14///
15/// [proleptic-greg]: https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#proleptic-gregorian-date
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub struct YearMonth {
18	pub(crate) year: i32,
19	pub(crate) month: u32,
20}
21
22impl YearMonth {
23	pub(crate) const fn new(year: i32, month: u32) -> Self {
24		Self { year, month }
25	}
26
27	/// Creates a new `YearMonth` from a year and a month number.
28	///
29	/// This asserts that:
30	/// - the year is greater than 0
31	/// - that the month number is between 1 and 12, inclusive
32	///
33	/// # Examples
34	/// ```
35	/// use whatwg_datetime::YearMonth;
36	///
37	/// assert!(YearMonth::new_opt(2011, 11).is_some());
38	/// assert!(YearMonth::new_opt(2011, 0).is_none()); // Month number must be at least 1
39	/// assert!(YearMonth::new_opt(0, 1).is_none()); // Year number must be greater than 0
40	/// ```
41	pub fn new_opt(year: i32, month: u32) -> Option<Self> {
42		if year == 0 {
43			return None;
44		}
45
46		if !is_valid_month(&month) {
47			return None;
48		}
49
50		Some(Self::new(year, month))
51	}
52
53	/// A year component. This is a number greater than 0.
54	///
55	/// # Examples
56	/// ```
57	/// use whatwg_datetime::YearMonth;
58	///
59	/// let year_month = YearMonth::new_opt(2011, 11).unwrap();
60	/// assert_eq!(year_month.year(), 2011);
61	/// ```
62	#[inline]
63	pub const fn year(&self) -> i32 {
64		self.year
65	}
66
67	/// A month component. This is a number from 1 to 12, inclusive.
68	///
69	/// # Examples
70	/// ```
71	/// use whatwg_datetime::YearMonth;
72	///
73	/// let year_month = YearMonth::new_opt(2011, 11).unwrap();
74	/// assert_eq!(year_month.month(), 11);
75	/// ```
76	#[inline]
77	pub const fn month(&self) -> u32 {
78		self.month
79	}
80}
81
82/// Parse a [proleptic-Gregorian date][proleptic-greg] consisting of a year and a month,
83/// with no time-zone or date information
84///
85/// This follows the rules for [parsing a month string][whatwg-html-parse]
86/// per [WHATWG HTML Standard § 2.3.5.1 Months][whatwg-html-months].
87///
88/// # Examples
89/// ```
90/// use whatwg_datetime::{parse_month, YearMonth};
91///
92/// assert_eq!(parse_month("2011-11"), YearMonth::new_opt(2011, 11));
93/// ```
94///
95/// [proleptic-greg]: https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#proleptic-gregorian-date
96/// [whatwg-html-months]: https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#months
97/// [whatwg-html-parse]: https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#parse-a-month-string
98#[inline]
99pub fn parse_month(s: &str) -> Option<YearMonth> {
100	parse_format(s, parse_month_component)
101}
102
103/// Low-level function for parsing an individual month component at a given position
104///
105/// This follows the rules for [parsing a month component][whatwg-html-parse]
106/// per [WHATWG HTML Standard § 2.3.5.1 Months][whatwg-html-months].
107///
108/// > **Note**:
109/// > This function exposes a lower-level API than [`parse_month`]. More than likely,
110/// > you will want to use [`parse_month`] instead.
111///
112/// # Examples
113/// ```
114/// use whatwg_datetime::{parse_month_component, YearMonth};
115///
116/// let mut position = 0usize;
117/// let date = parse_month_component("2011-11", &mut position);
118///
119/// assert_eq!(date, YearMonth::new_opt(2011, 11));
120/// ```
121///
122/// [whatwg-html-months]: https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#months
123/// [whatwg-html-parse]: https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#parse-a-month-component
124pub fn parse_month_component(s: &str, position: &mut usize) -> Option<YearMonth> {
125	let parsed_year = collect_ascii_digits(s, position);
126	if parsed_year.len() < 4 {
127		return None;
128	}
129
130	let year = parsed_year.parse::<i32>().ok()?;
131	if year == 0 {
132		return None;
133	}
134
135	if *position > s.len() || s.chars().nth(*position) != Some(TOKEN_HYPHEN) {
136		return None;
137	} else {
138		*position += 1;
139	}
140
141	let month = collect_month_and_validate(s, position)?;
142	Some(YearMonth::new(year, month))
143}
144
145#[cfg(test)]
146mod tests {
147	use super::{parse_month, parse_month_component, YearMonth};
148
149	#[test]
150	fn test_parse_month_string() {
151		let parsed = parse_month("2004-12");
152		assert_eq!(parsed, Some(YearMonth::new(2004, 12)));
153	}
154
155	#[test]
156	fn test_parse_month_string_fails_invalid_month() {
157		let parsed = parse_month("2004-2a");
158		assert_eq!(parsed, None);
159	}
160
161	#[test]
162	fn test_parse_month_string_fails() {
163		let parsed = parse_month("2004-13");
164		assert_eq!(parsed, None);
165	}
166
167	#[test]
168	fn test_parse_month_component() {
169		let mut position = 0usize;
170		let parsed = parse_month_component("2004-12", &mut position);
171
172		assert_eq!(parsed, Some(YearMonth::new(2004, 12)));
173	}
174
175	#[test]
176	fn test_parse_month_component_fails_year_lt_4_digits() {
177		let mut position = 0usize;
178		let parsed = parse_month_component("200-12", &mut position);
179
180		assert_eq!(parsed, None);
181	}
182
183	#[test]
184	fn test_parse_month_component_fails_invalid_month_lower_bound() {
185		let mut position = 0usize;
186		let parsed = parse_month_component("2004-0", &mut position);
187
188		assert_eq!(parsed, None);
189	}
190
191	#[test]
192	fn test_parse_month_component_fails_invalid_month_upper_bound() {
193		let mut position = 0usize;
194		let parsed = parse_month_component("2004-13", &mut position);
195
196		assert_eq!(parsed, None);
197	}
198
199	#[test]
200	fn test_parse_month_component_fails_invalid_month_syntax() {
201		let mut position = 0usize;
202		let parsed = parse_month_component("2004-1a", &mut position);
203
204		assert_eq!(parsed, None);
205	}
206
207	#[test]
208	fn test_parse_month_component_fails_invalid_separator() {
209		let mut position = 0usize;
210		let parsed = parse_month_component("2004/12", &mut position);
211
212		assert_eq!(parsed, None);
213	}
214}