1use super::evaluator::ConditionResult;
8
9pub use super::timezone::{dst_transitions_between, is_mesz_utc, is_mez_utc, parse_dtm303};
11pub use super::werktag::{
12 compute_easter, is_german_holiday, is_werktag, parse_ccyymmdd_prefix, werktage_between,
13};
14
15pub fn validate_max_decimal_places(value: &str, max: usize) -> ConditionResult {
26 if value.is_empty() {
27 return ConditionResult::Unknown;
28 }
29 let decimal_places = match value.find('.') {
30 Some(pos) => value.len() - pos - 1,
31 None => 0,
32 };
33 ConditionResult::from(decimal_places <= max)
34}
35
36pub fn validate_max_integer_digits(value: &str, max: usize) -> ConditionResult {
40 if value.is_empty() {
41 return ConditionResult::Unknown;
42 }
43 let s = value.strip_prefix('-').unwrap_or(value);
44 let integer_part = match s.find('.') {
45 Some(pos) => &s[..pos],
46 None => s,
47 };
48 ConditionResult::from(integer_part.len() <= max)
49}
50
51pub fn validate_numeric(value: &str, op: &str, threshold: f64) -> ConditionResult {
61 let parsed = match value.parse::<f64>() {
62 Ok(v) => v,
63 Err(_) => return ConditionResult::Unknown,
64 };
65 let result = match op {
66 "==" => (parsed - threshold).abs() < f64::EPSILON,
67 "!=" => (parsed - threshold).abs() >= f64::EPSILON,
68 ">" => parsed > threshold,
69 ">=" => parsed >= threshold,
70 "<" => parsed < threshold,
71 "<=" => parsed <= threshold,
72 _ => return ConditionResult::Unknown,
73 };
74 ConditionResult::from(result)
75}
76
77pub fn validate_hhmm_equals(dtm_value: &str, expected_hhmm: &str) -> ConditionResult {
86 if dtm_value.len() < 12 {
87 return ConditionResult::False;
89 }
90 ConditionResult::from(&dtm_value[8..12] == expected_hhmm)
91}
92
93pub fn validate_hhmm_range(dtm_value: &str, min: &str, max: &str) -> ConditionResult {
97 if dtm_value.len() < 12 {
98 return ConditionResult::False;
99 }
100 let hhmm = &dtm_value[8..12];
101 ConditionResult::from(hhmm >= min && hhmm <= max)
102}
103
104pub fn validate_mmddhhmm_equals(dtm_value: &str, expected: &str) -> ConditionResult {
110 if dtm_value.len() < 12 {
111 return ConditionResult::False;
112 }
113 ConditionResult::from(&dtm_value[4..12] == expected)
114}
115
116pub fn validate_timezone_utc(dtm_value: &str) -> ConditionResult {
124 if dtm_value.len() < 15 {
125 return ConditionResult::False;
127 }
128 ConditionResult::from(&dtm_value[12..] == "+00")
129}
130
131pub fn validate_email(value: &str) -> ConditionResult {
137 if value.is_empty() {
138 return ConditionResult::Unknown;
139 }
140 ConditionResult::from(value.contains('@') && value.contains('.'))
141}
142
143pub fn validate_phone(value: &str) -> ConditionResult {
148 if value.is_empty() {
149 return ConditionResult::Unknown;
150 }
151 if !value.starts_with('+') || value.len() < 2 {
152 return ConditionResult::from(false);
153 }
154 ConditionResult::from(value[1..].chars().all(|c| c.is_ascii_digit()))
155}
156
157pub fn validate_malo_id(value: &str) -> ConditionResult {
171 if value.len() != 11 {
172 return ConditionResult::from(false);
173 }
174 if !value.chars().all(|c| c.is_ascii_digit()) {
175 return ConditionResult::from(false);
176 }
177 let digits: Vec<u32> = value.chars().filter_map(|c| c.to_digit(10)).collect();
178 let check = digits[10];
179 let mut sum = 0u32;
180 for (i, &d) in digits[..10].iter().enumerate() {
181 if (i + 1) % 2 == 0 {
183 sum += 2 * d;
184 } else {
185 sum += d;
186 }
187 }
188 let expected = (10 - (sum % 10)) % 10;
189 ConditionResult::from(check == expected)
190}
191
192pub fn validate_tr_id(value: &str) -> ConditionResult {
194 if value.is_empty() {
195 return ConditionResult::Unknown;
196 }
197 ConditionResult::from(value.len() <= 35 && value.chars().all(|c| c.is_ascii_alphanumeric()))
198}
199
200fn validate_prefixed_location_id(value: &str, prefix: char) -> ConditionResult {
203 let mut chars = value.chars();
204 ConditionResult::from(
205 value.len() == 11
206 && chars.next() == Some(prefix)
207 && chars.all(|c| c.is_ascii_digit() || c.is_ascii_uppercase()),
208 )
209}
210
211pub fn validate_sr_id(value: &str) -> ConditionResult {
214 validate_prefixed_location_id(value, 'C')
215}
216
217pub fn validate_nelo_id(value: &str) -> ConditionResult {
219 validate_prefixed_location_id(value, 'E')
220}
221
222pub fn parse_obis(value: &str) -> Option<(u32, u32)> {
239 let value = value.trim();
240 let (_ab, cde) = value.split_once(':')?;
242 let cde = cde.split('*').next()?;
244 let mut parts = cde.split('.');
245 let c = parts.next()?.parse::<u32>().ok()?;
246 let d = parts.next()?.parse::<u32>().ok()?;
247 Some((c, d))
248}
249
250pub fn is_obis_wirkarbeit_kumuliert(value: &str) -> bool {
256 matches!(parse_obis(value), Some((1 | 2, 8 | 9)))
257}
258
259pub fn is_obis_wirkarbeit_quarter_hour(value: &str) -> bool {
266 matches!(parse_obis(value), Some((1 | 2, 29)))
267}
268
269pub fn validate_x509_cert_body(value: &str) -> ConditionResult {
288 let trimmed = value.trim();
289 if trimmed.is_empty() {
290 return ConditionResult::Unknown;
291 }
292 let body = trimmed
295 .trim_start_matches("-----BEGIN CERTIFICATE-----")
296 .trim_end_matches("-----END CERTIFICATE-----")
297 .trim();
298 let chars_ok = body.chars().all(|c| {
299 c.is_ascii_alphanumeric() || matches!(c, '+' | '/' | '=' | '\n' | '\r' | '\t' | ' ')
300 });
301 let len_ok = body.chars().filter(|c| !c.is_whitespace()).count() >= 100;
304 ConditionResult::from(chars_ok && len_ok)
305}
306
307pub fn validate_zahlpunkt(value: &str) -> ConditionResult {
309 if value.len() != 33 {
310 return ConditionResult::from(false);
311 }
312 ConditionResult::from(value.chars().all(|c| c.is_ascii_alphanumeric()))
313}
314
315pub fn validate_malo_or_zahlpunkt(value: &str) -> ConditionResult {
317 if value.len() == 11 && validate_malo_id(value).is_true() {
318 return ConditionResult::True;
319 }
320 if value.len() == 33 && validate_zahlpunkt(value).is_true() {
321 return ConditionResult::True;
322 }
323 ConditionResult::False
324}
325
326pub fn validate_artikel_pattern(value: &str, segment_lengths: &[usize]) -> ConditionResult {
335 if value.is_empty() {
336 return ConditionResult::Unknown;
337 }
338 let parts: Vec<&str> = value.split('-').collect();
339 if parts.len() != segment_lengths.len() {
340 return ConditionResult::from(false);
341 }
342 let valid = parts
343 .iter()
344 .zip(segment_lengths.iter())
345 .all(|(part, &expected_len)| {
346 part.len() == expected_len && part.chars().all(|c| c.is_ascii_digit())
347 });
348 ConditionResult::from(valid)
349}
350
351pub fn validate_exact_length(value: &str, expected: usize) -> ConditionResult {
355 if value.is_empty() {
356 return ConditionResult::Unknown;
357 }
358 ConditionResult::from(value.len() == expected)
359}
360
361pub fn validate_max_length(value: &str, max: usize) -> ConditionResult {
363 if value.is_empty() {
364 return ConditionResult::Unknown;
365 }
366 ConditionResult::from(value.len() <= max)
367}
368
369pub fn validate_all_digits(value: &str) -> ConditionResult {
371 if value.is_empty() {
372 return ConditionResult::Unknown;
373 }
374 ConditionResult::from(value.chars().all(|c| c.is_ascii_digit()))
375}
376
377pub fn utc_now_ccyymmddhhmm() -> String {
379 chrono::Utc::now().format("%Y%m%d%H%M").to_string()
380}
381
382#[cfg(test)]
383mod tests {
384 use super::*;
385
386 #[test]
389 fn test_max_decimal_places() {
390 assert_eq!(
391 validate_max_decimal_places("123.45", 2),
392 ConditionResult::True
393 );
394 assert_eq!(
395 validate_max_decimal_places("123.456", 2),
396 ConditionResult::False
397 );
398 assert_eq!(validate_max_decimal_places("123", 2), ConditionResult::True);
399 assert_eq!(validate_max_decimal_places("0.1", 3), ConditionResult::True);
400 assert_eq!(validate_max_decimal_places("", 2), ConditionResult::Unknown);
401 }
402
403 #[test]
404 fn test_no_decimal_places() {
405 assert_eq!(validate_max_decimal_places("100", 0), ConditionResult::True);
406 assert_eq!(
407 validate_max_decimal_places("100.5", 0),
408 ConditionResult::False
409 );
410 }
411
412 #[test]
413 fn test_max_integer_digits() {
414 assert_eq!(
415 validate_max_integer_digits("1234", 4),
416 ConditionResult::True
417 );
418 assert_eq!(
419 validate_max_integer_digits("12345", 4),
420 ConditionResult::False
421 );
422 assert_eq!(
423 validate_max_integer_digits("-123.45", 4),
424 ConditionResult::True
425 );
426 assert_eq!(validate_max_integer_digits("", 4), ConditionResult::Unknown);
427 }
428
429 #[test]
432 fn test_validate_numeric() {
433 assert_eq!(validate_numeric("5.0", ">=", 0.0), ConditionResult::True);
434 assert_eq!(validate_numeric("-1.0", ">=", 0.0), ConditionResult::False);
435 assert_eq!(validate_numeric("1", "==", 1.0), ConditionResult::True);
436 assert_eq!(validate_numeric("2", "==", 1.0), ConditionResult::False);
437 assert_eq!(validate_numeric("0", ">", 0.0), ConditionResult::False);
438 assert_eq!(validate_numeric("1", ">", 0.0), ConditionResult::True);
439 assert_eq!(validate_numeric("abc", ">=", 0.0), ConditionResult::Unknown);
440 }
441
442 #[test]
445 fn test_hhmm_equals() {
446 assert_eq!(
447 validate_hhmm_equals("202601012200+00", "2200"),
448 ConditionResult::True
449 );
450 assert_eq!(
451 validate_hhmm_equals("202601012300+00", "2200"),
452 ConditionResult::False
453 );
454 assert_eq!(
455 validate_hhmm_equals("short", "2200"),
456 ConditionResult::False
457 );
458 }
459
460 #[test]
461 fn test_hhmm_range() {
462 assert_eq!(
463 validate_hhmm_range("202601011530+00", "0000", "2359"),
464 ConditionResult::True
465 );
466 assert_eq!(
467 validate_hhmm_range("202601010000+00", "0000", "2359"),
468 ConditionResult::True
469 );
470 assert_eq!(
471 validate_hhmm_range("202601012359+00", "0000", "2359"),
472 ConditionResult::True
473 );
474 }
475
476 #[test]
477 fn test_mmddhhmm_equals() {
478 assert_eq!(
479 validate_mmddhhmm_equals("202612312300+00", "12312300"),
480 ConditionResult::True
481 );
482 assert_eq!(
483 validate_mmddhhmm_equals("202601012200+00", "12312300"),
484 ConditionResult::False
485 );
486 }
487
488 #[test]
489 fn test_timezone_utc() {
490 assert_eq!(
491 validate_timezone_utc("202601012200+00"),
492 ConditionResult::True
493 );
494 assert_eq!(
495 validate_timezone_utc("202601012200+01"),
496 ConditionResult::False
497 );
498 assert_eq!(
499 validate_timezone_utc("202601012200"),
500 ConditionResult::False
501 );
502 }
503
504 #[test]
507 fn test_email() {
508 assert_eq!(validate_email("user@example.com"), ConditionResult::True);
509 assert_eq!(validate_email("nope"), ConditionResult::False);
510 assert_eq!(validate_email("has@but-no-dot"), ConditionResult::False);
511 assert_eq!(validate_email(""), ConditionResult::Unknown);
512 }
513
514 #[test]
515 fn test_phone() {
516 assert_eq!(validate_phone("+4930123456"), ConditionResult::True);
517 assert_eq!(validate_phone("030123456"), ConditionResult::False);
518 assert_eq!(validate_phone("+"), ConditionResult::False);
519 assert_eq!(validate_phone("+49 30 123"), ConditionResult::False); assert_eq!(validate_phone(""), ConditionResult::Unknown);
521 }
522
523 #[test]
526 fn test_malo_id() {
527 assert_eq!(validate_malo_id("51238696781"), ConditionResult::True);
531 assert_eq!(validate_malo_id("41373559241"), ConditionResult::True);
532 assert_eq!(validate_malo_id("56789012345"), ConditionResult::True);
533 assert_eq!(validate_malo_id("52935155442"), ConditionResult::True);
534
535 assert_eq!(validate_malo_id("41373559240"), ConditionResult::False); assert_eq!(validate_malo_id("512386967890"), ConditionResult::False); assert_eq!(validate_malo_id("1234567890"), ConditionResult::False); assert_eq!(validate_malo_id("abcdefghijk"), ConditionResult::False); }
541
542 #[test]
543 fn test_zahlpunkt() {
544 let valid = "DE0001234567890123456789012345678";
545 assert_eq!(valid.len(), 33);
546 assert_eq!(validate_zahlpunkt(valid), ConditionResult::True);
547 assert_eq!(validate_zahlpunkt("tooshort"), ConditionResult::False);
548 }
549
550 #[test]
553 fn test_artikel_pattern() {
554 assert_eq!(
555 validate_artikel_pattern("1-23-4", &[1, 2, 1]),
556 ConditionResult::True
557 );
558 assert_eq!(
559 validate_artikel_pattern("1-23-4-567", &[1, 2, 1, 3]),
560 ConditionResult::True
561 );
562 assert_eq!(
563 validate_artikel_pattern("1-23-4-56", &[1, 2, 1, 3]),
564 ConditionResult::False
565 );
566 assert_eq!(
567 validate_artikel_pattern("1-AB-4", &[1, 2, 1]),
568 ConditionResult::False
569 );
570 assert_eq!(
571 validate_artikel_pattern("", &[1, 2, 1]),
572 ConditionResult::Unknown
573 );
574 }
575
576 #[test]
579 fn test_tr_id() {
580 assert_eq!(validate_tr_id("ABC123"), ConditionResult::True);
581 assert_eq!(validate_tr_id("A"), ConditionResult::True);
582 assert_eq!(validate_tr_id(&"A".repeat(35)), ConditionResult::True);
583 assert_eq!(validate_tr_id(&"A".repeat(36)), ConditionResult::False);
584 assert_eq!(validate_tr_id("has spaces"), ConditionResult::False);
585 assert_eq!(validate_tr_id("has-dash"), ConditionResult::False);
586 assert_eq!(validate_tr_id(""), ConditionResult::Unknown);
587 }
588
589 #[test]
590 fn test_parse_obis() {
591 assert_eq!(parse_obis("1-1:1.8.0"), Some((1, 8)));
592 assert_eq!(parse_obis("1-1:2.29.0"), Some((2, 29)));
593 assert_eq!(parse_obis("1-0:1.8.0*255"), Some((1, 8))); assert_eq!(parse_obis(" 1-1:1.8.0 "), Some((1, 8))); assert_eq!(parse_obis("no-colon"), None);
596 assert_eq!(parse_obis("1-1:abc.8.0"), None);
597 assert_eq!(parse_obis(""), None);
598 }
599
600 #[test]
601 fn test_obis_wirkarbeit_kumuliert() {
602 assert!(is_obis_wirkarbeit_kumuliert("1-1:1.8.0"));
604 assert!(is_obis_wirkarbeit_kumuliert("1-1:2.8.0"));
605 assert!(is_obis_wirkarbeit_kumuliert("1-1:1.9.0"));
606 assert!(is_obis_wirkarbeit_kumuliert("1-1:2.9.0"));
607 assert!(is_obis_wirkarbeit_kumuliert("1-0:1.8.0*255"));
608 assert!(!is_obis_wirkarbeit_kumuliert("1-1:3.8.0"));
610 assert!(!is_obis_wirkarbeit_kumuliert("1-1:4.8.0"));
611 assert!(!is_obis_wirkarbeit_kumuliert("1-1:1.29.0"));
613 assert!(!is_obis_wirkarbeit_kumuliert("1-1:1.5.0"));
615 }
616
617 #[test]
618 fn test_obis_wirkarbeit_quarter_hour() {
619 assert!(is_obis_wirkarbeit_quarter_hour("1-1:1.29.0"));
621 assert!(is_obis_wirkarbeit_quarter_hour("1-1:2.29.0"));
622 assert!(is_obis_wirkarbeit_quarter_hour("1-0:1.29.0*255"));
623 assert!(!is_obis_wirkarbeit_quarter_hour("1-1:1.8.0"));
625 assert!(!is_obis_wirkarbeit_quarter_hour("1-1:1.9.0"));
626 assert!(!is_obis_wirkarbeit_quarter_hour("1-1:3.29.0"));
628 }
629
630 #[test]
631 fn test_x509_cert_body() {
632 let body = "A".repeat(600);
634 assert_eq!(validate_x509_cert_body(&body), ConditionResult::True);
635
636 let pem_body = "A".repeat(64) + "\n" + &"B".repeat(64) + "\n" + &"C".repeat(64);
638 assert_eq!(validate_x509_cert_body(&pem_body), ConditionResult::True);
639
640 let armored = format!(
642 "-----BEGIN CERTIFICATE-----\n{}\n-----END CERTIFICATE-----",
643 "A".repeat(200)
644 );
645 assert_eq!(validate_x509_cert_body(&armored), ConditionResult::True);
646
647 let with_punct = "AB+/==".to_string() + &"A".repeat(200);
649 assert_eq!(validate_x509_cert_body(&with_punct), ConditionResult::True);
650
651 assert_eq!(validate_x509_cert_body("QUJD"), ConditionResult::False);
653
654 let invalid_chars = "A".repeat(200) + "!@#";
656 assert_eq!(
657 validate_x509_cert_body(&invalid_chars),
658 ConditionResult::False
659 );
660
661 assert_eq!(validate_x509_cert_body(""), ConditionResult::Unknown);
663 assert_eq!(validate_x509_cert_body(" \n "), ConditionResult::Unknown);
664 }
665
666 #[test]
670 fn test_sr_id() {
671 for real in ["C816417ST77", "CZZ6M5DGSS5", "COPFWS2EX96"] {
672 assert_eq!(validate_sr_id(real), ConditionResult::True, "{real}");
673 }
674 assert_eq!(
675 validate_sr_id("51238696781"),
676 ConditionResult::False,
677 "a MaLo-ID"
678 );
679 assert_eq!(validate_sr_id("C12345"), ConditionResult::False);
680 assert_eq!(validate_sr_id(""), ConditionResult::False);
681 }
682
683 #[test]
687 fn test_nelo_id() {
688 for real in ["E1688117482", "EU5UQAWW8D0", "EEW7TM7V906"] {
689 assert_eq!(validate_nelo_id(real), ConditionResult::True, "{real}");
690 }
691 assert_eq!(
692 validate_nelo_id("51238696781"),
693 ConditionResult::False,
694 "a MaLo-ID"
695 );
696 assert_eq!(validate_nelo_id("E123"), ConditionResult::False);
697 assert_eq!(validate_nelo_id("e1688117482"), ConditionResult::False);
698 }
699
700 #[test]
703 fn test_exact_length() {
704 assert_eq!(
705 validate_exact_length("1234567890123456", 16),
706 ConditionResult::True
707 );
708 assert_eq!(validate_exact_length("123", 16), ConditionResult::False);
709 assert_eq!(validate_exact_length("", 16), ConditionResult::Unknown);
710 }
711
712 #[test]
713 fn test_all_digits() {
714 assert_eq!(validate_all_digits("12345"), ConditionResult::True);
715 assert_eq!(validate_all_digits("123a5"), ConditionResult::False);
716 assert_eq!(validate_all_digits(""), ConditionResult::Unknown);
717 }
718}