whatwg_datetime/components/
global_datetime.rs

1use crate::tokens::{TOKEN_SPACE, TOKEN_T};
2use crate::{parse_date_component, parse_time_component, parse_timezone_offset_component};
3use chrono::{DateTime, Duration, NaiveDateTime, TimeZone, Utc};
4
5/// Parse a [proleptic-Gregorian date][proleptic-greg] consisting
6/// of a date, time, and an optional time-zone offset
7///
8/// This follows the rules for [parsing a global datetime string][whatwg-html-parse]
9/// per [WHATWG HTML Standard ยง 2.3.5.7 Global dates and times][whatwg-html-global-datetime].
10///
11/// # Examples
12/// A global date-time string with a time (hours and minutes):
13/// ```
14/// use chrono::{NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Utc};
15/// use whatwg_datetime::parse_global_datetime;
16///
17/// assert_eq!(
18/// 	parse_global_datetime("2011-11-18T14:54Z"),
19/// 	Some(Utc.from_utc_datetime(
20/// 		&NaiveDateTime::new(
21/// 			NaiveDate::from_ymd_opt(2011, 11, 18).unwrap(),
22/// 			NaiveTime::from_hms_opt(14, 54, 0).unwrap(),
23/// 		)
24/// 	))
25/// );
26/// ```
27///
28/// [proleptic-greg]: https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#proleptic-gregorian-date
29/// [whatwg-html-global-datetime]: https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#global-dates-and-times
30/// [whatwg-html-parse]: https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#parse-a-global-date-and-time-string
31pub fn parse_global_datetime(s: &str) -> Option<DateTime<Utc>> {
32	let mut position = 0usize;
33	let date = parse_date_component(s, &mut position)?;
34
35	let last_char = s.chars().nth(position);
36	if position > s.len() || !matches!(last_char, Some(TOKEN_T) | Some(TOKEN_SPACE)) {
37		return None;
38	} else {
39		position += 1;
40	}
41
42	let time = parse_time_component(s, &mut position)?;
43	if position > s.len() {
44		return None;
45	}
46
47	let timezone_offset = parse_timezone_offset_component(s, &mut position)?;
48	if position < s.len() {
49		return None;
50	}
51
52	let timezone_offset_as_duration =
53		Duration::minutes(timezone_offset.minute as i64 + timezone_offset.hour as i64 * 60);
54	let naive_datetime = NaiveDateTime::new(
55		date,
56		time.overflowing_sub_signed(timezone_offset_as_duration).0,
57	);
58
59	Some(Utc.from_utc_datetime(&naive_datetime))
60}
61
62#[cfg(test)]
63mod tests {
64	use super::parse_global_datetime;
65	use chrono::{NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Utc};
66
67	#[test]
68	fn test_parse_global_datetime_t_hm() {
69		assert_eq!(
70			parse_global_datetime("2004-12-31T12:31"),
71			Some(Utc.from_utc_datetime(&NaiveDateTime::new(
72				NaiveDate::from_ymd_opt(2004, 12, 31).unwrap(),
73				NaiveTime::from_hms_opt(12, 31, 0).unwrap(),
74			)))
75		);
76	}
77
78	#[test]
79	fn test_parse_global_datetime_t_hms() {
80		assert_eq!(
81			parse_global_datetime("2004-12-31T12:31:59"),
82			Some(Utc.from_utc_datetime(&NaiveDateTime::new(
83				NaiveDate::from_ymd_opt(2004, 12, 31).unwrap(),
84				NaiveTime::from_hms_opt(12, 31, 59).unwrap(),
85			)))
86		);
87	}
88
89	#[test]
90	fn test_parse_global_datetime_t_hms_milliseconds() {
91		assert_eq!(
92			parse_global_datetime("2027-11-29T12:31:59.123"),
93			Some(Utc.from_utc_datetime(&NaiveDateTime::new(
94				NaiveDate::from_ymd_opt(2027, 11, 29).unwrap(),
95				NaiveTime::from_hms_milli_opt(12, 31, 59, 123).unwrap(),
96			)))
97		);
98	}
99
100	#[test]
101	fn test_parse_global_datetime_t_hms_z() {
102		assert_eq!(
103			parse_global_datetime("2004-12-31T12:31:59Z"),
104			Some(Utc.from_utc_datetime(&NaiveDateTime::new(
105				NaiveDate::from_ymd_opt(2004, 12, 31).unwrap(),
106				NaiveTime::from_hms_opt(12, 31, 59).unwrap(),
107			)))
108		);
109	}
110
111	#[test]
112	fn test_parse_global_datetime_space_hm() {
113		assert_eq!(
114			parse_global_datetime("2004-12-31 12:31"),
115			Some(Utc.from_utc_datetime(&NaiveDateTime::new(
116				NaiveDate::from_ymd_opt(2004, 12, 31).unwrap(),
117				NaiveTime::from_hms_opt(12, 31, 0).unwrap(),
118			)))
119		);
120	}
121
122	#[test]
123	fn test_parse_global_datetime_space_hms() {
124		assert_eq!(
125			parse_global_datetime("2004-12-31 12:31:59"),
126			Some(Utc.from_utc_datetime(&NaiveDateTime::new(
127				NaiveDate::from_ymd_opt(2004, 12, 31).unwrap(),
128				NaiveTime::from_hms_opt(12, 31, 59).unwrap(),
129			)))
130		);
131	}
132
133	#[test]
134	fn test_parse_global_datetime_space_hms_milliseconds() {
135		assert_eq!(
136			parse_global_datetime("2004-12-31 12:31:59.123"),
137			Some(Utc.from_utc_datetime(&NaiveDateTime::new(
138				NaiveDate::from_ymd_opt(2004, 12, 31).unwrap(),
139				NaiveTime::from_hms_milli_opt(12, 31, 59, 123).unwrap(),
140			)))
141		);
142	}
143
144	#[test]
145	fn test_parse_global_datetime_fails_invalid_date() {
146		assert_eq!(parse_global_datetime("2004/13/31T12:31"), None);
147	}
148
149	#[test]
150	fn test_parse_global_datetime_fails_invalid_delimiter() {
151		assert_eq!(parse_global_datetime("1986-08-14/12-31"), None);
152	}
153
154	#[test]
155	fn test_parse_global_datetime_fails_invalid_time() {
156		assert_eq!(parse_global_datetime("2006-06-05T24:31"), None);
157	}
158
159	#[test]
160	fn test_parse_global_datetime_fails_invalid_time_long_pos() {
161		assert_eq!(parse_global_datetime("2006-06-05T24:31:5999"), None);
162	}
163
164	#[test]
165	fn test_parse_global_datetime_fails_invalid_timezone_offset_1() {
166		assert_eq!(parse_global_datetime("2019-12-31T11:17+24:00"), None);
167	}
168
169	#[test]
170	fn test_parse_global_datetime_fails_invalid_timezone_offset_2() {
171		assert_eq!(parse_global_datetime("1456-02-24T11:17C"), None);
172	}
173}