automapper-validation 0.30.21

AHB condition expression parsing, evaluation, and EDIFACT validation
Documentation
//! Conditions that say what a present value must be, judged on the value.
//!
//! Found by the BO4E-path value check (0.30.1): each implementation looked
//! at something other than the value it describes.

use automapper_validation::eval::{
    ConditionEvaluator, ConditionResult, EvaluationContext, MapExternalProvider,
};
use automapper_validation::generated::fv2504::{
    OrdersConditionEvaluatorFV2504, ReqoteConditionEvaluatorFV2504,
    UtilmdStromConditionEvaluatorFV2504,
};
use mig_types::segment::OwnedSegment;

fn seg(id: &str, elements: &[&[&str]]) -> OwnedSegment {
    OwnedSegment {
        id: id.to_string(),
        segment_number: 0,
        elements: elements
            .iter()
            .map(|e| e.iter().map(|c| c.to_string()).collect())
            .collect(),
    }
}

fn judge(
    eval: &dyn ConditionEvaluator,
    id: u32,
    segments: &[OwnedSegment],
    value: &str,
) -> ConditionResult {
    let external = MapExternalProvider::new(Default::default());
    let ctx = EvaluationContext::new("00000", &external, segments);
    let at = EvaluationContext {
        resolved_value: Some(value),
        ..ctx
    };
    eval.evaluate(id, &at)
}

/// [22] "Es ist die Zeitraum-ID vom DE1156 aus einem passenden SG6
/// RFF+Z47/Z48/Z49 einzutragen": the value is one of those IDs.
#[test]
fn utilmd_22_the_value_is_a_zeitraum_id_of_the_message() {
    let segments = [
        seg("RFF", &[&["Z49", "", "701537"]]),
        seg("RFF", &[&["Z47", "", "2"]]),
        seg("SEQ", &[&["Z02"], &["1"]]),
    ];
    let eval = UtilmdStromConditionEvaluatorFV2504::default();
    assert_eq!(judge(&eval, 22, &segments, "701537"), ConditionResult::True);
    assert_eq!(judge(&eval, 22, &segments, "2"), ConditionResult::True);
    // A MaLo-ID, and the SEQ's DE1050 the old code compared against.
    assert_eq!(
        judge(&eval, 22, &segments, "51737695732"),
        ConditionResult::False
    );
    assert_eq!(judge(&eval, 22, &segments, "1"), ConditionResult::False);
}

/// [88] "Wert muss identisch mit DE2380 aus dem SG4 DTM+157 sein".
#[test]
fn utilmd_88_the_value_equals_the_dtm_157() {
    let segments = [seg("DTM", &[&["157", "202502122300+00", "303"]])];
    let eval = UtilmdStromConditionEvaluatorFV2504::default();
    assert_eq!(
        judge(&eval, 88, &segments, "202502122300+00"),
        ConditionResult::True
    );
    assert_eq!(
        judge(&eval, 88, &segments, "20250401"),
        ConditionResult::False
    );
}

/// [950] "Format: Marktlokations-ID" was a stub that held for any value.
#[test]
fn reqote_950_is_the_malo_id_format() {
    let eval = ReqoteConditionEvaluatorFV2504::default();
    assert_eq!(judge(&eval, 950, &[], "51238696781"), ConditionResult::True);
    assert_eq!(judge(&eval, 950, &[], "1"), ConditionResult::False);
}

/// [961] "Format: SR-ID" checked the MaLo-ID format.
#[test]
fn orders_961_is_the_sr_id_format() {
    let eval = OrdersConditionEvaluatorFV2504::default();
    assert_eq!(
        judge(&eval, 961, &[], "06756243994"),
        ConditionResult::False
    );
    assert_eq!(judge(&eval, 961, &[], "C816417ST77"), ConditionResult::True);
}

/// [126] "Das erste SG6 RFF+Z49/Z53 hat somit die 1, das zweite die 2": the
/// Zeitraum-ID is the RFF's position among them.
#[test]
fn utilmd_126_the_zeitraum_id_counts_the_rff_repetitions() {
    let first = seg("RFF", &[&["Z49", "", "1"]]);
    let second = seg("RFF", &[&["Z53", "", "7"]]);
    let segments = [
        first.clone(),
        seg("RFF", &[&["Z47", "", "1"]]),
        second.clone(),
    ];
    let eval = UtilmdStromConditionEvaluatorFV2504::default();
    let external = MapExternalProvider::new(Default::default());
    let ctx = EvaluationContext::new("00000", &external, &segments);
    let at = |rff: &OwnedSegment, value: &'static str| {
        let ctx = EvaluationContext {
            resolved_value: Some(value),
            resolved_segment: Some(rff.elements.as_slice()),
            ..ctx
        };
        eval.evaluate(126, &ctx)
    };
    assert_eq!(at(&first, "1"), ConditionResult::True);
    assert_eq!(at(&second, "7"), ConditionResult::False, "the second is 2");
    let second_right = seg("RFF", &[&["Z53", "", "2"]]);
    let segments = [first.clone(), second_right.clone()];
    let ctx = EvaluationContext::new("00000", &external, &segments);
    let ctx = EvaluationContext {
        resolved_value: Some("2"),
        resolved_segment: Some(second_right.elements.as_slice()),
        ..ctx
    };
    assert_eq!(eval.evaluate(126, &ctx), ConditionResult::True);
}

