whatwg_datetime/components/
yearless_date.rs

1use crate::tokens::TOKEN_HYPHEN;
2use crate::utils::is_valid_month;
3use crate::{collect_day_and_validate, collect_month_and_validate, parse_format};
4use whatwg_infra::collect_codepoints;
5
6/// A yearless date, consisting of a gregorian month and a day
7/// within the month, without an associated year.
8///
9/// # Examples
10///
11/// ```
12/// use whatwg_datetime::{parse_yearless_date, YearlessDate};
13///
14/// assert_eq!(parse_yearless_date("11-18"), YearlessDate::new_opt(11, 18));
15/// ```
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub struct YearlessDate {
18	pub(crate) month: u32,
19	pub(crate) day: u32,
20}
21
22impl YearlessDate {
23	#[inline]
24	pub(crate) fn new(month: u32, day: u32) -> Self {
25		Self { month, day }
26	}
27
28	/// Creates a new `YearlessDate` from a month and a day.
29	///
30	/// This asserts that the month is in between 1 through 12,
31	/// inclusive, and that the day is in the valid range for
32	/// the month. Specifically:
33	/// - February must be between 1 and 29, inclusive
34	/// - April, June, September, and November must be between 1 and 30, inclusive
35	/// - All other months must be between 1 and 31, inclusive
36	///
37	/// # Examples
38	/// ```
39	/// use whatwg_datetime::YearlessDate;
40	///
41	/// assert!(YearlessDate::new_opt(11, 18).is_some());
42	/// assert!(YearlessDate::new_opt(2, 29).is_some());
43	/// assert!(YearlessDate::new_opt(2, 30).is_none()); // February never has 30 days
44	/// assert!(YearlessDate::new_opt(4, 31).is_none()); // April only has 30 days
45	/// assert!(YearlessDate::new_opt(13, 1).is_none()); // There are only 12 months
46	/// assert!(YearlessDate::new_opt(12, 32).is_none()); // December only has 31 days
47	/// ```
48	#[rustfmt::skip]
49	pub fn new_opt(month: u32, day: u32) -> Option<Self> {
50		if !is_valid_month(&month) {
51			return None;
52		}
53
54		match month {
55			2 => if day > 29 { return None; },
56			4 | 6 | 9 | 11 => if day > 30 { return None; },
57			_ => if day > 31 { return None; },
58		}
59
60		Some(Self::new(month, day))
61	}
62
63	/// A month component. This is a number from 1 to 12, inclusive.
64	///
65	/// # Examples
66	/// ```
67	/// use whatwg_datetime::YearlessDate;
68	///
69	/// let yearless_date = YearlessDate::new_opt(11, 18).unwrap();
70	/// assert_eq!(yearless_date.month(), 11);
71	/// ```
72	#[inline]
73	pub const fn month(&self) -> u32 {
74		self.month
75	}
76
77	/// A day component. This is a number from 1 to the max number
78	/// of days in the month, inclusive.
79	///
80	/// # Examples
81	/// ```
82	/// use whatwg_datetime::YearlessDate;
83	///
84	/// let yearless_date = YearlessDate::new_opt(11, 18).unwrap();
85	/// assert_eq!(yearless_date.day(), 18);
86	/// ```
87	#[inline]
88	pub const fn day(&self) -> u32 {
89		self.day
90	}
91}
92
93/// Parses a string consisting of a gregorian month and a day
94/// within the month, without an associated year
95///
96/// This follows the rules for [parsing a yearless date string][whatwg-html-parse]
97/// per [WHATWG HTML Standard § 2.3.5.3 Yearless dates][whatwg-html-yearless].
98///
99/// # Examples
100/// ```
101/// use whatwg_datetime::{parse_yearless_date, YearlessDate};
102///
103/// assert_eq!(parse_yearless_date("11-18"), YearlessDate::new_opt(11, 18));
104/// assert_eq!(parse_yearless_date("02-29"), YearlessDate::new_opt(2, 29));
105/// assert_eq!(parse_yearless_date("02-30"), None); // February never has 30 days
106/// assert_eq!(parse_yearless_date("04-31"), None); // April only has 30 days
107/// assert_eq!(parse_yearless_date("13-01"), None);
108/// ```
109///
110/// [whatwg-html-yearless]: https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#yearless-dates
111/// [whatwg-html-parse]: https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#parse-a-yearless-date-string
112#[inline]
113pub fn parse_yearless_date(s: &str) -> Option<YearlessDate> {
114	parse_format(s, parse_yearless_date_component)
115}
116
117/// Low-level function for parsing an individual yearless date component
118/// at a given position
119///
120/// This follows the rules for [parsing a yearless date component][whatwg-html-parse]
121/// per [WHATWG HTML Standard § 2.3.5.3 Yearless dates][whatwg-html-yearless].
122///
123/// > **Note**:
124/// > This function exposes a lower-level API than [`parse_yearless_date`].
125/// > More than likely, you will want to use [`parse_yearless_date`] instead.
126///
127/// # Examples
128/// ```
129/// use whatwg_datetime::{parse_yearless_date_component, YearlessDate};
130///
131/// let mut position = 0usize;
132/// let date = parse_yearless_date_component("11-18", &mut position);
133///
134/// assert_eq!(date, YearlessDate::new_opt(11, 18));
135/// ```
136///
137/// [whatwg-html-yearless]: https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#yearless-dates
138/// [whatwg-html-parse]: https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#parse-a-yearless-date-component
139pub fn parse_yearless_date_component(s: &str, position: &mut usize) -> Option<YearlessDate> {
140	let collected = collect_codepoints(s, position, |c| c == TOKEN_HYPHEN);
141	if !matches!(collected.len(), 0 | 2) {
142		return None;
143	}
144
145	let month = collect_month_and_validate(s, position)?;
146	if *position > s.len() || s.chars().nth(*position) != Some(TOKEN_HYPHEN) {
147		return None;
148	} else {
149		*position += 1;
150	}
151
152	let day = collect_day_and_validate(s, position, month)?;
153	Some(YearlessDate::new(month, day))
154}
155
156#[cfg(test)]
157mod tests {
158	#[rustfmt::skip]
159	use super::{
160		parse_yearless_date,
161		parse_yearless_date_component,
162		YearlessDate,
163	};
164
165	#[test]
166	fn test_parse_yearless_date() {
167		assert_eq!(
168			parse_yearless_date("11-18"),
169			Some(YearlessDate::new(11, 18))
170		);
171	}
172
173	#[test]
174	fn test_parse_yearless_date_fails_empty_string() {
175		assert_eq!(parse_yearless_date(""), None);
176	}
177
178	#[test]
179	fn test_parse_yearless_date_fails_separator() {
180		assert_eq!(parse_yearless_date("11/18"), None);
181	}
182
183	#[test]
184	fn test_parse_yearless_date_fails_month_upper_bound() {
185		assert_eq!(parse_yearless_date("13-01"), None);
186	}
187
188	#[test]
189	fn test_parse_yearless_date_fails_month_length() {
190		assert_eq!(parse_yearless_date("1-01"), None);
191	}
192
193	#[test]
194	fn test_parse_yearless_date_fails_day_lower_bound() {
195		assert_eq!(parse_yearless_date("01-00"), None);
196	}
197
198	#[test]
199	fn test_parse_yearless_date_fails_day_upper_bound() {
200		assert_eq!(parse_yearless_date("01-32"), None);
201	}
202
203	#[test]
204	fn test_parse_yearless_date_fails_day_length() {
205		assert_eq!(parse_yearless_date("01-9"), None);
206	}
207
208	#[test]
209	fn test_parse_yearless_date_component() {
210		let mut position = 0usize;
211		let parsed = parse_yearless_date_component("12-31", &mut position);
212
213		assert_eq!(parsed, Some(YearlessDate::new(12, 31)));
214	}
215
216	#[test]
217	fn test_parse_yearless_date_component_fails_empty_string() {
218		let mut position = 0usize;
219		let parsed = parse_yearless_date_component("", &mut position);
220
221		assert_eq!(parsed, None);
222	}
223
224	#[test]
225	fn test_parse_yearless_date_only_one_separator() {
226		let mut position = 0usize;
227		let parsed = parse_yearless_date_component("-", &mut position);
228
229		assert_eq!(parsed, None);
230	}
231}