1use super::{Anchor, ByteSet, Finder, RunClass, RunRule};
2use std::ops::Range;
3
4#[derive(Default)]
5pub struct Datetime {}
6
7impl Datetime {
8 fn try_date(input: &[u8], idx: usize) -> Option<Range<usize>> {
9 if idx + 10 > input.len() {
11 return None;
12 }
13
14 if idx > 0 {
18 let prev = input[idx - 1];
19 if prev.is_ascii_digit() {
20 return None;
21 }
22 if prev == b'-' && idx >= 2 && input[idx - 2].is_ascii_digit() {
23 return None;
24 }
25 }
26
27 if !Self::is_4_digits(input, idx) {
29 return None;
30 }
31 let year = Self::parse_num(input, idx, 4);
32 if !(1000..=9999).contains(&year) {
33 return None;
34 }
35
36 if input[idx + 4] != b'-' {
37 return None;
38 }
39
40 if !Self::is_2_digits(input, idx + 5) {
42 return None;
43 }
44 let month = Self::parse_num(input, idx + 5, 2);
45 if !(1..=12).contains(&month) {
46 return None;
47 }
48
49 if input[idx + 7] != b'-' {
50 return None;
51 }
52
53 if !Self::is_2_digits(input, idx + 8) {
55 return None;
56 }
57 let day = Self::parse_num(input, idx + 8, 2);
58 if !(1..=31).contains(&day) {
59 return None;
60 }
61
62 let mut end = idx + 10;
63
64 if end < input.len()
66 && (input[end] == b'T' || input[end] == b't' || input[end] == b' ')
67 && let Some(time_end) = Self::try_time(input, end + 1)
68 {
69 end = time_end;
70 }
71
72 if end < input.len() {
77 let next = input[end];
78 if next.is_ascii_digit() {
79 return None;
80 }
81 if next == b'-' && end + 1 < input.len() && input[end + 1].is_ascii_digit() {
82 return None;
83 }
84 }
85
86 Some(idx..end)
87 }
88
89 fn try_time(input: &[u8], idx: usize) -> Option<usize> {
90 if idx + 5 > input.len() {
92 return None;
93 }
94
95 if !Self::is_2_digits(input, idx) {
96 return None;
97 }
98 let hour = Self::parse_num(input, idx, 2);
99 if hour > 23 {
100 return None;
101 }
102
103 if input[idx + 2] != b':' {
104 return None;
105 }
106
107 if !Self::is_2_digits(input, idx + 3) {
108 return None;
109 }
110 let minute = Self::parse_num(input, idx + 3, 2);
111 if minute > 59 {
112 return None;
113 }
114
115 let mut end = idx + 5;
116
117 if end + 3 <= input.len() && input[end] == b':' && Self::is_2_digits(input, end + 1) {
119 let second = Self::parse_num(input, end + 1, 2);
120 if second <= 60 {
121 end += 3;
122
123 if end < input.len() && (input[end] == b'.' || input[end] == b',') {
126 let frac_start = end + 1;
127 let mut frac_end = frac_start;
128 while frac_end < input.len() && input[frac_end].is_ascii_digit() {
129 frac_end += 1;
130 }
131 if frac_end > frac_start {
132 end = frac_end;
133 }
134 }
135 }
136 }
137
138 if end < input.len() && (input[end] == b'Z' || input[end] == b'z') {
141 end += 1;
142 } else if end < input.len() && (input[end] == b'+' || input[end] == b'-') {
143 let mut digits = 0;
146 while digits < 5
147 && end + 1 + digits < input.len()
148 && input[end + 1 + digits].is_ascii_digit()
149 {
150 digits += 1;
151 }
152 if digits == 2 {
153 if end + 6 <= input.len()
154 && input[end + 3] == b':'
155 && Self::is_2_digits(input, end + 4)
156 {
157 end += 6; } else {
159 end += 3; }
161 } else if digits == 4 {
162 end += 5; }
164 }
167
168 Some(end)
169 }
170
171 fn is_2_digits(input: &[u8], pos: usize) -> bool {
172 pos + 2 <= input.len() && input[pos].is_ascii_digit() && input[pos + 1].is_ascii_digit()
173 }
174
175 fn is_4_digits(input: &[u8], pos: usize) -> bool {
176 pos + 4 <= input.len()
177 && input[pos].is_ascii_digit()
178 && input[pos + 1].is_ascii_digit()
179 && input[pos + 2].is_ascii_digit()
180 && input[pos + 3].is_ascii_digit()
181 }
182
183 fn parse_num(input: &[u8], pos: usize, len: usize) -> u32 {
184 let mut n = 0u32;
185 for i in 0..len {
186 n = n * 10 + (input[pos + i] - b'0') as u32;
187 }
188 n
189 }
190}
191
192impl Finder for Datetime {
193 fn line_agnostic(&self) -> bool {
194 true
197 }
198
199 fn id(&self) -> &'static str {
200 "datetime"
201 }
202
203 fn dispatchable(&self) -> bool {
204 true
205 }
206
207 fn could_start_at(&self, byte: u8) -> bool {
208 byte.is_ascii_digit()
209 }
210
211 fn could_start_after(&self, prev: u8, _cur: u8) -> bool {
212 !prev.is_ascii_digit()
213 }
214
215 fn could_continue_with(&self, _cur: u8, next: u8) -> bool {
216 next.is_ascii_digit()
217 }
218
219 fn anchor(&self) -> Option<Anchor> {
220 Some(
222 Anchor::new(
223 ByteSet::from_bytes(b"-"),
224 ByteSet::from_fn(|b| b.is_ascii_digit()),
225 )
226 .confirm(b"-", &[3], b"-")
227 .back(4),
228 )
229 }
230
231 fn run_rules(&self) -> Vec<RunRule> {
232 vec![
234 RunRule::new(RunClass::Digit, 4, 4)
235 .followed_by(b"-")
236 .then(RunClass::Digit, 2, 2)
237 .followed_by(b"-"),
238 ]
239 }
240
241 fn try_at(&self, input: &[u8], pos: usize) -> Option<Range<usize>> {
242 if !input[pos].is_ascii_digit() {
243 return None;
244 }
245 Self::try_date(input, pos)
246 }
247
248 fn find(&self, s: &str) -> Option<Range<usize>> {
249 let input = s.as_bytes();
250 let mut idx = 0;
251
252 while idx + 10 <= input.len() {
253 if input[idx].is_ascii_digit()
254 && let Some(range) = Self::try_date(input, idx)
255 {
256 return Some(range);
257 }
258 idx += 1;
259 }
260
261 None
262 }
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268
269 #[test]
270 fn id_should_return_datetime() {
271 let finder = Datetime::default();
272 assert_eq!("datetime", finder.id());
273 }
274
275 #[test]
276 fn find_should_extract_date() {
277 let finder = Datetime::default();
278 let input = "created on 2024-01-15 by admin";
279 let range = finder.find(input).unwrap();
280 assert_eq!("2024-01-15", &input[range]);
281 }
282
283 #[test]
284 fn find_should_extract_datetime_with_t() {
285 let finder = Datetime::default();
286 let input = "timestamp: 2024-01-15T10:30:00";
287 let range = finder.find(input).unwrap();
288 assert_eq!("2024-01-15T10:30:00", &input[range]);
289 }
290
291 #[test]
292 fn find_should_extract_datetime_with_timezone_z() {
293 let finder = Datetime::default();
294 let input = "2024-01-15T10:30:00Z";
295 let range = finder.find(input).unwrap();
296 assert_eq!("2024-01-15T10:30:00Z", &input[range]);
297 }
298
299 #[test]
300 fn find_should_extract_datetime_with_timezone_offset() {
301 let finder = Datetime::default();
302 let input = "2024-01-15T10:30:00+05:30";
303 let range = finder.find(input).unwrap();
304 assert_eq!("2024-01-15T10:30:00+05:30", &input[range]);
305 }
306
307 #[test]
308 fn find_should_extract_datetime_with_negative_offset() {
309 let finder = Datetime::default();
310 let input = "2024-01-15T10:30:00-08:00";
311 let range = finder.find(input).unwrap();
312 assert_eq!("2024-01-15T10:30:00-08:00", &input[range]);
313 }
314
315 #[test]
316 fn find_should_extract_datetime_with_fractional_seconds() {
317 let finder = Datetime::default();
318 let input = "2024-01-15T10:30:00.123Z";
319 let range = finder.find(input).unwrap();
320 assert_eq!("2024-01-15T10:30:00.123Z", &input[range]);
321 }
322
323 #[test]
324 fn find_should_extract_datetime_with_fractional_and_offset() {
325 let finder = Datetime::default();
326 let input = "2024-01-15T10:30:00.123456+02:00";
327 let range = finder.find(input).unwrap();
328 assert_eq!("2024-01-15T10:30:00.123456+02:00", &input[range]);
329 }
330
331 #[test]
332 fn find_should_extract_datetime_without_seconds() {
333 let finder = Datetime::default();
334 let input = "2024-01-15T10:30Z";
335 let range = finder.find(input).unwrap();
336 assert_eq!("2024-01-15T10:30Z", &input[range]);
337 }
338
339 #[test]
340 fn find_should_extract_date_in_text() {
341 let finder = Datetime::default();
342 let input = "deployed on 2024-12-25 successfully";
343 let range = finder.find(input).unwrap();
344 assert_eq!("2024-12-25", &input[range]);
345 }
346
347 #[test]
348 fn find_should_reject_invalid_month() {
349 let finder = Datetime::default();
350 assert!(finder.find("2024-13-01").is_none());
351 }
352
353 #[test]
354 fn find_should_reject_invalid_day() {
355 let finder = Datetime::default();
356 assert!(finder.find("2024-01-32").is_none());
357 }
358
359 #[test]
360 fn find_should_reject_month_zero() {
361 let finder = Datetime::default();
362 assert!(finder.find("2024-00-15").is_none());
363 }
364
365 #[test]
366 fn find_should_reject_day_zero() {
367 let finder = Datetime::default();
368 assert!(finder.find("2024-01-00").is_none());
369 }
370
371 #[test]
372 fn find_should_reject_preceded_by_digit() {
373 let finder = Datetime::default();
374 assert!(finder.find("x12024-01-15").is_none());
375 }
376
377 #[test]
378 fn find_should_reject_followed_by_digit() {
379 let finder = Datetime::default();
380 assert!(finder.find("2024-01-155").is_none());
381 }
382
383 #[test]
384 fn find_should_handle_empty_input() {
385 let finder = Datetime::default();
386 assert!(finder.find("").is_none());
387 }
388
389 #[test]
390 fn find_should_extract_multiple_datetimes_iteratively() {
391 let finder = Datetime::default();
392 let input = "2024-01-15T10:00:00Z to 2024-01-16T12:00:00Z";
393
394 let mut results = Vec::new();
395 let mut idx = 0;
396 while idx < input.len() {
397 if let Some(range) = finder.find(&input[idx..]) {
398 results.push(&input[idx + range.start..idx + range.end]);
399 idx += range.end;
400 } else {
401 break;
402 }
403 }
404
405 assert_eq!(
406 vec!["2024-01-15T10:00:00Z", "2024-01-16T12:00:00Z"],
407 results
408 );
409 }
410
411 #[test]
412 fn find_should_extract_date_at_start() {
413 let finder = Datetime::default();
414 let input = "2024-01-15 is the date";
415 let range = finder.find(input).unwrap();
416 assert_eq!("2024-01-15", &input[range]);
417 }
418
419 #[test]
420 fn find_should_extract_date_at_end() {
421 let finder = Datetime::default();
422 let input = "the date is 2024-01-15";
423 let range = finder.find(input).unwrap();
424 assert_eq!("2024-01-15", &input[range]);
425 }
426
427 #[test]
428 fn find_should_reject_invalid_hour() {
429 let finder = Datetime::default();
430 let input = "2024-01-15T25:00:00";
431 let range = finder.find(input).unwrap();
432 assert_eq!("2024-01-15", &input[range]);
433 }
434
435 #[test]
436 fn find_should_reject_invalid_minute() {
437 let finder = Datetime::default();
438 let input = "2024-01-15T10:61:00";
439 let range = finder.find(input).unwrap();
440 assert_eq!("2024-01-15", &input[range]);
441 }
442
443 #[test]
444 fn find_should_extract_datetime_with_space_separator() {
445 let finder = Datetime::default();
446 let input = "log: 2024-01-15 10:30:00";
447 let range = finder.find(input).unwrap();
448 assert_eq!("2024-01-15 10:30:00", &input[range]);
449 }
450
451 #[test]
452 fn try_at_date_at_start() {
453 let finder = Datetime::default();
454 let input = b"2024-01-15 rest";
455 assert_eq!(finder.try_at(input, 0), Some(0..10));
456 }
457
458 #[test]
459 fn try_at_non_digit() {
460 let finder = Datetime::default();
461 let input = b"abc";
462 assert!(finder.try_at(input, 0).is_none());
463 }
464
465 #[test]
466 fn try_at_short_input() {
467 let finder = Datetime::default();
468 let input = b"2024";
469 assert!(finder.try_at(input, 0).is_none());
470 }
471
472 #[test]
473 fn try_at_preceded_by_digit() {
474 let finder = Datetime::default();
475 let input = b"12024-01-15";
476 assert!(finder.try_at(input, 1).is_none());
477 }
478
479 #[test]
480 fn find_boundary_year_1000() {
481 let finder = Datetime::default();
482 let input = "1000-01-01";
483 assert!(finder.find(input).is_some());
484 }
485
486 #[test]
487 fn find_boundary_year_9999() {
488 let finder = Datetime::default();
489 let input = "9999-12-31";
490 assert!(finder.find(input).is_some());
491 }
492
493 #[test]
494 fn find_leap_second() {
495 let finder = Datetime::default();
496 let input = "2024-06-30T23:59:60Z";
497 let range = finder.find(input).unwrap();
498 assert_eq!("2024-06-30T23:59:60Z", &input[range]);
499 }
500
501 #[test]
502 fn find_fractional_seconds_many_digits() {
503 let finder = Datetime::default();
504 let input = "2024-01-15T10:30:00.123456789Z";
505 let range = finder.find(input).unwrap();
506 assert_eq!("2024-01-15T10:30:00.123456789Z", &input[range]);
507 }
508
509 #[test]
510 fn find_single_digit_input() {
511 let finder = Datetime::default();
512 assert!(finder.find("1").is_none());
513 }
514}