1use std::sync::LazyLock;
12
13use regex::Regex;
14
15use crate::rs_var::ReadStatVarFormatClass;
16
17static RE_DATETIME_WITH_NANO: LazyLock<Regex> =
20 LazyLock::new(|| Regex::new(r"(?xi)^DATETIME[0-9]{1,2}\.[7-9]$").unwrap());
21
22static RE_DATETIME_WITH_MICRO: LazyLock<Regex> =
24 LazyLock::new(|| Regex::new(r"(?xi)^DATETIME[0-9]{1,2}\.[4-6]$").unwrap());
25
26static RE_DATETIME_WITH_MILLI: LazyLock<Regex> =
28 LazyLock::new(|| Regex::new(r"(?xi)^DATETIME[0-9]{1,2}\.[1-3]$").unwrap());
29
30static RE_TIME_WITH_NANO: LazyLock<Regex> =
32 LazyLock::new(|| Regex::new(r"(?xi)^TIME[0-9]{1,2}\.[7-9]$").unwrap());
33
34static RE_TIME_WITH_MICRO: LazyLock<Regex> =
36 LazyLock::new(|| Regex::new(r"(?xi)^TIME[0-9]{1,2}\.[4-6]$").unwrap());
37
38static RE_TIME_WITH_MILLI: LazyLock<Regex> =
40 LazyLock::new(|| Regex::new(r"(?xi)^TIME[0-9]{1,2}\.[1-3]$").unwrap());
41
42static RE_TIME: LazyLock<Regex> = LazyLock::new(|| {
45 Regex::new(
46 r"(?xi)
47 ^(
48 B8601LZ |
49 B8601TM |
50 B8601TX |
51 B8601TZ |
52 E8601LZ |
53 E8601TM |
54 E8601TX |
55 E8601TZ |
56 HHMM |
57 HOUR |
58 MMSS |
59 NLDATMTM |
60 NLDATMTZ |
61 NLTIMAP |
62 NLTIME |
63 TIMEAMPM |
64 TIME |
65 TOD
66 )[A-Z0-9]*(\.[A-Z0-9]*)?$",
67 )
68 .unwrap()
69});
70
71static RE_DATETIME: LazyLock<Regex> = LazyLock::new(|| {
74 Regex::new(
75 r"(?xi)
76 ^(
77 B8601DT |
78 B8601DX |
79 B8601DZ |
80 B8601LX |
81 DATEAMPM |
82 DATETIME |
83 E8601DT |
84 E8601DX |
85 E8601DZ |
86 E8601LX |
87 MDYAMPM |
88 NLDATM
89 )[A-Z0-9]*(\.[A-Z0-9]*)?$",
90 )
91 .unwrap()
92});
93
94static RE_DATE: LazyLock<Regex> = LazyLock::new(|| {
96 Regex::new(
97 r"(?xi)
98 ^(
99 B8601DA |
100 B8601DN |
101 DATE |
102 DAY |
103 DDMMYY |
104 DOWNAME |
105 DTDATE |
106 DTMONYY |
107 DTWKDATX |
108 DTYEAR |
109 DTYYQC |
110 E8601DA |
111 E8601DN |
112 JULDAY |
113 JULIAN |
114 MMDDYY |
115 MMYY |
116 MONNAME |
117 MONTH |
118 MONYY |
119 NENGO |
120 NLDATE |
121 QTRR? |
122 WEEKDATX |
123 WEEKDAY |
124 YEAR |
125 YYMMDD |
126 YYMM |
127 YYMON |
128 YYQR |
129 YYQ |
130 YYWEEK[UVW]
131 )[A-Z0-9]*(\.[A-Z0-9]*)?$",
132 )
133 .unwrap()
134});
135
136pub(crate) fn match_var_format(v: &str) -> Option<ReadStatVarFormatClass> {
143 if RE_DATETIME_WITH_NANO.is_match(v) {
150 Some(ReadStatVarFormatClass::DateTimeWithNanoseconds)
151 } else if RE_DATETIME_WITH_MICRO.is_match(v) {
152 Some(ReadStatVarFormatClass::DateTimeWithMicroseconds)
153 } else if RE_DATETIME_WITH_MILLI.is_match(v) {
154 Some(ReadStatVarFormatClass::DateTimeWithMilliseconds)
155 } else if RE_TIME_WITH_NANO.is_match(v) {
156 Some(ReadStatVarFormatClass::TimeWithNanoseconds)
157 } else if RE_TIME_WITH_MICRO.is_match(v) {
158 Some(ReadStatVarFormatClass::TimeWithMicroseconds)
159 } else if RE_TIME_WITH_MILLI.is_match(v) {
160 Some(ReadStatVarFormatClass::TimeWithMilliseconds)
161 } else if RE_TIME.is_match(v) {
162 Some(ReadStatVarFormatClass::Time)
163 } else if RE_DATETIME.is_match(v) {
164 Some(ReadStatVarFormatClass::DateTime)
165 } else if RE_DATE.is_match(v) {
166 Some(ReadStatVarFormatClass::Date)
167 } else {
168 None
169 }
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175
176 #[test]
179 fn date_formats_with_numeric_width() {
180 assert_eq!(
182 match_var_format("DATE9"),
183 Some(ReadStatVarFormatClass::Date)
184 );
185 assert_eq!(
186 match_var_format("DDMMYY10"),
187 Some(ReadStatVarFormatClass::Date)
188 );
189 assert_eq!(
190 match_var_format("DDMMYYB10"),
191 Some(ReadStatVarFormatClass::Date)
192 );
193 assert_eq!(
194 match_var_format("MMDDYY10"),
195 Some(ReadStatVarFormatClass::Date)
196 );
197 assert_eq!(
198 match_var_format("YYMMDD10"),
199 Some(ReadStatVarFormatClass::Date)
200 );
201 }
202
203 #[test]
204 fn recognized_date_format_names() {
205 let date_formats = [
206 "B8601DA",
207 "B8601DN",
208 "DATE",
209 "DAY",
210 "DDMMYY",
211 "DDMMYYD",
212 "DOWNAME",
213 "DTDATE",
214 "DTMONYY",
215 "DTWKDATX",
216 "DTYEAR",
217 "DTYYQC",
218 "E8601DA",
219 "E8601DN",
220 "JULDAY",
221 "JULIAN",
222 "MMDDYY",
223 "MMDDYYD",
224 "MMYY",
225 "MMYYD",
226 "MONNAME",
227 "MONTH",
228 "MONYY",
229 "NENGO",
230 "NLDATE",
231 "NLDATECP",
232 "NLDATEL",
233 "NLDATEM",
234 "NLDATEMD",
235 "NLDATEMDL",
236 "NLDATEMDM",
237 "NLDATEMDS",
238 "NLDATEMN",
239 "NLDATES",
240 "NLDATEW",
241 "NLDATEWN",
242 "NLDATEYM",
243 "NLDATEYML",
244 "NLDATEYMM",
245 "NLDATEYMS",
246 "NLDATEYQ",
247 "NLDATEYQL",
248 "NLDATEYQM",
249 "NLDATEYQS",
250 "NLDATEYR",
251 "NLDATEYW",
252 "QTR",
253 "QTRR",
254 "WEEKDATX",
255 "WEEKDAY",
256 "YEAR",
257 "YYMM",
258 "YYMMDD",
259 "YYMMDDD",
260 "YYMMD",
261 "YYMON",
262 "YYQ",
263 "YYQD",
264 "YYQR",
265 "YYQRD",
266 "YYWEEKU",
267 "YYWEEKV",
268 "YYWEEKW",
269 ];
270 for fmt in &date_formats {
271 assert_eq!(
272 match_var_format(fmt),
273 Some(ReadStatVarFormatClass::Date),
274 "Expected Date for format: {fmt}"
275 );
276 }
277 }
278
279 #[test]
282 fn time_format_bare() {
283 assert_eq!(match_var_format("TIME"), Some(ReadStatVarFormatClass::Time));
284 assert_eq!(
285 match_var_format("TIME8"),
286 Some(ReadStatVarFormatClass::Time)
287 );
288 }
289
290 #[test]
291 fn recognized_time_format_names() {
292 let time_formats = [
293 "B8601LZ", "B8601TM", "B8601TX", "B8601TZ", "E8601LZ", "E8601TM", "E8601TX", "E8601TZ",
294 "HHMM", "HOUR", "MMSS", "NLDATMTM", "NLDATMTZ", "NLTIMAP", "NLTIME", "TIME",
295 "TIMEAMPM", "TOD",
296 ];
297 for fmt in &time_formats {
298 assert_eq!(
299 match_var_format(fmt),
300 Some(ReadStatVarFormatClass::Time),
301 "Expected Time for format: {fmt}"
302 );
303 }
304 }
305
306 #[test]
309 fn datetime_format_with_numeric_width() {
310 assert_eq!(
311 match_var_format("DATETIME22"),
312 Some(ReadStatVarFormatClass::DateTime)
313 );
314 }
315
316 #[test]
317 fn time_precision_formats() {
318 for fmt in ["TIME10.1", "TIME11.2", "TIME12.3"] {
320 assert_eq!(
321 match_var_format(fmt),
322 Some(ReadStatVarFormatClass::TimeWithMilliseconds),
323 "Expected milliseconds for {fmt}"
324 );
325 }
326 for fmt in ["TIME13.4", "TIME14.5", "TIME15.6"] {
328 assert_eq!(
329 match_var_format(fmt),
330 Some(ReadStatVarFormatClass::TimeWithMicroseconds),
331 "Expected microseconds for {fmt}"
332 );
333 }
334 for fmt in ["TIME16.7", "TIME17.8", "TIME18.9"] {
336 assert_eq!(
337 match_var_format(fmt),
338 Some(ReadStatVarFormatClass::TimeWithNanoseconds),
339 "Expected nanoseconds for {fmt}"
340 );
341 }
342 assert_eq!(
344 match_var_format("TIME15"),
345 Some(ReadStatVarFormatClass::Time)
346 );
347 }
348
349 #[test]
350 fn datetime_precision_formats() {
351 assert_eq!(
352 match_var_format("DATETIME22.3"),
353 Some(ReadStatVarFormatClass::DateTimeWithMilliseconds)
354 );
355 assert_eq!(
356 match_var_format("DATETIME25.6"),
357 Some(ReadStatVarFormatClass::DateTimeWithMicroseconds)
358 );
359 assert_eq!(
360 match_var_format("DATETIME28.9"),
361 Some(ReadStatVarFormatClass::DateTimeWithNanoseconds)
362 );
363 }
364
365 #[test]
366 fn recognized_datetime_format_names() {
367 let datetime_formats = [
368 "B8601DT",
369 "B8601DX",
370 "B8601DZ",
371 "B8601LX",
372 "DATEAMPM",
373 "DATETIME",
374 "E8601DT",
375 "E8601DX",
376 "E8601DZ",
377 "E8601LX",
378 "MDYAMPM",
379 "NLDATM",
380 "NLDATMAP",
381 "NLDATMCP",
382 "NLDATMDT",
383 "NLDATML",
384 "NLDATMM",
385 "NLDATMMD",
386 "NLDATMMDL",
387 "NLDATMMDM",
388 "NLDATMMDS",
389 "NLDATMMN",
390 "NLDATMS",
391 "NLDATMW",
392 "NLDATMWN",
393 "NLDATMWZ",
394 "NLDATMYM",
395 "NLDATMYML",
396 "NLDATMYMM",
397 "NLDATMYMS",
398 "NLDATMYQ",
399 "NLDATMYQL",
400 "NLDATMYQM",
401 "NLDATMYQS",
402 "NLDATMYR",
403 "NLDATMYW",
404 "NLDATMZ",
405 ];
406 for fmt in &datetime_formats {
407 assert_eq!(
408 match_var_format(fmt),
409 Some(ReadStatVarFormatClass::DateTime),
410 "Expected DateTime for format: {fmt}"
411 );
412 }
413 }
414
415 #[test]
418 fn non_date_time_formats() {
419 assert_eq!(match_var_format("BEST12"), None);
420 assert_eq!(match_var_format("$30"), None);
421 assert_eq!(match_var_format("$10"), None);
422 assert_eq!(match_var_format("COMMA12"), None);
423 assert_eq!(match_var_format(""), None);
424 }
425
426 #[test]
429 fn case_insensitive() {
430 assert_eq!(
431 match_var_format("date9"),
432 Some(ReadStatVarFormatClass::Date)
433 );
434 assert_eq!(
435 match_var_format("datetime22"),
436 Some(ReadStatVarFormatClass::DateTime)
437 );
438 assert_eq!(
439 match_var_format("time8"),
440 Some(ReadStatVarFormatClass::Time)
441 );
442 }
443
444 mod property_tests {
447 use super::*;
448 use proptest::prelude::*;
449 use proptest::sample;
450
451 proptest! {
452 #[test]
454 fn arbitrary_strings_never_panic(s in "\\PC*") {
455 let _ = match_var_format(&s);
456 }
457
458 #[test]
460 fn result_is_deterministic(s in "\\PC*") {
461 let a = match_var_format(&s);
462 let b = match_var_format(&s);
463 prop_assert_eq!(a, b);
464 }
465
466 #[test]
468 fn case_insensitive_matching(s in "[A-Za-z0-9.]{1,20}") {
469 let upper = match_var_format(&s.to_uppercase());
470 let lower = match_var_format(&s.to_lowercase());
471 prop_assert_eq!(upper, lower, "case mismatch for '{}'", s);
472 }
473
474 #[test]
476 fn date_prefix_always_matches(
477 prefix in sample::select(vec![
478 "DATE", "DDMMYY", "MMDDYY", "YYMMDD", "JULIAN", "MONYY",
479 "YEAR", "MONTH", "DAY", "WEEKDAY", "JULDAY",
480 ]),
481 width in 1u32..30
482 ) {
483 let fmt = format!("{prefix}{width}");
484 prop_assert_eq!(
485 match_var_format(&fmt),
486 Some(ReadStatVarFormatClass::Date),
487 "Expected Date for '{}'", fmt
488 );
489 }
490
491 #[test]
493 fn time_prefix_always_matches(
494 prefix in sample::select(vec![
495 "TIME", "HHMM", "HOUR", "MMSS", "TOD", "TIMEAMPM",
496 ]),
497 width in 1u32..30
498 ) {
499 let fmt = format!("{prefix}{width}");
500 prop_assert_eq!(
501 match_var_format(&fmt),
502 Some(ReadStatVarFormatClass::Time),
503 "Expected Time for '{}'", fmt
504 );
505 }
506
507 #[test]
509 fn datetime_prefix_always_matches(width in 1u32..30) {
510 let fmt = format!("DATETIME{width}");
511 prop_assert_eq!(
512 match_var_format(&fmt),
513 Some(ReadStatVarFormatClass::DateTime),
514 "Expected DateTime for '{}'", fmt
515 );
516 }
517
518 #[test]
520 fn datetime_precision_classifies_correctly(
521 width in 1u32..30,
522 precision in 1u32..=9
523 ) {
524 let fmt = format!("DATETIME{width}.{precision}");
525 let expected = match precision {
526 1..=3 => ReadStatVarFormatClass::DateTimeWithMilliseconds,
527 4..=6 => ReadStatVarFormatClass::DateTimeWithMicroseconds,
528 7..=9 => ReadStatVarFormatClass::DateTimeWithNanoseconds,
529 _ => unreachable!(),
530 };
531 prop_assert_eq!(
532 match_var_format(&fmt),
533 Some(expected),
534 "Wrong class for '{}'", fmt
535 );
536 }
537
538 #[test]
540 fn time_precision_classifies_correctly(
541 width in 1u32..30,
542 precision in 1u32..=9
543 ) {
544 let fmt = format!("TIME{width}.{precision}");
545 let expected = match precision {
546 1..=3 => ReadStatVarFormatClass::TimeWithMilliseconds,
547 4..=6 => ReadStatVarFormatClass::TimeWithMicroseconds,
548 7..=9 => ReadStatVarFormatClass::TimeWithNanoseconds,
549 _ => unreachable!(),
550 };
551 prop_assert_eq!(
552 match_var_format(&fmt),
553 Some(expected),
554 "Wrong class for '{}'", fmt
555 );
556 }
557
558 #[test]
560 fn numeric_formats_return_none(
561 prefix in sample::select(vec!["BEST", "COMMA", "DOLLAR", "PERCENT", "F", "E"]),
562 width in 1u32..30
563 ) {
564 let fmt = format!("{prefix}{width}");
565 prop_assert_eq!(
566 match_var_format(&fmt),
567 None,
568 "Expected None for '{}'", fmt
569 );
570 }
571 }
572 }
573}