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 {
63 if value.is_empty() {
64 return ConditionResult::Unknown;
65 }
66 let parsed = match edifact_number(value) {
67 Some(v) => v,
68 None => return ConditionResult::False,
69 };
70 let result = match op {
71 "==" => (parsed - threshold).abs() < f64::EPSILON,
72 "!=" => (parsed - threshold).abs() >= f64::EPSILON,
73 ">" => parsed > threshold,
74 ">=" => parsed >= threshold,
75 "<" => parsed < threshold,
76 "<=" => parsed <= threshold,
77 _ => return ConditionResult::Unknown,
78 };
79 ConditionResult::from(result)
80}
81
82pub fn is_edifact_number(value: &str) -> bool {
86 edifact_number(value).is_some()
87}
88
89fn edifact_number(value: &str) -> Option<f64> {
92 let unsigned = value.strip_prefix('-').unwrap_or(value);
93 let mut marks = 0;
94 let mut digits = 0;
95 for c in unsigned.chars() {
96 match c {
97 '0'..='9' => digits += 1,
98 '.' | ',' => marks += 1,
99 _ => return None,
100 }
101 }
102 if digits == 0 || marks > 1 {
103 return None;
104 }
105 value.replace(',', ".").parse().ok()
106}
107
108pub fn fits_date_format(value: &str, format: &str) -> Option<bool> {
116 let digits = |s: &str| !s.is_empty() && s.bytes().all(|b| b.is_ascii_digit());
117 let num = |s: &str| s.parse::<u32>().unwrap_or(u32::MAX);
118 let month = |s: &str| (1..=12).contains(&num(s));
119 let day = |s: &str| (1..=31).contains(&num(s));
120 let week = |s: &str| (1..=53).contains(&num(s));
121 let time = |s: &str| num(&s[..2]) <= 23 && num(&s[2..]) <= 59;
122 let date = |s: &str| digits(s) && s.len() == 8 && month(&s[4..6]) && day(&s[6..8]);
123 let zone =
124 |s: &str| s.is_empty() || (s.len() == 3 && matches!(&s[..1], "+" | "-") && digits(&s[1..]));
125 if !value.is_ascii() {
127 return fits_date_format("", format);
128 }
129 let fits = match format {
130 "102" => date(value),
131 "104" => {
132 digits(value)
133 && value.len() == 8
134 && month(&value[..2])
135 && week(&value[2..4])
136 && month(&value[4..6])
137 && week(&value[6..])
138 }
139 "106" => digits(value) && value.len() == 4 && month(&value[..2]) && day(&value[2..]),
140 "203" => value.len() == 12 && date(&value[..8]) && digits(&value[8..]) && time(&value[8..]),
141 "303" => {
142 value.len() >= 12
143 && date(&value[..8])
144 && digits(&value[8..12])
145 && time(&value[8..12])
146 && zone(&value[12..])
147 }
148 "304" => {
149 value.len() >= 14
150 && date(&value[..8])
151 && digits(&value[8..14])
152 && time(&value[8..12])
153 && num(&value[12..14]) <= 59
154 && zone(&value[14..])
155 }
156 "401" => digits(value) && value.len() == 4 && time(value),
157 "501" => digits(value) && value.len() == 8 && time(&value[..4]) && time(&value[4..]),
158 "602" => digits(value) && value.len() == 4,
159 "610" => digits(value) && value.len() == 6 && month(&value[4..]),
160 "802" | "803" | "804" => digits(value),
161 "Z01" => {
165 let n = value.len().saturating_sub(2);
166 (1..=2).contains(&n)
167 && digits(&value[..n])
168 && matches!(&value[n..n + 1], "T" | "W" | "M")
169 && matches!(&value[n + 1..], "M" | "Q" | "H" | "J" | "T" | "R")
170 }
171 _ => return None,
172 };
173 Some(fits)
174}
175
176pub fn validate_hhmm_equals(dtm_value: &str, expected_hhmm: &str) -> ConditionResult {
185 if dtm_value.len() < 12 {
186 return ConditionResult::False;
188 }
189 ConditionResult::from(&dtm_value[8..12] == expected_hhmm)
190}
191
192pub fn validate_hhmm_range(dtm_value: &str, min: &str, max: &str) -> ConditionResult {
196 if dtm_value.len() < 12 {
197 return ConditionResult::False;
198 }
199 let hhmm = &dtm_value[8..12];
200 ConditionResult::from(hhmm >= min && hhmm <= max)
201}
202
203pub fn validate_mmddhhmm_equals(dtm_value: &str, expected: &str) -> ConditionResult {
209 if dtm_value.len() < 12 {
210 return ConditionResult::False;
211 }
212 ConditionResult::from(&dtm_value[4..12] == expected)
213}
214
215pub fn validate_timezone_utc(dtm_value: &str) -> ConditionResult {
226 if dtm_value.len() < 15 || !dtm_value.is_char_boundary(dtm_value.len() - 3) {
227 return ConditionResult::False;
229 }
230 ConditionResult::from(&dtm_value[dtm_value.len() - 3..] == "+00")
231}
232
233pub fn validate_email(value: &str) -> ConditionResult {
239 if value.is_empty() {
240 return ConditionResult::Unknown;
241 }
242 ConditionResult::from(value.contains('@') && value.contains('.'))
243}
244
245pub fn validate_phone(value: &str) -> ConditionResult {
250 if value.is_empty() {
251 return ConditionResult::Unknown;
252 }
253 if !value.starts_with('+') || value.len() < 2 {
254 return ConditionResult::from(false);
255 }
256 ConditionResult::from(value[1..].chars().all(|c| c.is_ascii_digit()))
257}
258
259pub fn validate_malo_id(value: &str) -> ConditionResult {
273 if value.len() != 11 {
274 return ConditionResult::from(false);
275 }
276 if !value.chars().all(|c| c.is_ascii_digit()) {
277 return ConditionResult::from(false);
278 }
279 let digits: Vec<u32> = value.chars().filter_map(|c| c.to_digit(10)).collect();
280 let check = digits[10];
281 let mut sum = 0u32;
282 for (i, &d) in digits[..10].iter().enumerate() {
283 if (i + 1) % 2 == 0 {
285 sum += 2 * d;
286 } else {
287 sum += d;
288 }
289 }
290 let expected = (10 - (sum % 10)) % 10;
291 ConditionResult::from(check == expected)
292}
293
294pub fn validate_tr_id(value: &str) -> ConditionResult {
296 if value.is_empty() {
297 return ConditionResult::Unknown;
298 }
299 ConditionResult::from(value.len() <= 35 && value.chars().all(|c| c.is_ascii_alphanumeric()))
300}
301
302fn validate_prefixed_location_id(value: &str, prefix: char) -> ConditionResult {
305 let mut chars = value.chars();
306 ConditionResult::from(
307 value.len() == 11
308 && chars.next() == Some(prefix)
309 && chars.all(|c| c.is_ascii_digit() || c.is_ascii_uppercase()),
310 )
311}
312
313pub fn validate_sr_id(value: &str) -> ConditionResult {
316 validate_prefixed_location_id(value, 'C')
317}
318
319pub fn validate_technische_ressource_id(value: &str) -> ConditionResult {
323 validate_prefixed_location_id(value, 'D')
324}
325
326pub fn validate_nelo_id(value: &str) -> ConditionResult {
328 validate_prefixed_location_id(value, 'E')
329}
330
331pub fn parse_obis(value: &str) -> Option<(u32, u32)> {
350 let value = value.trim();
351 let (_ab, cde) = value.split_once(':')?;
353 let cde = cde.split('*').next()?;
355 let mut parts = cde.split('.');
356 let c = parts.next()?.parse::<u32>().ok()?;
357 let d = parts.next()?.parse::<u32>().ok()?;
358 Some((c, d))
359}
360
361pub fn obis_tariff(value: &str) -> Option<u32> {
365 let (_ab, cde) = value.trim().split_once(':')?;
366 let cde = cde.split('*').next()?;
367 let mut parts = cde.split('.');
368 parts.next()?.parse::<u32>().ok()?;
369 parts.next()?.parse::<u32>().ok()?;
370 parts.next()?.parse::<u32>().ok()
371}
372
373pub fn is_obis_wirkarbeit_kumuliert(value: &str) -> bool {
379 matches!(parse_obis(value), Some((1 | 2, 8 | 9)))
380}
381
382pub fn is_obis_wirkarbeit_quarter_hour(value: &str) -> bool {
389 matches!(parse_obis(value), Some((1 | 2, 29)))
390}
391
392pub fn is_obis_energiemenge(value: &str) -> bool {
396 value.trim().starts_with("1-") && matches!(parse_obis(value), Some((_, 9 | 29)))
397}
398
399pub fn is_obis_zaehlerstand(value: &str) -> bool {
401 value.trim().starts_with("1-") && matches!(parse_obis(value), Some((_, 8)))
402}
403
404pub fn is_obis_wirkleistung_maximum(value: &str) -> bool {
408 value.trim().starts_with("1-") && matches!(parse_obis(value), Some((1 | 2, 6)))
409}
410
411pub fn is_obis_blindarbeit(value: &str) -> bool {
414 value.trim().starts_with("1-") && matches!(parse_obis(value), Some((3..=8, 8 | 9 | 29)))
415}
416
417pub fn obis_has_zaehlzeit(value: &str) -> bool {
420 obis_tariff(value).is_some_and(|e| (1..=62).contains(&e))
421}
422
423pub fn is_energy_register_obis(value: &str) -> bool {
429 value.trim().starts_with("1-") && matches!(parse_obis(value), Some((1..=8, 8)))
430}
431
432pub fn validate_x509_cert_body(value: &str) -> ConditionResult {
451 let trimmed = value.trim();
452 if trimmed.is_empty() {
453 return ConditionResult::Unknown;
454 }
455 let body = trimmed
458 .trim_start_matches("-----BEGIN CERTIFICATE-----")
459 .trim_end_matches("-----END CERTIFICATE-----")
460 .trim();
461 let chars_ok = body.chars().all(|c| {
462 c.is_ascii_alphanumeric() || matches!(c, '+' | '/' | '=' | '\n' | '\r' | '\t' | ' ')
463 });
464 let len_ok = body.chars().filter(|c| !c.is_whitespace()).count() >= 100;
467 ConditionResult::from(chars_ok && len_ok)
468}
469
470pub fn validate_zahlpunkt(value: &str) -> ConditionResult {
472 if value.len() != 33 {
473 return ConditionResult::from(false);
474 }
475 ConditionResult::from(value.chars().all(|c| c.is_ascii_alphanumeric()))
476}
477
478pub fn validate_malo_or_zahlpunkt(value: &str) -> ConditionResult {
480 if value.len() == 11 && validate_malo_id(value).is_true() {
481 return ConditionResult::True;
482 }
483 if value.len() == 33 && validate_zahlpunkt(value).is_true() {
484 return ConditionResult::True;
485 }
486 ConditionResult::False
487}
488
489pub fn validate_artikel_pattern(value: &str, segment_lengths: &[usize]) -> ConditionResult {
498 if value.is_empty() {
499 return ConditionResult::Unknown;
500 }
501 let parts: Vec<&str> = value.split('-').collect();
502 if parts.len() != segment_lengths.len() {
503 return ConditionResult::from(false);
504 }
505 let valid = parts
506 .iter()
507 .zip(segment_lengths.iter())
508 .all(|(part, &expected_len)| {
509 part.len() == expected_len && part.chars().all(|c| c.is_ascii_digit())
510 });
511 ConditionResult::from(valid)
512}
513
514pub fn validate_exact_length(value: &str, expected: usize) -> ConditionResult {
518 if value.is_empty() {
519 return ConditionResult::Unknown;
520 }
521 ConditionResult::from(value.len() == expected)
522}
523
524pub fn validate_max_length(value: &str, max: usize) -> ConditionResult {
526 if value.is_empty() {
527 return ConditionResult::Unknown;
528 }
529 ConditionResult::from(value.len() <= max)
530}
531
532pub fn validate_all_digits(value: &str) -> ConditionResult {
534 if value.is_empty() {
535 return ConditionResult::Unknown;
536 }
537 ConditionResult::from(value.chars().all(|c| c.is_ascii_digit()))
538}
539
540pub fn utc_now_ccyymmddhhmm() -> String {
542 chrono::Utc::now().format("%Y%m%d%H%M").to_string()
543}
544
545pub fn german_local_time(value: &str) -> Option<chrono::NaiveDateTime> {
552 use chrono::{Datelike, Duration, NaiveDate, NaiveDateTime, Weekday};
553 let value = value.replace('?', "");
554 let stamp = NaiveDateTime::parse_from_str(value.get(..12)?, "%Y%m%d%H%M").ok()?;
555 let offset: i64 = match value.get(12..) {
556 None | Some("") => 0,
557 Some(zone) => zone.parse().ok()?,
558 };
559 let utc = stamp - Duration::hours(offset);
560 let switch = |month: u32| {
561 let mut day = NaiveDate::from_ymd_opt(utc.year(), month, 31)?;
562 while day.weekday() != Weekday::Sun {
563 day = day.pred_opt()?;
564 }
565 day.and_hms_opt(1, 0, 0)
566 };
567 let summer = utc >= switch(3)? && utc < switch(10)?;
568 Some(utc + Duration::hours(if summer { 2 } else { 1 }))
569}
570
571#[cfg(test)]
572mod tests {
573
574 #[test]
575 fn date_values_follow_their_format_code() {
576 assert_eq!(fits_date_format("20250401", "102"), Some(true));
577 assert_eq!(fits_date_format("20250401", "104"), Some(false));
578 assert_eq!(fits_date_format("04010402", "104"), Some(true));
579 assert_eq!(fits_date_format("20250401", "106"), Some(false));
580 assert_eq!(fits_date_format("0401", "106"), Some(true));
581 assert_eq!(fits_date_format("202503312200+00", "303"), Some(true));
582 assert_eq!(fits_date_format("202503312200", "303"), Some(true));
583 assert_eq!(fits_date_format("20250331", "303"), Some(false));
584 assert_eq!(fits_date_format("20250331220000+00", "304"), Some(true));
585 assert_eq!(fits_date_format("2025", "602"), Some(true));
586 assert_eq!(fits_date_format("202513", "610"), Some(false));
587 assert_eq!(fits_date_format("12", "802"), Some(true));
588 assert_eq!(fits_date_format("06002200", "501"), Some(true));
589 assert_eq!(fits_date_format("03MM", "Z01"), Some(true));
590 assert_eq!(fits_date_format("20250401", "Z01"), Some(false));
591 assert_eq!(fits_date_format("x", "999"), None);
592 assert_eq!(fits_date_format("ä", "999"), None);
593 assert_eq!(fits_date_format("2025040ä", "102"), Some(false));
594 }
595
596 #[test]
599 fn obis_classes_follow_the_code_list() {
600 use super::*;
601 assert!(is_obis_zaehlerstand("1-1:1.8.0") && !is_obis_energiemenge("1-1:1.8.0"));
603 assert!(is_obis_energiemenge("1-1:1.9.0") && !is_obis_zaehlerstand("1-1:1.9.0"));
604 assert!(
605 is_obis_energiemenge("1-1:1.29.0") && is_obis_wirkarbeit_quarter_hour("1-1:1.29.0")
606 );
607 assert!(!is_obis_wirkarbeit_quarter_hour("1-1:1.9.0"));
608 assert!(is_obis_wirkleistung_maximum("1-1:2.6.0"));
610 assert!(!is_obis_wirkleistung_maximum("1-1:3.6.0"));
611 assert!(is_obis_blindarbeit("1-1:3.9.0") && is_obis_blindarbeit("1-1:4.29.0"));
612 assert!(!is_obis_blindarbeit("1-1:1.8.0"));
613 assert!(obis_has_zaehlzeit("1-1:1.9.1") && obis_has_zaehlzeit("1-1:1.8.62"));
615 assert!(!obis_has_zaehlzeit("1-1:1.9.0") && !obis_has_zaehlzeit("1-1:1.8.63"));
616 assert!(!is_obis_energiemenge("7-0:3.9.0"));
618 }
619
620 #[test]
621 fn the_tariff_is_the_e_of_c_d_e() {
622 use super::obis_tariff;
623 assert_eq!(obis_tariff("1-1:1.8.1"), Some(1));
624 assert_eq!(obis_tariff("1-0:2.8.62*255"), Some(62));
625 assert_eq!(obis_tariff("1-0:1.8.0"), Some(0));
626 assert_eq!(obis_tariff("1-0:1.8"), None);
627 assert_eq!(obis_tariff("x"), None);
628 }
629
630 #[test]
631 fn energy_registers_are_c_8_e_with_c_one_to_eight() {
632 use super::is_energy_register_obis;
633 for obis in [
634 "1-1:1.8.0",
635 "1-0:2.8.1",
636 "1-1:8.8.0",
637 "1-1:1.8.63",
638 "1-65:3.8.2",
639 ] {
640 assert!(is_energy_register_obis(obis), "{obis}");
641 }
642 for obis in [
643 "1-1:1.29.0",
644 "1-1:2.29.0",
645 "1-1:9.8.0",
646 "7-0:3.0.0",
647 "1-1:1.9.0",
648 "x",
649 ] {
650 assert!(!is_energy_register_obis(obis), "{obis}");
651 }
652 }
653
654 use super::*;
655
656 #[test]
659 fn test_max_decimal_places() {
660 assert_eq!(
661 validate_max_decimal_places("123.45", 2),
662 ConditionResult::True
663 );
664 assert_eq!(
665 validate_max_decimal_places("123.456", 2),
666 ConditionResult::False
667 );
668 assert_eq!(validate_max_decimal_places("123", 2), ConditionResult::True);
669 assert_eq!(validate_max_decimal_places("0.1", 3), ConditionResult::True);
670 assert_eq!(validate_max_decimal_places("", 2), ConditionResult::Unknown);
671 }
672
673 #[test]
674 fn test_no_decimal_places() {
675 assert_eq!(validate_max_decimal_places("100", 0), ConditionResult::True);
676 assert_eq!(
677 validate_max_decimal_places("100.5", 0),
678 ConditionResult::False
679 );
680 }
681
682 #[test]
683 fn test_max_integer_digits() {
684 assert_eq!(
685 validate_max_integer_digits("1234", 4),
686 ConditionResult::True
687 );
688 assert_eq!(
689 validate_max_integer_digits("12345", 4),
690 ConditionResult::False
691 );
692 assert_eq!(
693 validate_max_integer_digits("-123.45", 4),
694 ConditionResult::True
695 );
696 assert_eq!(validate_max_integer_digits("", 4), ConditionResult::Unknown);
697 }
698
699 #[test]
702 fn test_validate_numeric() {
703 assert_eq!(validate_numeric("5.0", ">=", 0.0), ConditionResult::True);
704 assert_eq!(validate_numeric("-1.0", ">=", 0.0), ConditionResult::False);
705 assert_eq!(validate_numeric("1", "==", 1.0), ConditionResult::True);
706 assert_eq!(validate_numeric("2", "==", 1.0), ConditionResult::False);
707 assert_eq!(validate_numeric("0", ">", 0.0), ConditionResult::False);
708 assert_eq!(validate_numeric("1", ">", 0.0), ConditionResult::True);
709 assert_eq!(validate_numeric("abc", ">=", 0.0), ConditionResult::False);
712 assert_eq!(
713 validate_numeric("O5D2DXBG1A", ">", 0.0),
714 ConditionResult::False
715 );
716 assert_eq!(validate_numeric("NaN", ">", 0.0), ConditionResult::False);
717 assert_eq!(validate_numeric("inf", ">", 0.0), ConditionResult::False);
718 assert_eq!(validate_numeric("1e3", ">", 0.0), ConditionResult::False);
719 assert_eq!(validate_numeric("0,5", ">", 0.0), ConditionResult::True);
721 assert_eq!(validate_numeric("-2", "<=", 0.0), ConditionResult::True);
722 assert_eq!(validate_numeric("", ">", 0.0), ConditionResult::Unknown);
723 }
724
725 #[test]
728 fn test_hhmm_equals() {
729 assert_eq!(
730 validate_hhmm_equals("202601012200+00", "2200"),
731 ConditionResult::True
732 );
733 assert_eq!(
734 validate_hhmm_equals("202601012300+00", "2200"),
735 ConditionResult::False
736 );
737 assert_eq!(
738 validate_hhmm_equals("short", "2200"),
739 ConditionResult::False
740 );
741 }
742
743 #[test]
744 fn test_hhmm_range() {
745 assert_eq!(
746 validate_hhmm_range("202601011530+00", "0000", "2359"),
747 ConditionResult::True
748 );
749 assert_eq!(
750 validate_hhmm_range("202601010000+00", "0000", "2359"),
751 ConditionResult::True
752 );
753 assert_eq!(
754 validate_hhmm_range("202601012359+00", "0000", "2359"),
755 ConditionResult::True
756 );
757 }
758
759 #[test]
760 fn test_mmddhhmm_equals() {
761 assert_eq!(
762 validate_mmddhhmm_equals("202612312300+00", "12312300"),
763 ConditionResult::True
764 );
765 assert_eq!(
766 validate_mmddhhmm_equals("202601012200+00", "12312300"),
767 ConditionResult::False
768 );
769 }
770
771 #[test]
772 fn test_timezone_utc() {
773 assert_eq!(
774 validate_timezone_utc("202601012200+00"),
775 ConditionResult::True
776 );
777 assert_eq!(
778 validate_timezone_utc("202601012200+01"),
779 ConditionResult::False
780 );
781 assert_eq!(
782 validate_timezone_utc("202601012200"),
783 ConditionResult::False
784 );
785 assert_eq!(
787 validate_timezone_utc("20260101220000+00"),
788 ConditionResult::True
789 );
790 assert_eq!(
791 validate_timezone_utc("20260101220000+01"),
792 ConditionResult::False
793 );
794 }
795
796 #[test]
799 fn test_email() {
800 assert_eq!(validate_email("user@example.com"), ConditionResult::True);
801 assert_eq!(validate_email("nope"), ConditionResult::False);
802 assert_eq!(validate_email("has@but-no-dot"), ConditionResult::False);
803 assert_eq!(validate_email(""), ConditionResult::Unknown);
804 }
805
806 #[test]
807 fn test_phone() {
808 assert_eq!(validate_phone("+4930123456"), ConditionResult::True);
809 assert_eq!(validate_phone("030123456"), ConditionResult::False);
810 assert_eq!(validate_phone("+"), ConditionResult::False);
811 assert_eq!(validate_phone("+49 30 123"), ConditionResult::False); assert_eq!(validate_phone(""), ConditionResult::Unknown);
813 }
814
815 #[test]
818 fn test_malo_id() {
819 assert_eq!(validate_malo_id("51238696781"), ConditionResult::True);
823 assert_eq!(validate_malo_id("41373559241"), ConditionResult::True);
824 assert_eq!(validate_malo_id("56789012345"), ConditionResult::True);
825 assert_eq!(validate_malo_id("52935155442"), ConditionResult::True);
826
827 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); }
833
834 #[test]
835 fn test_zahlpunkt() {
836 let valid = "DE0001234567890123456789012345678";
837 assert_eq!(valid.len(), 33);
838 assert_eq!(validate_zahlpunkt(valid), ConditionResult::True);
839 assert_eq!(validate_zahlpunkt("tooshort"), ConditionResult::False);
840 }
841
842 #[test]
845 fn test_artikel_pattern() {
846 assert_eq!(
847 validate_artikel_pattern("1-23-4", &[1, 2, 1]),
848 ConditionResult::True
849 );
850 assert_eq!(
851 validate_artikel_pattern("1-23-4-567", &[1, 2, 1, 3]),
852 ConditionResult::True
853 );
854 assert_eq!(
855 validate_artikel_pattern("1-23-4-56", &[1, 2, 1, 3]),
856 ConditionResult::False
857 );
858 assert_eq!(
859 validate_artikel_pattern("1-AB-4", &[1, 2, 1]),
860 ConditionResult::False
861 );
862 assert_eq!(
863 validate_artikel_pattern("", &[1, 2, 1]),
864 ConditionResult::Unknown
865 );
866 }
867
868 #[test]
871 fn test_tr_id() {
872 assert_eq!(validate_tr_id("ABC123"), ConditionResult::True);
873 assert_eq!(validate_tr_id("A"), ConditionResult::True);
874 assert_eq!(validate_tr_id(&"A".repeat(35)), ConditionResult::True);
875 assert_eq!(validate_tr_id(&"A".repeat(36)), ConditionResult::False);
876 assert_eq!(validate_tr_id("has spaces"), ConditionResult::False);
877 assert_eq!(validate_tr_id("has-dash"), ConditionResult::False);
878 assert_eq!(validate_tr_id(""), ConditionResult::Unknown);
879 }
880
881 #[test]
882 fn test_parse_obis() {
883 assert_eq!(parse_obis("1-1:1.8.0"), Some((1, 8)));
884 assert_eq!(parse_obis("1-1:2.29.0"), Some((2, 29)));
885 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);
888 assert_eq!(parse_obis("1-1:abc.8.0"), None);
889 assert_eq!(parse_obis(""), None);
890 }
891
892 #[test]
893 fn test_obis_wirkarbeit_kumuliert() {
894 assert!(is_obis_wirkarbeit_kumuliert("1-1:1.8.0"));
896 assert!(is_obis_wirkarbeit_kumuliert("1-1:2.8.0"));
897 assert!(is_obis_wirkarbeit_kumuliert("1-1:1.9.0"));
898 assert!(is_obis_wirkarbeit_kumuliert("1-1:2.9.0"));
899 assert!(is_obis_wirkarbeit_kumuliert("1-0:1.8.0*255"));
900 assert!(!is_obis_wirkarbeit_kumuliert("1-1:3.8.0"));
902 assert!(!is_obis_wirkarbeit_kumuliert("1-1:4.8.0"));
903 assert!(!is_obis_wirkarbeit_kumuliert("1-1:1.29.0"));
905 assert!(!is_obis_wirkarbeit_kumuliert("1-1:1.5.0"));
907 }
908
909 #[test]
910 fn test_obis_wirkarbeit_quarter_hour() {
911 assert!(is_obis_wirkarbeit_quarter_hour("1-1:1.29.0"));
913 assert!(is_obis_wirkarbeit_quarter_hour("1-1:2.29.0"));
914 assert!(is_obis_wirkarbeit_quarter_hour("1-0:1.29.0*255"));
915 assert!(!is_obis_wirkarbeit_quarter_hour("1-1:1.8.0"));
917 assert!(!is_obis_wirkarbeit_quarter_hour("1-1:1.9.0"));
918 assert!(!is_obis_wirkarbeit_quarter_hour("1-1:3.29.0"));
920 }
921
922 #[test]
923 fn test_x509_cert_body() {
924 let body = "A".repeat(600);
926 assert_eq!(validate_x509_cert_body(&body), ConditionResult::True);
927
928 let pem_body = "A".repeat(64) + "\n" + &"B".repeat(64) + "\n" + &"C".repeat(64);
930 assert_eq!(validate_x509_cert_body(&pem_body), ConditionResult::True);
931
932 let armored = format!(
934 "-----BEGIN CERTIFICATE-----\n{}\n-----END CERTIFICATE-----",
935 "A".repeat(200)
936 );
937 assert_eq!(validate_x509_cert_body(&armored), ConditionResult::True);
938
939 let with_punct = "AB+/==".to_string() + &"A".repeat(200);
941 assert_eq!(validate_x509_cert_body(&with_punct), ConditionResult::True);
942
943 assert_eq!(validate_x509_cert_body("QUJD"), ConditionResult::False);
945
946 let invalid_chars = "A".repeat(200) + "!@#";
948 assert_eq!(
949 validate_x509_cert_body(&invalid_chars),
950 ConditionResult::False
951 );
952
953 assert_eq!(validate_x509_cert_body(""), ConditionResult::Unknown);
955 assert_eq!(validate_x509_cert_body(" \n "), ConditionResult::Unknown);
956 }
957
958 #[test]
962 fn test_sr_id() {
963 for real in ["C816417ST77", "CZZ6M5DGSS5", "COPFWS2EX96"] {
964 assert_eq!(validate_sr_id(real), ConditionResult::True, "{real}");
965 }
966 assert_eq!(
967 validate_sr_id("51238696781"),
968 ConditionResult::False,
969 "a MaLo-ID"
970 );
971 assert_eq!(validate_sr_id("C12345"), ConditionResult::False);
972 assert_eq!(validate_sr_id(""), ConditionResult::False);
973 }
974
975 #[test]
979 fn test_nelo_id() {
980 for real in ["E1688117482", "EU5UQAWW8D0", "EEW7TM7V906"] {
981 assert_eq!(validate_nelo_id(real), ConditionResult::True, "{real}");
982 }
983 assert_eq!(
984 validate_nelo_id("51238696781"),
985 ConditionResult::False,
986 "a MaLo-ID"
987 );
988 assert_eq!(validate_nelo_id("E123"), ConditionResult::False);
989 assert_eq!(validate_nelo_id("e1688117482"), ConditionResult::False);
990 }
991
992 #[test]
995 fn test_exact_length() {
996 assert_eq!(
997 validate_exact_length("1234567890123456", 16),
998 ConditionResult::True
999 );
1000 assert_eq!(validate_exact_length("123", 16), ConditionResult::False);
1001 assert_eq!(validate_exact_length("", 16), ConditionResult::Unknown);
1002 }
1003
1004 #[test]
1005 fn test_all_digits() {
1006 assert_eq!(validate_all_digits("12345"), ConditionResult::True);
1007 assert_eq!(validate_all_digits("123a5"), ConditionResult::False);
1008 assert_eq!(validate_all_digits(""), ConditionResult::Unknown);
1009 }
1010}