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 {
127 if dtm_value.len() < 15 || !dtm_value.is_char_boundary(dtm_value.len() - 3) {
128 return ConditionResult::False;
130 }
131 ConditionResult::from(&dtm_value[dtm_value.len() - 3..] == "+00")
132}
133
134pub fn validate_email(value: &str) -> ConditionResult {
140 if value.is_empty() {
141 return ConditionResult::Unknown;
142 }
143 ConditionResult::from(value.contains('@') && value.contains('.'))
144}
145
146pub fn validate_phone(value: &str) -> ConditionResult {
151 if value.is_empty() {
152 return ConditionResult::Unknown;
153 }
154 if !value.starts_with('+') || value.len() < 2 {
155 return ConditionResult::from(false);
156 }
157 ConditionResult::from(value[1..].chars().all(|c| c.is_ascii_digit()))
158}
159
160pub fn validate_malo_id(value: &str) -> ConditionResult {
174 if value.len() != 11 {
175 return ConditionResult::from(false);
176 }
177 if !value.chars().all(|c| c.is_ascii_digit()) {
178 return ConditionResult::from(false);
179 }
180 let digits: Vec<u32> = value.chars().filter_map(|c| c.to_digit(10)).collect();
181 let check = digits[10];
182 let mut sum = 0u32;
183 for (i, &d) in digits[..10].iter().enumerate() {
184 if (i + 1) % 2 == 0 {
186 sum += 2 * d;
187 } else {
188 sum += d;
189 }
190 }
191 let expected = (10 - (sum % 10)) % 10;
192 ConditionResult::from(check == expected)
193}
194
195pub fn validate_tr_id(value: &str) -> ConditionResult {
197 if value.is_empty() {
198 return ConditionResult::Unknown;
199 }
200 ConditionResult::from(value.len() <= 35 && value.chars().all(|c| c.is_ascii_alphanumeric()))
201}
202
203fn validate_prefixed_location_id(value: &str, prefix: char) -> ConditionResult {
206 let mut chars = value.chars();
207 ConditionResult::from(
208 value.len() == 11
209 && chars.next() == Some(prefix)
210 && chars.all(|c| c.is_ascii_digit() || c.is_ascii_uppercase()),
211 )
212}
213
214pub fn validate_sr_id(value: &str) -> ConditionResult {
217 validate_prefixed_location_id(value, 'C')
218}
219
220pub fn validate_nelo_id(value: &str) -> ConditionResult {
222 validate_prefixed_location_id(value, 'E')
223}
224
225pub fn parse_obis(value: &str) -> Option<(u32, u32)> {
242 let value = value.trim();
243 let (_ab, cde) = value.split_once(':')?;
245 let cde = cde.split('*').next()?;
247 let mut parts = cde.split('.');
248 let c = parts.next()?.parse::<u32>().ok()?;
249 let d = parts.next()?.parse::<u32>().ok()?;
250 Some((c, d))
251}
252
253pub fn is_obis_wirkarbeit_kumuliert(value: &str) -> bool {
259 matches!(parse_obis(value), Some((1 | 2, 8 | 9)))
260}
261
262pub fn is_obis_wirkarbeit_quarter_hour(value: &str) -> bool {
269 matches!(parse_obis(value), Some((1 | 2, 29)))
270}
271
272pub fn validate_x509_cert_body(value: &str) -> ConditionResult {
291 let trimmed = value.trim();
292 if trimmed.is_empty() {
293 return ConditionResult::Unknown;
294 }
295 let body = trimmed
298 .trim_start_matches("-----BEGIN CERTIFICATE-----")
299 .trim_end_matches("-----END CERTIFICATE-----")
300 .trim();
301 let chars_ok = body.chars().all(|c| {
302 c.is_ascii_alphanumeric() || matches!(c, '+' | '/' | '=' | '\n' | '\r' | '\t' | ' ')
303 });
304 let len_ok = body.chars().filter(|c| !c.is_whitespace()).count() >= 100;
307 ConditionResult::from(chars_ok && len_ok)
308}
309
310pub fn validate_zahlpunkt(value: &str) -> ConditionResult {
312 if value.len() != 33 {
313 return ConditionResult::from(false);
314 }
315 ConditionResult::from(value.chars().all(|c| c.is_ascii_alphanumeric()))
316}
317
318pub fn validate_malo_or_zahlpunkt(value: &str) -> ConditionResult {
320 if value.len() == 11 && validate_malo_id(value).is_true() {
321 return ConditionResult::True;
322 }
323 if value.len() == 33 && validate_zahlpunkt(value).is_true() {
324 return ConditionResult::True;
325 }
326 ConditionResult::False
327}
328
329pub fn validate_artikel_pattern(value: &str, segment_lengths: &[usize]) -> ConditionResult {
338 if value.is_empty() {
339 return ConditionResult::Unknown;
340 }
341 let parts: Vec<&str> = value.split('-').collect();
342 if parts.len() != segment_lengths.len() {
343 return ConditionResult::from(false);
344 }
345 let valid = parts
346 .iter()
347 .zip(segment_lengths.iter())
348 .all(|(part, &expected_len)| {
349 part.len() == expected_len && part.chars().all(|c| c.is_ascii_digit())
350 });
351 ConditionResult::from(valid)
352}
353
354pub fn validate_exact_length(value: &str, expected: usize) -> ConditionResult {
358 if value.is_empty() {
359 return ConditionResult::Unknown;
360 }
361 ConditionResult::from(value.len() == expected)
362}
363
364pub fn validate_max_length(value: &str, max: usize) -> ConditionResult {
366 if value.is_empty() {
367 return ConditionResult::Unknown;
368 }
369 ConditionResult::from(value.len() <= max)
370}
371
372pub fn validate_all_digits(value: &str) -> ConditionResult {
374 if value.is_empty() {
375 return ConditionResult::Unknown;
376 }
377 ConditionResult::from(value.chars().all(|c| c.is_ascii_digit()))
378}
379
380pub fn utc_now_ccyymmddhhmm() -> String {
382 chrono::Utc::now().format("%Y%m%d%H%M").to_string()
383}
384
385#[cfg(test)]
386mod tests {
387 use super::*;
388
389 #[test]
392 fn test_max_decimal_places() {
393 assert_eq!(
394 validate_max_decimal_places("123.45", 2),
395 ConditionResult::True
396 );
397 assert_eq!(
398 validate_max_decimal_places("123.456", 2),
399 ConditionResult::False
400 );
401 assert_eq!(validate_max_decimal_places("123", 2), ConditionResult::True);
402 assert_eq!(validate_max_decimal_places("0.1", 3), ConditionResult::True);
403 assert_eq!(validate_max_decimal_places("", 2), ConditionResult::Unknown);
404 }
405
406 #[test]
407 fn test_no_decimal_places() {
408 assert_eq!(validate_max_decimal_places("100", 0), ConditionResult::True);
409 assert_eq!(
410 validate_max_decimal_places("100.5", 0),
411 ConditionResult::False
412 );
413 }
414
415 #[test]
416 fn test_max_integer_digits() {
417 assert_eq!(
418 validate_max_integer_digits("1234", 4),
419 ConditionResult::True
420 );
421 assert_eq!(
422 validate_max_integer_digits("12345", 4),
423 ConditionResult::False
424 );
425 assert_eq!(
426 validate_max_integer_digits("-123.45", 4),
427 ConditionResult::True
428 );
429 assert_eq!(validate_max_integer_digits("", 4), ConditionResult::Unknown);
430 }
431
432 #[test]
435 fn test_validate_numeric() {
436 assert_eq!(validate_numeric("5.0", ">=", 0.0), ConditionResult::True);
437 assert_eq!(validate_numeric("-1.0", ">=", 0.0), ConditionResult::False);
438 assert_eq!(validate_numeric("1", "==", 1.0), ConditionResult::True);
439 assert_eq!(validate_numeric("2", "==", 1.0), ConditionResult::False);
440 assert_eq!(validate_numeric("0", ">", 0.0), ConditionResult::False);
441 assert_eq!(validate_numeric("1", ">", 0.0), ConditionResult::True);
442 assert_eq!(validate_numeric("abc", ">=", 0.0), ConditionResult::Unknown);
443 }
444
445 #[test]
448 fn test_hhmm_equals() {
449 assert_eq!(
450 validate_hhmm_equals("202601012200+00", "2200"),
451 ConditionResult::True
452 );
453 assert_eq!(
454 validate_hhmm_equals("202601012300+00", "2200"),
455 ConditionResult::False
456 );
457 assert_eq!(
458 validate_hhmm_equals("short", "2200"),
459 ConditionResult::False
460 );
461 }
462
463 #[test]
464 fn test_hhmm_range() {
465 assert_eq!(
466 validate_hhmm_range("202601011530+00", "0000", "2359"),
467 ConditionResult::True
468 );
469 assert_eq!(
470 validate_hhmm_range("202601010000+00", "0000", "2359"),
471 ConditionResult::True
472 );
473 assert_eq!(
474 validate_hhmm_range("202601012359+00", "0000", "2359"),
475 ConditionResult::True
476 );
477 }
478
479 #[test]
480 fn test_mmddhhmm_equals() {
481 assert_eq!(
482 validate_mmddhhmm_equals("202612312300+00", "12312300"),
483 ConditionResult::True
484 );
485 assert_eq!(
486 validate_mmddhhmm_equals("202601012200+00", "12312300"),
487 ConditionResult::False
488 );
489 }
490
491 #[test]
492 fn test_timezone_utc() {
493 assert_eq!(
494 validate_timezone_utc("202601012200+00"),
495 ConditionResult::True
496 );
497 assert_eq!(
498 validate_timezone_utc("202601012200+01"),
499 ConditionResult::False
500 );
501 assert_eq!(
502 validate_timezone_utc("202601012200"),
503 ConditionResult::False
504 );
505 assert_eq!(
507 validate_timezone_utc("20260101220000+00"),
508 ConditionResult::True
509 );
510 assert_eq!(
511 validate_timezone_utc("20260101220000+01"),
512 ConditionResult::False
513 );
514 }
515
516 #[test]
519 fn test_email() {
520 assert_eq!(validate_email("user@example.com"), ConditionResult::True);
521 assert_eq!(validate_email("nope"), ConditionResult::False);
522 assert_eq!(validate_email("has@but-no-dot"), ConditionResult::False);
523 assert_eq!(validate_email(""), ConditionResult::Unknown);
524 }
525
526 #[test]
527 fn test_phone() {
528 assert_eq!(validate_phone("+4930123456"), ConditionResult::True);
529 assert_eq!(validate_phone("030123456"), ConditionResult::False);
530 assert_eq!(validate_phone("+"), ConditionResult::False);
531 assert_eq!(validate_phone("+49 30 123"), ConditionResult::False); assert_eq!(validate_phone(""), ConditionResult::Unknown);
533 }
534
535 #[test]
538 fn test_malo_id() {
539 assert_eq!(validate_malo_id("51238696781"), ConditionResult::True);
543 assert_eq!(validate_malo_id("41373559241"), ConditionResult::True);
544 assert_eq!(validate_malo_id("56789012345"), ConditionResult::True);
545 assert_eq!(validate_malo_id("52935155442"), ConditionResult::True);
546
547 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); }
553
554 #[test]
555 fn test_zahlpunkt() {
556 let valid = "DE0001234567890123456789012345678";
557 assert_eq!(valid.len(), 33);
558 assert_eq!(validate_zahlpunkt(valid), ConditionResult::True);
559 assert_eq!(validate_zahlpunkt("tooshort"), ConditionResult::False);
560 }
561
562 #[test]
565 fn test_artikel_pattern() {
566 assert_eq!(
567 validate_artikel_pattern("1-23-4", &[1, 2, 1]),
568 ConditionResult::True
569 );
570 assert_eq!(
571 validate_artikel_pattern("1-23-4-567", &[1, 2, 1, 3]),
572 ConditionResult::True
573 );
574 assert_eq!(
575 validate_artikel_pattern("1-23-4-56", &[1, 2, 1, 3]),
576 ConditionResult::False
577 );
578 assert_eq!(
579 validate_artikel_pattern("1-AB-4", &[1, 2, 1]),
580 ConditionResult::False
581 );
582 assert_eq!(
583 validate_artikel_pattern("", &[1, 2, 1]),
584 ConditionResult::Unknown
585 );
586 }
587
588 #[test]
591 fn test_tr_id() {
592 assert_eq!(validate_tr_id("ABC123"), ConditionResult::True);
593 assert_eq!(validate_tr_id("A"), ConditionResult::True);
594 assert_eq!(validate_tr_id(&"A".repeat(35)), ConditionResult::True);
595 assert_eq!(validate_tr_id(&"A".repeat(36)), ConditionResult::False);
596 assert_eq!(validate_tr_id("has spaces"), ConditionResult::False);
597 assert_eq!(validate_tr_id("has-dash"), ConditionResult::False);
598 assert_eq!(validate_tr_id(""), ConditionResult::Unknown);
599 }
600
601 #[test]
602 fn test_parse_obis() {
603 assert_eq!(parse_obis("1-1:1.8.0"), Some((1, 8)));
604 assert_eq!(parse_obis("1-1:2.29.0"), Some((2, 29)));
605 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);
608 assert_eq!(parse_obis("1-1:abc.8.0"), None);
609 assert_eq!(parse_obis(""), None);
610 }
611
612 #[test]
613 fn test_obis_wirkarbeit_kumuliert() {
614 assert!(is_obis_wirkarbeit_kumuliert("1-1:1.8.0"));
616 assert!(is_obis_wirkarbeit_kumuliert("1-1:2.8.0"));
617 assert!(is_obis_wirkarbeit_kumuliert("1-1:1.9.0"));
618 assert!(is_obis_wirkarbeit_kumuliert("1-1:2.9.0"));
619 assert!(is_obis_wirkarbeit_kumuliert("1-0:1.8.0*255"));
620 assert!(!is_obis_wirkarbeit_kumuliert("1-1:3.8.0"));
622 assert!(!is_obis_wirkarbeit_kumuliert("1-1:4.8.0"));
623 assert!(!is_obis_wirkarbeit_kumuliert("1-1:1.29.0"));
625 assert!(!is_obis_wirkarbeit_kumuliert("1-1:1.5.0"));
627 }
628
629 #[test]
630 fn test_obis_wirkarbeit_quarter_hour() {
631 assert!(is_obis_wirkarbeit_quarter_hour("1-1:1.29.0"));
633 assert!(is_obis_wirkarbeit_quarter_hour("1-1:2.29.0"));
634 assert!(is_obis_wirkarbeit_quarter_hour("1-0:1.29.0*255"));
635 assert!(!is_obis_wirkarbeit_quarter_hour("1-1:1.8.0"));
637 assert!(!is_obis_wirkarbeit_quarter_hour("1-1:1.9.0"));
638 assert!(!is_obis_wirkarbeit_quarter_hour("1-1:3.29.0"));
640 }
641
642 #[test]
643 fn test_x509_cert_body() {
644 let body = "A".repeat(600);
646 assert_eq!(validate_x509_cert_body(&body), ConditionResult::True);
647
648 let pem_body = "A".repeat(64) + "\n" + &"B".repeat(64) + "\n" + &"C".repeat(64);
650 assert_eq!(validate_x509_cert_body(&pem_body), ConditionResult::True);
651
652 let armored = format!(
654 "-----BEGIN CERTIFICATE-----\n{}\n-----END CERTIFICATE-----",
655 "A".repeat(200)
656 );
657 assert_eq!(validate_x509_cert_body(&armored), ConditionResult::True);
658
659 let with_punct = "AB+/==".to_string() + &"A".repeat(200);
661 assert_eq!(validate_x509_cert_body(&with_punct), ConditionResult::True);
662
663 assert_eq!(validate_x509_cert_body("QUJD"), ConditionResult::False);
665
666 let invalid_chars = "A".repeat(200) + "!@#";
668 assert_eq!(
669 validate_x509_cert_body(&invalid_chars),
670 ConditionResult::False
671 );
672
673 assert_eq!(validate_x509_cert_body(""), ConditionResult::Unknown);
675 assert_eq!(validate_x509_cert_body(" \n "), ConditionResult::Unknown);
676 }
677
678 #[test]
682 fn test_sr_id() {
683 for real in ["C816417ST77", "CZZ6M5DGSS5", "COPFWS2EX96"] {
684 assert_eq!(validate_sr_id(real), ConditionResult::True, "{real}");
685 }
686 assert_eq!(
687 validate_sr_id("51238696781"),
688 ConditionResult::False,
689 "a MaLo-ID"
690 );
691 assert_eq!(validate_sr_id("C12345"), ConditionResult::False);
692 assert_eq!(validate_sr_id(""), ConditionResult::False);
693 }
694
695 #[test]
699 fn test_nelo_id() {
700 for real in ["E1688117482", "EU5UQAWW8D0", "EEW7TM7V906"] {
701 assert_eq!(validate_nelo_id(real), ConditionResult::True, "{real}");
702 }
703 assert_eq!(
704 validate_nelo_id("51238696781"),
705 ConditionResult::False,
706 "a MaLo-ID"
707 );
708 assert_eq!(validate_nelo_id("E123"), ConditionResult::False);
709 assert_eq!(validate_nelo_id("e1688117482"), ConditionResult::False);
710 }
711
712 #[test]
715 fn test_exact_length() {
716 assert_eq!(
717 validate_exact_length("1234567890123456", 16),
718 ConditionResult::True
719 );
720 assert_eq!(validate_exact_length("123", 16), ConditionResult::False);
721 assert_eq!(validate_exact_length("", 16), ConditionResult::Unknown);
722 }
723
724 #[test]
725 fn test_all_digits() {
726 assert_eq!(validate_all_digits("12345"), ConditionResult::True);
727 assert_eq!(validate_all_digits("123a5"), ConditionResult::False);
728 assert_eq!(validate_all_digits(""), ConditionResult::Unknown);
729 }
730}