use super::evaluator::ConditionResult;
pub use super::timezone::{dst_transitions_between, is_mesz_utc, is_mez_utc, parse_dtm303};
pub use super::werktag::{
compute_easter, is_german_holiday, is_werktag, parse_ccyymmdd_prefix, werktage_between,
};
pub fn validate_max_decimal_places(value: &str, max: usize) -> ConditionResult {
if value.is_empty() {
return ConditionResult::Unknown;
}
let decimal_places = match value.find('.') {
Some(pos) => value.len() - pos - 1,
None => 0,
};
ConditionResult::from(decimal_places <= max)
}
pub fn validate_max_integer_digits(value: &str, max: usize) -> ConditionResult {
if value.is_empty() {
return ConditionResult::Unknown;
}
let s = value.strip_prefix('-').unwrap_or(value);
let integer_part = match s.find('.') {
Some(pos) => &s[..pos],
None => s,
};
ConditionResult::from(integer_part.len() <= max)
}
pub fn validate_numeric(value: &str, op: &str, threshold: f64) -> ConditionResult {
let parsed = match value.parse::<f64>() {
Ok(v) => v,
Err(_) => return ConditionResult::Unknown,
};
let result = match op {
"==" => (parsed - threshold).abs() < f64::EPSILON,
"!=" => (parsed - threshold).abs() >= f64::EPSILON,
">" => parsed > threshold,
">=" => parsed >= threshold,
"<" => parsed < threshold,
"<=" => parsed <= threshold,
_ => return ConditionResult::Unknown,
};
ConditionResult::from(result)
}
pub fn validate_hhmm_equals(dtm_value: &str, expected_hhmm: &str) -> ConditionResult {
if dtm_value.len() < 12 {
return ConditionResult::False;
}
ConditionResult::from(&dtm_value[8..12] == expected_hhmm)
}
pub fn validate_hhmm_range(dtm_value: &str, min: &str, max: &str) -> ConditionResult {
if dtm_value.len() < 12 {
return ConditionResult::False;
}
let hhmm = &dtm_value[8..12];
ConditionResult::from(hhmm >= min && hhmm <= max)
}
pub fn validate_mmddhhmm_equals(dtm_value: &str, expected: &str) -> ConditionResult {
if dtm_value.len() < 12 {
return ConditionResult::False;
}
ConditionResult::from(&dtm_value[4..12] == expected)
}
pub fn validate_timezone_utc(dtm_value: &str) -> ConditionResult {
if dtm_value.len() < 15 {
return ConditionResult::False;
}
ConditionResult::from(&dtm_value[12..] == "+00")
}
pub fn validate_email(value: &str) -> ConditionResult {
if value.is_empty() {
return ConditionResult::Unknown;
}
ConditionResult::from(value.contains('@') && value.contains('.'))
}
pub fn validate_phone(value: &str) -> ConditionResult {
if value.is_empty() {
return ConditionResult::Unknown;
}
if !value.starts_with('+') || value.len() < 2 {
return ConditionResult::from(false);
}
ConditionResult::from(value[1..].chars().all(|c| c.is_ascii_digit()))
}
pub fn validate_malo_id(value: &str) -> ConditionResult {
if value.len() != 11 {
return ConditionResult::from(false);
}
if !value.chars().all(|c| c.is_ascii_digit()) {
return ConditionResult::from(false);
}
let digits: Vec<u32> = value.chars().filter_map(|c| c.to_digit(10)).collect();
let check = digits[10];
let mut sum = 0u32;
for (i, &d) in digits[..10].iter().enumerate() {
if (i + 1) % 2 == 0 {
sum += 2 * d;
} else {
sum += d;
}
}
let expected = (10 - (sum % 10)) % 10;
ConditionResult::from(check == expected)
}
pub fn validate_tr_id(value: &str) -> ConditionResult {
if value.is_empty() {
return ConditionResult::Unknown;
}
ConditionResult::from(value.len() <= 35 && value.chars().all(|c| c.is_ascii_alphanumeric()))
}
pub fn validate_sr_id(value: &str) -> ConditionResult {
validate_malo_id(value)
}
pub fn parse_obis(value: &str) -> Option<(u32, u32)> {
let value = value.trim();
let (_ab, cde) = value.split_once(':')?;
let cde = cde.split('*').next()?;
let mut parts = cde.split('.');
let c = parts.next()?.parse::<u32>().ok()?;
let d = parts.next()?.parse::<u32>().ok()?;
Some((c, d))
}
pub fn is_obis_wirkarbeit_kumuliert(value: &str) -> bool {
matches!(parse_obis(value), Some((1 | 2, 8 | 9)))
}
pub fn is_obis_wirkarbeit_quarter_hour(value: &str) -> bool {
matches!(parse_obis(value), Some((1 | 2, 29)))
}
pub fn validate_x509_cert_body(value: &str) -> ConditionResult {
let trimmed = value.trim();
if trimmed.is_empty() {
return ConditionResult::Unknown;
}
let body = trimmed
.trim_start_matches("-----BEGIN CERTIFICATE-----")
.trim_end_matches("-----END CERTIFICATE-----")
.trim();
let chars_ok = body.chars().all(|c| {
c.is_ascii_alphanumeric()
|| matches!(c, '+' | '/' | '=' | '\n' | '\r' | '\t' | ' ')
});
let len_ok = body.chars().filter(|c| !c.is_whitespace()).count() >= 100;
ConditionResult::from(chars_ok && len_ok)
}
pub fn validate_zahlpunkt(value: &str) -> ConditionResult {
if value.len() != 33 {
return ConditionResult::from(false);
}
ConditionResult::from(value.chars().all(|c| c.is_ascii_alphanumeric()))
}
pub fn validate_malo_or_zahlpunkt(value: &str) -> ConditionResult {
if value.len() == 11 && validate_malo_id(value).is_true() {
return ConditionResult::True;
}
if value.len() == 33 && validate_zahlpunkt(value).is_true() {
return ConditionResult::True;
}
ConditionResult::False
}
pub fn validate_artikel_pattern(value: &str, segment_lengths: &[usize]) -> ConditionResult {
if value.is_empty() {
return ConditionResult::Unknown;
}
let parts: Vec<&str> = value.split('-').collect();
if parts.len() != segment_lengths.len() {
return ConditionResult::from(false);
}
let valid = parts
.iter()
.zip(segment_lengths.iter())
.all(|(part, &expected_len)| {
part.len() == expected_len && part.chars().all(|c| c.is_ascii_digit())
});
ConditionResult::from(valid)
}
pub fn validate_exact_length(value: &str, expected: usize) -> ConditionResult {
if value.is_empty() {
return ConditionResult::Unknown;
}
ConditionResult::from(value.len() == expected)
}
pub fn validate_max_length(value: &str, max: usize) -> ConditionResult {
if value.is_empty() {
return ConditionResult::Unknown;
}
ConditionResult::from(value.len() <= max)
}
pub fn validate_all_digits(value: &str) -> ConditionResult {
if value.is_empty() {
return ConditionResult::Unknown;
}
ConditionResult::from(value.chars().all(|c| c.is_ascii_digit()))
}
pub fn utc_now_ccyymmddhhmm() -> String {
chrono::Utc::now().format("%Y%m%d%H%M").to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_max_decimal_places() {
assert_eq!(
validate_max_decimal_places("123.45", 2),
ConditionResult::True
);
assert_eq!(
validate_max_decimal_places("123.456", 2),
ConditionResult::False
);
assert_eq!(validate_max_decimal_places("123", 2), ConditionResult::True);
assert_eq!(validate_max_decimal_places("0.1", 3), ConditionResult::True);
assert_eq!(validate_max_decimal_places("", 2), ConditionResult::Unknown);
}
#[test]
fn test_no_decimal_places() {
assert_eq!(validate_max_decimal_places("100", 0), ConditionResult::True);
assert_eq!(
validate_max_decimal_places("100.5", 0),
ConditionResult::False
);
}
#[test]
fn test_max_integer_digits() {
assert_eq!(
validate_max_integer_digits("1234", 4),
ConditionResult::True
);
assert_eq!(
validate_max_integer_digits("12345", 4),
ConditionResult::False
);
assert_eq!(
validate_max_integer_digits("-123.45", 4),
ConditionResult::True
);
assert_eq!(validate_max_integer_digits("", 4), ConditionResult::Unknown);
}
#[test]
fn test_validate_numeric() {
assert_eq!(validate_numeric("5.0", ">=", 0.0), ConditionResult::True);
assert_eq!(validate_numeric("-1.0", ">=", 0.0), ConditionResult::False);
assert_eq!(validate_numeric("1", "==", 1.0), ConditionResult::True);
assert_eq!(validate_numeric("2", "==", 1.0), ConditionResult::False);
assert_eq!(validate_numeric("0", ">", 0.0), ConditionResult::False);
assert_eq!(validate_numeric("1", ">", 0.0), ConditionResult::True);
assert_eq!(validate_numeric("abc", ">=", 0.0), ConditionResult::Unknown);
}
#[test]
fn test_hhmm_equals() {
assert_eq!(
validate_hhmm_equals("202601012200+00", "2200"),
ConditionResult::True
);
assert_eq!(
validate_hhmm_equals("202601012300+00", "2200"),
ConditionResult::False
);
assert_eq!(
validate_hhmm_equals("short", "2200"),
ConditionResult::False
);
}
#[test]
fn test_hhmm_range() {
assert_eq!(
validate_hhmm_range("202601011530+00", "0000", "2359"),
ConditionResult::True
);
assert_eq!(
validate_hhmm_range("202601010000+00", "0000", "2359"),
ConditionResult::True
);
assert_eq!(
validate_hhmm_range("202601012359+00", "0000", "2359"),
ConditionResult::True
);
}
#[test]
fn test_mmddhhmm_equals() {
assert_eq!(
validate_mmddhhmm_equals("202612312300+00", "12312300"),
ConditionResult::True
);
assert_eq!(
validate_mmddhhmm_equals("202601012200+00", "12312300"),
ConditionResult::False
);
}
#[test]
fn test_timezone_utc() {
assert_eq!(
validate_timezone_utc("202601012200+00"),
ConditionResult::True
);
assert_eq!(
validate_timezone_utc("202601012200+01"),
ConditionResult::False
);
assert_eq!(
validate_timezone_utc("202601012200"),
ConditionResult::False
);
}
#[test]
fn test_email() {
assert_eq!(validate_email("user@example.com"), ConditionResult::True);
assert_eq!(validate_email("nope"), ConditionResult::False);
assert_eq!(validate_email("has@but-no-dot"), ConditionResult::False);
assert_eq!(validate_email(""), ConditionResult::Unknown);
}
#[test]
fn test_phone() {
assert_eq!(validate_phone("+4930123456"), ConditionResult::True);
assert_eq!(validate_phone("030123456"), ConditionResult::False);
assert_eq!(validate_phone("+"), ConditionResult::False);
assert_eq!(validate_phone("+49 30 123"), ConditionResult::False); assert_eq!(validate_phone(""), ConditionResult::Unknown);
}
#[test]
fn test_malo_id() {
assert_eq!(validate_malo_id("51238696781"), ConditionResult::True);
assert_eq!(validate_malo_id("41373559241"), ConditionResult::True);
assert_eq!(validate_malo_id("56789012345"), ConditionResult::True);
assert_eq!(validate_malo_id("52935155442"), ConditionResult::True);
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); }
#[test]
fn test_zahlpunkt() {
let valid = "DE0001234567890123456789012345678";
assert_eq!(valid.len(), 33);
assert_eq!(validate_zahlpunkt(valid), ConditionResult::True);
assert_eq!(validate_zahlpunkt("tooshort"), ConditionResult::False);
}
#[test]
fn test_artikel_pattern() {
assert_eq!(
validate_artikel_pattern("1-23-4", &[1, 2, 1]),
ConditionResult::True
);
assert_eq!(
validate_artikel_pattern("1-23-4-567", &[1, 2, 1, 3]),
ConditionResult::True
);
assert_eq!(
validate_artikel_pattern("1-23-4-56", &[1, 2, 1, 3]),
ConditionResult::False
);
assert_eq!(
validate_artikel_pattern("1-AB-4", &[1, 2, 1]),
ConditionResult::False
);
assert_eq!(
validate_artikel_pattern("", &[1, 2, 1]),
ConditionResult::Unknown
);
}
#[test]
fn test_tr_id() {
assert_eq!(validate_tr_id("ABC123"), ConditionResult::True);
assert_eq!(validate_tr_id("A"), ConditionResult::True);
assert_eq!(validate_tr_id(&"A".repeat(35)), ConditionResult::True);
assert_eq!(validate_tr_id(&"A".repeat(36)), ConditionResult::False);
assert_eq!(validate_tr_id("has spaces"), ConditionResult::False);
assert_eq!(validate_tr_id("has-dash"), ConditionResult::False);
assert_eq!(validate_tr_id(""), ConditionResult::Unknown);
}
#[test]
fn test_parse_obis() {
assert_eq!(parse_obis("1-1:1.8.0"), Some((1, 8)));
assert_eq!(parse_obis("1-1:2.29.0"), Some((2, 29)));
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);
assert_eq!(parse_obis("1-1:abc.8.0"), None);
assert_eq!(parse_obis(""), None);
}
#[test]
fn test_obis_wirkarbeit_kumuliert() {
assert!(is_obis_wirkarbeit_kumuliert("1-1:1.8.0"));
assert!(is_obis_wirkarbeit_kumuliert("1-1:2.8.0"));
assert!(is_obis_wirkarbeit_kumuliert("1-1:1.9.0"));
assert!(is_obis_wirkarbeit_kumuliert("1-1:2.9.0"));
assert!(is_obis_wirkarbeit_kumuliert("1-0:1.8.0*255"));
assert!(!is_obis_wirkarbeit_kumuliert("1-1:3.8.0"));
assert!(!is_obis_wirkarbeit_kumuliert("1-1:4.8.0"));
assert!(!is_obis_wirkarbeit_kumuliert("1-1:1.29.0"));
assert!(!is_obis_wirkarbeit_kumuliert("1-1:1.5.0"));
}
#[test]
fn test_obis_wirkarbeit_quarter_hour() {
assert!(is_obis_wirkarbeit_quarter_hour("1-1:1.29.0"));
assert!(is_obis_wirkarbeit_quarter_hour("1-1:2.29.0"));
assert!(is_obis_wirkarbeit_quarter_hour("1-0:1.29.0*255"));
assert!(!is_obis_wirkarbeit_quarter_hour("1-1:1.8.0"));
assert!(!is_obis_wirkarbeit_quarter_hour("1-1:1.9.0"));
assert!(!is_obis_wirkarbeit_quarter_hour("1-1:3.29.0"));
}
#[test]
fn test_x509_cert_body() {
let body = "A".repeat(600);
assert_eq!(validate_x509_cert_body(&body), ConditionResult::True);
let pem_body = "A".repeat(64) + "\n" + &"B".repeat(64) + "\n" + &"C".repeat(64);
assert_eq!(validate_x509_cert_body(&pem_body), ConditionResult::True);
let armored = format!(
"-----BEGIN CERTIFICATE-----\n{}\n-----END CERTIFICATE-----",
"A".repeat(200)
);
assert_eq!(validate_x509_cert_body(&armored), ConditionResult::True);
let with_punct = "AB+/==".to_string() + &"A".repeat(200);
assert_eq!(validate_x509_cert_body(&with_punct), ConditionResult::True);
assert_eq!(validate_x509_cert_body("QUJD"), ConditionResult::False);
let invalid_chars = "A".repeat(200) + "!@#";
assert_eq!(
validate_x509_cert_body(&invalid_chars),
ConditionResult::False
);
assert_eq!(validate_x509_cert_body(""), ConditionResult::Unknown);
assert_eq!(validate_x509_cert_body(" \n "), ConditionResult::Unknown);
}
#[test]
fn test_sr_id() {
assert_eq!(validate_sr_id("51238696781"), ConditionResult::True);
assert_eq!(validate_sr_id("41373559240"), ConditionResult::False);
assert_eq!(validate_sr_id("1234567890"), ConditionResult::False);
assert_eq!(validate_sr_id(""), ConditionResult::False);
}
#[test]
fn test_exact_length() {
assert_eq!(
validate_exact_length("1234567890123456", 16),
ConditionResult::True
);
assert_eq!(validate_exact_length("123", 16), ConditionResult::False);
assert_eq!(validate_exact_length("", 16), ConditionResult::Unknown);
}
#[test]
fn test_all_digits() {
assert_eq!(validate_all_digits("12345"), ConditionResult::True);
assert_eq!(validate_all_digits("123a5"), ConditionResult::False);
assert_eq!(validate_all_digits(""), ConditionResult::Unknown);
}
}