whatwg_datetime/components/
timezone_offset.rs1use crate::parse_format;
2use crate::tokens::{TOKEN_COLON, TOKEN_MINUS, TOKEN_PLUS, TOKEN_Z};
3use crate::utils::collect_ascii_digits;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub struct TimeZoneOffset {
15 pub(crate) hour: i32,
16 pub(crate) minute: i32,
17}
18
19impl TimeZoneOffset {
20 #[inline]
21 pub(crate) fn new(hour: i32, minute: i32) -> Self {
22 Self { hour, minute }
23 }
24
25 pub fn new_opt(hours: i32, minutes: i32) -> Option<Self> {
41 if !(-23..=23).contains(&hours) {
42 return None;
43 }
44
45 if !(0..=59).contains(&minutes) {
46 return None;
47 }
48
49 Some(Self::new(hours, minutes))
50 }
51
52 #[inline]
62 pub const fn minute(&self) -> i32 {
63 self.minute
64 }
65
66 #[inline]
76 pub const fn hour(&self) -> i32 {
77 self.hour
78 }
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82enum TimeZoneSign {
83 Positive,
84 Negative,
85}
86
87impl TryFrom<char> for TimeZoneSign {
88 type Error = ();
89 fn try_from(value: char) -> Result<Self, Self::Error> {
90 match value {
91 TOKEN_PLUS => Ok(TimeZoneSign::Positive),
92 TOKEN_MINUS => Ok(TimeZoneSign::Negative),
93 _ => Err(()),
94 }
95 }
96}
97
98#[inline]
118pub fn parse_timezone_offset(s: &str) -> Option<TimeZoneOffset> {
119 parse_format(s, parse_timezone_offset_component)
120}
121
122pub fn parse_timezone_offset_component(s: &str, position: &mut usize) -> Option<TimeZoneOffset> {
145 let char_at = s.chars().nth(*position);
146
147 let mut minutes = 0i32;
148 let mut hours = 0i32;
149
150 match char_at {
151 Some(TOKEN_Z) => {
152 *position += 1;
153 }
154 Some(TOKEN_PLUS) | Some(TOKEN_MINUS) => {
155 let sign = TimeZoneSign::try_from(char_at.unwrap()).ok().unwrap();
156 *position += 1;
157
158 let collected = collect_ascii_digits(s, position);
159 let collected_len = collected.len();
160 if collected_len == 2 {
161 hours = collected.parse::<i32>().unwrap();
162 if *position > s.len()
163 || s.chars().nth(*position) != Some(TOKEN_COLON)
164 {
165 return None;
166 } else {
167 *position += 1;
168 }
169
170 let parsed_mins = collect_ascii_digits(s, position);
171 if parsed_mins.len() != 2 {
172 return None;
173 }
174
175 minutes = parsed_mins.parse::<i32>().unwrap();
176 } else if collected_len == 4 {
177 let (hour_str, min_str) = collected.split_at(2);
178 hours = hour_str.parse::<i32>().unwrap();
179 minutes = min_str.parse::<i32>().unwrap();
180 } else {
181 return None;
182 }
183
184 if !(0..=23).contains(&hours) {
185 return None;
186 }
187
188 if !(0..=59).contains(&minutes) {
189 return None;
190 }
191
192 if sign == TimeZoneSign::Negative {
193 hours *= -1;
194 minutes *= -1;
195 }
196 }
197 _ => (),
198 }
199
200 Some(TimeZoneOffset::new(hours, minutes))
201}
202
203#[cfg(test)]
204mod tests {
205 #[rustfmt::skip]
206 use super::{
207 parse_timezone_offset,
208 parse_timezone_offset_component,
209 TimeZoneOffset,
210 TimeZoneSign,
211 };
212
213 #[test]
214 pub fn test_parse_timezone_sign_tryfrom_char_positive() {
215 let parsed = TimeZoneSign::try_from('+');
216 assert_eq!(parsed, Ok(TimeZoneSign::Positive));
217 }
218
219 #[test]
220 pub fn test_parse_timezone_sign_tryfrom_char_negative() {
221 let parsed = TimeZoneSign::try_from('-');
222 assert_eq!(parsed, Ok(TimeZoneSign::Negative));
223 }
224
225 #[test]
226 pub fn test_parse_timezone_sign_tryfrom_char_fails() {
227 let parsed = TimeZoneSign::try_from('a');
228 assert_eq!(parsed, Err(()));
229 }
230
231 #[test]
232 pub fn test_parse_timezone_offset() {
233 let parsed = parse_timezone_offset("+01:00");
234 assert_eq!(parsed, Some(TimeZoneOffset::new(1, 0)));
235 }
236
237 #[test]
238 pub fn test_parse_timezone_offset_z() {
239 let parsed = parse_timezone_offset("Z");
240 assert_eq!(parsed, Some(TimeZoneOffset::new(0, 0)));
241 }
242
243 #[test]
244 pub fn test_parse_timezone_offset_plus_1_hour_colon() {
245 let mut position = 0usize;
246 let parsed = parse_timezone_offset_component("+01:00", &mut position);
247
248 assert_eq!(parsed, Some(TimeZoneOffset::new(1, 0)));
249 }
250
251 #[test]
252 pub fn test_parse_timezone_offset_neg_1_hour_colon() {
253 let mut position = 0usize;
254 let parsed = parse_timezone_offset_component("-01:00", &mut position);
255
256 assert_eq!(parsed, Some(TimeZoneOffset::new(-1, 0)));
257 }
258
259 #[test]
260 pub fn test_parse_timezone_offset_plus_1_hour_no_delim() {
261 let mut position = 0usize;
262 let parsed = parse_timezone_offset_component("+0100", &mut position);
263
264 assert_eq!(parsed, Some(TimeZoneOffset::new(1, 0)));
265 }
266
267 #[test]
268 fn parse_timezone_offset_component_neg_1_hour_no_delim() {
269 let mut position = 0usize;
270 let parsed = parse_timezone_offset_component("-0100", &mut position);
271
272 assert_eq!(parsed, Some(TimeZoneOffset::new(-1, 0)));
273 }
274
275 #[test]
276 fn parse_timezone_offset_fails_not_colon() {
277 let mut position = 0usize;
278 let parsed = parse_timezone_offset_component("-01/", &mut position);
279
280 assert_eq!(parsed, None);
281 }
282
283 #[test]
284 fn parse_timezone_offset_fails_invalid_min_length() {
285 let mut position = 0usize;
286 let parsed = parse_timezone_offset_component("-010", &mut position);
287
288 assert_eq!(parsed, None);
289 }
290
291 #[test]
292 fn parse_timezone_offset_fails_colon_invalid_length_empty() {
293 let mut position = 0usize;
294 let parsed = parse_timezone_offset_component("-01:", &mut position);
295
296 assert_eq!(parsed, None);
297 }
298
299 #[test]
300 fn parse_timezone_offset_fails_colon_invalid_length() {
301 let mut position = 0usize;
302 let parsed = parse_timezone_offset_component("-01:0", &mut position);
303
304 assert_eq!(parsed, None);
305 }
306
307 #[test]
308 fn parse_timezone_offset_fails_invalid_length() {
309 let mut position = 0usize;
310 let parsed = parse_timezone_offset_component("-01000", &mut position);
311
312 assert_eq!(parsed, None);
313 }
314
315 #[test]
316 fn parse_timezone_offset_fails_invalid_hour_upper_bound() {
317 let mut position = 0usize;
318 let parsed = parse_timezone_offset_component("+24:00", &mut position);
319
320 assert_eq!(parsed, None);
321 }
322
323 #[test]
324 fn parse_timezone_offset_fails_invalid_minute_upper_bound() {
325 let mut position = 0usize;
326 let parsed = parse_timezone_offset_component("-00:67", &mut position);
327
328 assert_eq!(parsed, None);
329 }
330}