whatwg_datetime/components/
week.rs

1use crate::tokens::{TOKEN_ABBR_WEEK, TOKEN_HYPHEN};
2use crate::utils::{collect_ascii_digits, week_number_of_year};
3
4/// A week date consisting of a year and a week number.
5///
6/// # Examples
7/// ```
8/// use whatwg_datetime::{parse_week, YearWeek};
9///
10/// assert_eq!(parse_week("2011-W47"), YearWeek::new_opt(2011, 47));
11/// ```
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub struct YearWeek {
14	pub(crate) year: i32,
15	pub(crate) week: u32,
16}
17
18impl YearWeek {
19	#[inline]
20	pub(crate) const fn new(year: i32, week: u32) -> Self {
21		Self { year, week }
22	}
23
24	/// Creates a new `YearWeek` from a year and a week number.
25	///
26	/// This asserts that the year is greater than 0 and that the week number
27	/// is in the valid range for the year. Specifically, the week number
28	/// must be between 1 and the number of weeks in the year, inclusive.
29	///
30	/// The number of weeks in a year is described by the algorithm
31	/// in [WHATWG HTML Standard § 2.3.5.8 Weeks][whatwg-html-weeks].
32	///
33	/// # Examples
34	/// ```
35	/// use whatwg_datetime::YearWeek;
36	///
37	/// assert!(YearWeek::new_opt(2004, 53).is_some());
38	/// assert!(YearWeek::new_opt(2011, 47).is_some());
39	/// assert!(YearWeek::new_opt(2011, 53).is_none()); // 2011 only has 52 weeks
40	/// assert!(YearWeek::new_opt(1952, 0).is_none()); // Week number must be at least 1
41	/// assert!(YearWeek::new_opt(0, 1).is_none()); // Year number must be greater than 0
42	/// ```
43	pub fn new_opt(year: i32, week: u32) -> Option<Self> {
44		if year <= 0 {
45			return None;
46		}
47
48		if week < 1 || week > week_number_of_year(year)? {
49			return None;
50		}
51
52		Some(Self::new(year, week))
53	}
54
55	/// A year component. This is a number greater than 0.
56	///
57	/// # Examples
58	/// ```
59	/// use whatwg_datetime::YearWeek;
60	///
61	/// let year_week = YearWeek::new_opt(2004, 53).unwrap();
62	/// assert_eq!(year_week.year(), 2004);
63	/// ```
64	#[inline]
65	pub const fn year(&self) -> i32 {
66		self.year
67	}
68
69	/// A week component. This is a number between 1 and the number of weeks
70	/// in the year, inclusive.
71	///
72	/// # Examples
73	/// ```
74	/// use whatwg_datetime::YearWeek;
75	///
76	/// let year_week = YearWeek::new_opt(2004, 53).unwrap();
77	/// assert_eq!(year_week.week(), 53);
78	/// ```
79	#[inline]
80	pub const fn week(&self) -> u32 {
81		self.week
82	}
83}
84
85/// Parse a week-year number and a week-number
86///
87/// This follows the rules for [parsing a week string][whatwg-html-parse]
88/// per [WHATWG HTML Standard § 2.3.5.8 Weeks][whatwg-html-weeks].
89///
90/// # Examples
91/// ```
92/// use whatwg_datetime::{parse_week, YearWeek};
93///
94/// assert_eq!(parse_week("2004-W53"), YearWeek::new_opt(2004, 53));
95/// assert_eq!(parse_week("2011-W47"), YearWeek::new_opt(2011, 47));
96/// assert_eq!(parse_week("2011-W53"), None); // 2011 only has 52 weeks
97/// ```
98///
99/// [whatwg-html-weeks]: https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#weeks
100/// [whatwg-html-parse]: https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#parse-a-week-string
101pub fn parse_week(input: &str) -> Option<YearWeek> {
102	// Step 1, 2
103	let mut position = 0usize;
104
105	// Step 3, 4
106	let year_string = collect_ascii_digits(input, &mut position);
107	let year = year_string.parse::<i32>().unwrap();
108	if year <= 0 {
109		return None;
110	}
111
112	// Step 5
113	if position > input.len() || input.chars().nth(position) != Some(TOKEN_HYPHEN) {
114		return None;
115	} else {
116		position += 1;
117	}
118
119	// Step 6
120	if position > input.len() || input.chars().nth(position) != Some(TOKEN_ABBR_WEEK) {
121		return None;
122	} else {
123		position += 1;
124	}
125
126	// Step 7
127	let parsed_week = collect_ascii_digits(input, &mut position);
128	if parsed_week.len() != 2 {
129		return None;
130	}
131
132	let week = parsed_week.parse::<u32>().unwrap();
133	let max_weeks = week_number_of_year(year)?;
134	if week < 1 || week > max_weeks {
135		return None;
136	}
137
138	Some(YearWeek::new(year, week))
139}
140
141#[cfg(test)]
142mod tests {
143	use super::{parse_week, YearWeek};
144
145	#[test]
146	fn test_parse_week() {
147		assert_eq!(parse_week("2004-W53"), Some(YearWeek::new(2004, 53)));
148	}
149
150	#[test]
151	fn test_parse_week_fails_year_is_zero() {
152		assert_eq!(parse_week("0000-W01"), None);
153	}
154
155	#[test]
156	fn test_parse_week_fails_invalid_separator() {
157		assert_eq!(parse_week("2004_W01"), None);
158	}
159
160	#[test]
161	fn test_parse_week_fails_invalid_week_abbr() {
162		assert_eq!(parse_week("2003-𝓌01"), None);
163	}
164
165	#[test]
166	fn test_parse_week_fails_invalid_week_length() {
167		assert_eq!(parse_week("2004-W1"), None);
168		assert_eq!(parse_week("2008-W001"), None);
169	}
170
171	#[test]
172	fn test_parse_week_fails_invalid_week_num_lower_bound() {
173		assert_eq!(parse_week("2022-W00"), None);
174		assert_eq!(parse_week("1897-W00"), None);
175	}
176
177	#[test]
178	fn test_parse_week_fails_invalid_week_num_upper_bound() {
179		assert_eq!(parse_week("2004-W54"), None);
180		assert_eq!(parse_week("1996-W53"), None);
181	}
182}