whatwg_datetime/components/
time.rs

1use crate::parse_format;
2use crate::tokens::{TOKEN_COLON, TOKEN_DOT};
3use crate::utils::{collect_ascii_digits, is_valid_hour, is_valid_min_or_sec};
4use chrono::NaiveTime;
5use whatwg_infra::collect_codepoints;
6
7/// Parse a specific time containing an hour, minute, and optionally a second,
8/// and a fraction of a second
9///
10/// This follows the rules for [parsing a time string][whatwg-html-parse]
11/// per [WHATWG HTML Standard § 2.3.5.4 Times][whatwg-html-time].
12///
13/// # Examples
14/// ```
15/// use chrono::NaiveTime;
16/// use whatwg_datetime::parse_time;
17///
18/// // parse a local datetime with hours and minutes
19/// assert_eq!(parse_time("14:59"), NaiveTime::from_hms_opt(14, 59, 0));
20///
21/// // parse a local datetime with hours, minutes, and seconds
22/// assert_eq!(parse_time("14:59:39"), NaiveTime::from_hms_opt(14, 59, 39));
23///
24/// // parse a local datetime with hours, minutes, seconds, and milliseconds
25/// assert_eq!(parse_time("14:59:39.929"), NaiveTime::from_hms_milli_opt(14, 59, 39, 929));
26/// ```
27///
28/// [whatwg-html-time]: https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#times
29/// [whatwg-html-parse]: https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#parse-a-time-string
30#[inline]
31pub fn parse_time(s: &str) -> Option<NaiveTime> {
32	parse_format(s, parse_time_component)
33}
34
35/// Low-level function for parsing an individual time component at a given position
36///
37/// This follows the rules for [parsing a time component][whatwg-html-parse]
38/// per [WHATWG HTML Standard § 2.3.5.4 Times][whatwg-html-time].
39///
40/// > **Note**:
41/// > This function exposes a lower-level API than [`parse_time`]. More than likely,
42/// > you will want to use [`parse_time`] instead.
43///
44/// # Examples
45/// ```
46/// use chrono::NaiveTime;
47/// use whatwg_datetime::parse_time_component;
48///
49/// let mut position = 0usize;
50/// let date = parse_time_component("14:59", &mut position);
51///
52/// assert_eq!(date, NaiveTime::from_hms_opt(14, 59, 0));
53/// ```
54///
55/// [whatwg-html-time]: https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#times
56/// [whatwg-html-parse]: https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#parse-a-time-component
57pub fn parse_time_component(s: &str, position: &mut usize) -> Option<NaiveTime> {
58	let parsed_hour = collect_ascii_digits(s, position);
59	if parsed_hour.len() != 2 {
60		return None;
61	}
62
63	let hour = parsed_hour.parse::<u32>().ok()?;
64	if !is_valid_hour(&hour) {
65		return None;
66	}
67
68	if *position > s.len() || s.chars().nth(*position) != Some(TOKEN_COLON) {
69		return None;
70	} else {
71		*position += 1;
72	}
73
74	let parsed_minute = collect_ascii_digits(s, position);
75	if parsed_minute.len() != 2 {
76		return None;
77	}
78	let minute = parsed_minute.parse::<u32>().ok()?;
79	if !is_valid_min_or_sec(&minute) {
80		return None;
81	}
82
83	let mut seconds = 0u32;
84	let mut milliseconds = 0u32;
85	if *position < s.len() && s.chars().nth(*position) == Some(TOKEN_COLON) {
86		*position += 1;
87
88		if *position >= s.len() {
89			return None;
90		}
91
92		let parsed_second =
93			collect_codepoints(s, position, |c| c.is_ascii_digit() || c == TOKEN_DOT);
94		let parsed_second_len = parsed_second.len();
95		if parsed_second_len == 3
96			|| (parsed_second_len > 3
97				&& parsed_second.chars().nth(2) != Some(TOKEN_DOT))
98			|| has_at_least_n_instances(s, TOKEN_DOT, 2)
99		{
100			return None;
101		}
102
103		let (parsed_seconds, parsed_milliseconds) =
104			parse_seconds_milliseconds(&parsed_second);
105		seconds = parsed_seconds;
106		milliseconds = parsed_milliseconds;
107		if !is_valid_min_or_sec(&seconds) {
108			return None;
109		}
110	}
111
112	NaiveTime::from_hms_milli_opt(hour, minute, seconds, milliseconds)
113}
114
115fn has_at_least_n_instances(s: &str, c: char, n: usize) -> bool {
116	let mut count = 0usize;
117	for ch in s.chars() {
118		if ch == c {
119			count += 1usize;
120			if count >= n {
121				return true;
122			}
123		}
124	}
125	false
126}
127
128fn parse_seconds_milliseconds(s: &str) -> (u32, u32) {
129	let parts: Vec<&str> = s.split(TOKEN_DOT).collect();
130	let seconds = parts.first().unwrap_or(&"0").parse().unwrap_or(0);
131	let milliseconds = parts.get(1).unwrap_or(&"0").parse().unwrap_or(0);
132
133	(seconds, milliseconds)
134}
135
136#[cfg(test)]
137mod tests {
138	use super::{parse_time, parse_time_component, NaiveTime};
139
140	#[test]
141	fn test_parse_time_succeeds_hm() {
142		assert_eq!(
143			parse_time("12:31"),
144			NaiveTime::from_hms_milli_opt(12, 31, 0, 0)
145		);
146	}
147
148	#[test]
149	fn test_parse_time_succeeds_hms() {
150		assert_eq!(
151			parse_time("12:31:59"),
152			NaiveTime::from_hms_milli_opt(12, 31, 59, 0)
153		);
154	}
155
156	#[test]
157	fn test_parse_time_succeeds_hms_fractional_seconds() {
158		assert_eq!(
159			parse_time("14:54:39.929"),
160			NaiveTime::from_hms_milli_opt(14, 54, 39, 929)
161		);
162	}
163
164	#[test]
165	fn test_parse_time_fails_multiple_decimals() {
166		assert_eq!(parse_time("12:31:59...29"), None);
167	}
168
169	#[test]
170	fn test_parse_time_fails_hour_length() {
171		assert_eq!(parse_time("123:31:59"), None);
172	}
173
174	#[test]
175	fn test_parse_time_fails_hour_value_upper_bound() {
176		assert_eq!(parse_time("24:31:59"), None);
177	}
178
179	#[test]
180	fn test_parse_time_fails_delimiter() {
181		assert_eq!(parse_time("12-31-59"), None);
182	}
183
184	#[test]
185	fn test_parse_time_fails_minute_length() {
186		assert_eq!(parse_time("12:311:59"), None);
187	}
188
189	#[test]
190	fn test_parse_time_fails_minute_value_upper_bound() {
191		assert_eq!(parse_time("12:79:59"), None);
192	}
193
194	#[test]
195	fn test_parse_time_fails_seconds_length() {
196		assert_eq!(parse_time("12:31:591"), None);
197	}
198
199	#[test]
200	fn test_parse_time_fails_seconds_value_upper_bound() {
201		assert_eq!(parse_time("12:31:79"), None);
202	}
203
204	#[test]
205	fn test_parse_time_component() {
206		let mut position = 0usize;
207		let parsed = parse_time_component("12:31:59", &mut position);
208
209		assert_eq!(parsed, NaiveTime::from_hms_milli_opt(12, 31, 59, 0));
210	}
211}