/// [2004]/[2005] "Segmentgruppe ist genau/mindestens einmal für jede
/// Zeitraum-ID … anzugeben": the group applies where an RFF+Z49 names a
/// Zeitraum-ID, however many SG8 variants name it. The generated code counted
/// the SG8 of every variant, so two variants made the group never apply.
#[test]
fn utilmd_2004_and_2005_apply_where_a_zeitraum_id_is_named() {
    let eval = UtilmdStromConditionEvaluatorFV2504::default();
    let external = MapExternalProvider::new(Default::default());
    let with = [
        seg("RFF", &[&["Z49", "", "1"]]),
        seg("SEQ", &[&["Z02"], &["1"]]),
        seg("SEQ", &[&["Z59"], &["1"]]),
    ];
    let without = [seg("RFF", &[&["Z13", "55640"]])];
    for id in [2004, 2005] {
        let ctx = EvaluationContext::new("00000", &external, &with);
        assert_eq!(eval.evaluate(id, &ctx), ConditionResult::True, "[{id}]");
        let ctx = EvaluationContext::new("00000", &external, &without);
        assert_eq!(eval.evaluate(id, &ctx), ConditionResult::False, "[{id}]");
    }
}

/// [32] "Wenn mehr als ein SG8 (Referenz auf die Lokationsbündelstruktur)
/// vorhanden": the SEQ+Z78, not every SEQ (mako-twin, 55043 on 0.30.1).
#[test]
fn utilmd_32_counts_the_lokationsbuendel_references() {
    let eval = UtilmdStromConditionEvaluatorFV2504::default();
    let external = MapExternalProvider::new(Default::default());
    let one = [
        seg("SEQ", &[&["Z78"], &["1"]]),
        seg("SEQ", &[&["Z01"], &["1"]]),
        seg("SEQ", &[&["Z58"], &["1"]]),
    ];
    let ctx = EvaluationContext::new("55043", &external, &one);
    assert_eq!(eval.evaluate(32, &ctx), ConditionResult::False);
    let two = [
        seg("SEQ", &[&["Z78"], &["1"]]),
        seg("SEQ", &[&["Z78"], &["2"]]),
    ];
    let ctx = EvaluationContext::new("55043", &external, &two);
    assert_eq!(eval.evaluate(32, &ctx), ConditionResult::True);
}

/// [902] "Möglicher Wert: ≥ 0" (UTILMD QTY 6060 / CAV 7110, PRICAT PRI 5118)
/// was a stub that always held: a Vorjahresverbrauch of -5 passed
/// (reported by mako-twin for 55017).
#[test]
fn condition_902_judges_the_value() {
    use automapper_validation::generated::fv2504::PricatConditionEvaluatorFV2504;
    let strom = UtilmdStromConditionEvaluatorFV2504::default();
    let pricat = PricatConditionEvaluatorFV2504::default();
    for eval in [&strom as &dyn ConditionEvaluator, &pricat] {
        let qty = [seg("QTY", &[&["Z09", "-5", "KWH"]])];
        assert_eq!(judge(eval, 902, &qty, "-5"), ConditionResult::False);
        assert_eq!(judge(eval, 902, &qty, "0"), ConditionResult::True);
        assert_eq!(judge(eval, 902, &qty, "12.5"), ConditionResult::True);
    }
}

/// [352] "Wenn SG4 STS++A05/ A06/ A14 /A15 (Status der Antwort) vorhanden":
/// the answer stands in C556 DE9013 (`STS+E01++A05:E_0614`). Up to 0.30.18
/// it was read from C555, so it never held (reported by mako-twin for 55018).
#[test]
fn condition_352_reads_the_answer() {
    let eval = UtilmdStromConditionEvaluatorFV2504::default();
    let external = MapExternalProvider::new(Default::default());
    for (answer, expected) in [
        ("A05", ConditionResult::True),
        ("A15", ConditionResult::True),
        ("A04", ConditionResult::False),
    ] {
        let segments = [seg("STS", &[&["E01"], &[], &[answer, "E_0614"]])];
        let ctx = EvaluationContext::new("55018", &external, &segments);
        assert_eq!(eval.evaluate(352, &ctx), expected, "{answer}");
    }
}