automapper-validation 0.4.0

AHB condition expression parsing, evaluation, and EDIFACT validation
Documentation
// <auto-generated>
// Generated by automapper-generator generate-conditions
// AHB: xml-migs-and-ahbs/FV2610/UTILTS_AHB_1_1_20260401.xml
// Generated: 2026-04-22T18:06:18Z
// Delegates unchanged conditions to UtiltsConditionEvaluatorFV2504 (FV2504).
// </auto-generated>

#![allow(clippy::style, clippy::complexity)]

#[allow(unused_imports)]
use crate::eval::format_validators::*;
use crate::eval::{ConditionEvaluator, ConditionResult, EvaluationContext};
use crate::generated::fv2504::UtiltsConditionEvaluatorFV2504;

/// Condition evaluator for UTILTS FV2610.
///
/// Delegates to [`UtiltsConditionEvaluatorFV2504`] for conditions whose AHB description is
/// unchanged from that version. Local overrides: [15, 63, 511, 2005].
pub struct UtiltsConditionEvaluatorFV2610 {
    fallback: UtiltsConditionEvaluatorFV2504,
}

impl Default for UtiltsConditionEvaluatorFV2610 {
    fn default() -> Self {
        Self {
            fallback: UtiltsConditionEvaluatorFV2504::default(),
        }
    }
}

/// Condition IDs whose implementation differs from the fallback.
const FV2610_OVERRIDES: &[u32] = &[15, 63, 511, 2005];

impl ConditionEvaluator for UtiltsConditionEvaluatorFV2610 {
    fn message_type(&self) -> &str {
        "UTILTS"
    }

    fn format_version(&self) -> &str {
        "FV2610"
    }

    fn evaluate(&self, condition: u32, ctx: &EvaluationContext) -> ConditionResult {
        match condition {
            15 => self.evaluate_15(ctx),
            63 => self.evaluate_63(ctx),
            511 => self.evaluate_511(ctx),
            2005 => self.evaluate_2005(ctx),
            other => self.fallback.evaluate(other, ctx),
        }
    }

    fn is_external(&self, condition: u32) -> bool {
        matches!(condition, 63) || self.fallback.is_external(condition)
    }

    fn is_known(&self, condition: u32) -> bool {
        FV2610_OVERRIDES.contains(&condition) || self.fallback.is_known(condition)
    }
}

impl UtiltsConditionEvaluatorFV2610 {
    /// [15] Wenn in einem SG5 IDE+24 nur eine SEQ+Z37 mit einer SG8 RFF+Z19 (Messlokation) und derselben Zeitraum-ID vorhanden ist
    // REVIEW: Navigates SG5 groups to find IDE+24, then counts SG8 child instances where SEQ+Z37 and RFF+Z19 coexist with a matching Zeitraum-ID. 'Derselben Zeitraum-ID' is interpreted as: the identifier in SEQ elements[1][0] must equal the reference value in RFF C506 elements[0][1]. True if exactly one such paired SG8 is found. Element positions for the Zeitraum-ID in SEQ and RFF assume standard BDEW UTILTS structure (RFF+Z19:id → elements[0][1], SEQ+Z37+id → elements[1][0]) — no segment structure reference was provided to confirm. (medium confidence)
    fn evaluate_15(&self, ctx: &EvaluationContext) -> ConditionResult {
        let nav = match ctx.navigator() {
            Some(n) => n,
            None => return ConditionResult::Unknown,
        };
        let sg5_count = nav.group_instance_count(&["SG5"]);
        for sg5_i in 0..sg5_count {
            let ide_segs = nav.find_segments_in_group("IDE", &["SG5"], sg5_i);
            let has_ide_24 = ide_segs.iter().any(|s| {
                s.elements
                    .first()
                    .and_then(|e| e.first())
                    .is_some_and(|v| v == "24")
            });
            if !has_ide_24 {
                continue;
            }
            let sg8_count = nav.child_group_instance_count(&["SG5"], sg5_i, "SG8");
            let mut matching_count = 0usize;
            for sg8_j in 0..sg8_count {
                let seq_segs =
                    nav.find_segments_in_child_group("SEQ", &["SG5"], sg5_i, "SG8", sg8_j);
                let seq_zeitraum_id = seq_segs
                    .iter()
                    .find(|s| {
                        s.elements
                            .first()
                            .and_then(|e| e.first())
                            .is_some_and(|v| v == "Z37")
                    })
                    .and_then(|s| s.elements.get(1).and_then(|e| e.first()).cloned());
                let Some(seq_id) = seq_zeitraum_id else {
                    continue;
                };
                let rff_segs =
                    nav.find_segments_in_child_group("RFF", &["SG5"], sg5_i, "SG8", sg8_j);
                let has_z19_with_id = rff_segs.iter().any(|s| {
                    s.elements
                        .first()
                        .and_then(|e| e.first())
                        .is_some_and(|v| v == "Z19")
                        && s.elements
                            .first()
                            .and_then(|e| e.get(1))
                            .is_some_and(|v| v == &seq_id)
                });
                if has_z19_with_id {
                    matching_count += 1;
                }
            }
            if matching_count > 0 {
                return ConditionResult::from(matching_count == 1);
            }
        }
        ConditionResult::Unknown
    }

    /// [63] Wenn MP-ID in SG2 NAD+MR (Nachrichtenempfänger) in der Rolle NB
    /// EXTERNAL: Requires context from outside the message.
    fn evaluate_63(&self, ctx: &EvaluationContext) -> ConditionResult {
        ctx.external.evaluate("recipient_is_nb")
    }

    /// [511] Hinweis: Der Zählzeitänderungszeitpunkt (SG8 DTM+Z33) dieser SG8 darf in keiner anderen SG8 „Zählzeitdefinition“ wiederholt werden
    fn evaluate_511(&self, _ctx: &EvaluationContext) -> ConditionResult {
        // Hinweis: DTM+Z33 Zählzeitänderungszeitpunkt must not repeat across SG8 Zählzeitdefinition groups — informational annotation
        ConditionResult::True
    }

    /// [2005] Segment ist genau einmal für jede Zeitraum-ID aus dem DE9012 der SG5 STS+E01 ("Status der Antwort") anzugeben, wenn im selben SG5 STS+E01 im DE9013 der Code A99 ("Sonstiges") enthalten ist
    // REVIEW: Finds SG5 STS+E01 segments where DE9013 (C556 comp 0, elements[2][0]) = 'A99' and collects their Zeitraum-IDs from DE9012 (C556 comp 3, elements[2][3]). The annotated segment is FTX+ACB (UTILTS 25010 FV2610 Number=00018), which carries the Zeitraum-ID at C107.DE4441 (elements[2][0]). For each qualifying ID, exactly one FTX+ACB must reference it.
    fn evaluate_2005(&self, ctx: &EvaluationContext) -> ConditionResult {
        // Collect Zeitraum-IDs from SG5 STS+E01 where DE9013 = A99.
        let sts_e01 = ctx.find_segments_with_qualifier("STS", 0, "E01");
        let zeitraum_ids: Vec<String> = sts_e01
            .iter()
            .filter(|s| {
                s.elements
                    .get(2)
                    .and_then(|e| e.first())
                    .is_some_and(|v| v == "A99")
            })
            .filter_map(|s| s.elements.get(2).and_then(|e| e.get(3)).cloned())
            .filter(|id| !id.is_empty())
            .collect();
        if zeitraum_ids.is_empty() {
            return ConditionResult::Unknown;
        }
        // For each Zeitraum-ID, exactly one FTX+ACB (C107.DE4441 at elements[2][0]) must reference it.
        let ftx_acb = ctx.find_segments_with_qualifier("FTX", 0, "ACB");
        for zid in &zeitraum_ids {
            let count = ftx_acb
                .iter()
                .filter(|s| {
                    s.elements
                        .get(2)
                        .and_then(|e| e.first())
                        .is_some_and(|v| v == zid)
                })
                .count();
            if count != 1 {
                return ConditionResult::False;
            }
        }
        ConditionResult::True
    }
}