automapper-validation 0.41.3

AHB condition expression parsing, evaluation, and EDIFACT validation
Documentation
//! Zeiträume of a transaction: its SG6 groups, each naming a Zeitraum-ID in
//! RFF DE1156 (UTILMD SG4, UTILTS SG5).

use crate::eval::{ConditionResult, EvaluationContext};

/// Whether the SG6 in scope (RFF with one of `qualifiers`) names a Zeitraum-ID
/// smaller than another SG6 of the same transaction with one of them — whether
/// a later Zeitraum follows it. UTILMD [471]/[472], UTILTS [58]: DTM+Z26 closes
/// such a Zeitraum, the last one may stay open (#476). False for an SG6 of
/// another kind. Without a scope: whether any SG6 is followed so.
pub fn followed_by_later_period(
    ctx: &EvaluationContext,
    tx_group: &str,
    qualifiers: &[&str],
) -> ConditionResult {
    let id = |path: &[(&str, usize)]| -> Option<u32> {
        ctx.find_segments_at(path, "RFF").iter().find_map(|rff| {
            qualifiers
                .contains(&rff.get_component(0, 0))
                .then(|| rff.get_component(0, 2).parse::<u32>().ok())
                .flatten()
        })
    };
    let followed = |tx: usize, sg6: usize| -> bool {
        let Some(own) = id(&[(tx_group, tx), ("SG6", sg6)]) else {
            return false;
        };
        (0..ctx.child_group_instance_count(&[tx_group], tx, "SG6"))
            .filter(|other| *other != sg6)
            .filter_map(|other| id(&[(tx_group, tx), ("SG6", other)]))
            .any(|other| other > own)
    };
    if let Some(scope) = ctx.enclosing_scope("SG6") {
        let at = |id: &str| scope.iter().find(|(g, _)| *g == id).map(|(_, i)| *i);
        return match (at(tx_group), at("SG6")) {
            (Some(tx), Some(sg6)) => ConditionResult::from(followed(tx, sg6)),
            _ => ConditionResult::Unknown,
        };
    }
    if ctx.navigator().is_none() {
        return ConditionResult::Unknown;
    }
    ConditionResult::from((0..ctx.group_instance_count(&[tx_group])).any(|tx| {
        (0..ctx.child_group_instance_count(&[tx_group], tx, "SG6")).any(|sg6| followed(tx, sg6))
    }))
}

/// UTILMD Strom `[2004]` / `[2005]`: "Segmentgruppe ist genau einmal /
/// mindestens einmal für jede Zeitraum-ID aus dem DE1156 der SG6 RFF+Z49
/// anzugeben" (#483). The SG8 with a SEQ qualifier among `seq_codes` that a
/// group `status` naming one of them governs, counted per Zeitraum-ID of the
/// transaction's SG6 RFF+Z49 (by SEQ DE1050): the IDs held another number of
/// times than the status allows, with how often. Empty when the status names
/// neither, or the transaction has no RFF+Z49. An SG8 naming a Zeitraum
/// without RFF+Z49 is `[22]`'s business, not counted here, and a kind not
/// sent at all the required-group check's.
pub fn sg8_off_count_per_zeitraum(
    ctx: &EvaluationContext,
    tx_group: &str,
    tx: usize,
    seq_codes: &[&str],
    status: &str,
) -> Vec<(String, usize)> {
    let exactly = status.contains("[2004]");
    if !exactly && !status.contains("[2005]") {
        return Vec::new();
    }
    let ids: Vec<String> = (0..ctx.child_group_instance_count(&[tx_group], tx, "SG6"))
        .flat_map(|sg6| ctx.find_segments_at(&[(tx_group, tx), ("SG6", sg6)], "RFF"))
        .filter(|rff| rff.get_component(0, 0) == "Z49")
        .map(|rff| rff.get_component(0, 2).to_string())
        .filter(|id| !id.is_empty())
        .collect();
    let named: Vec<String> = (0..ctx.child_group_instance_count(&[tx_group], tx, "SG8"))
        .flat_map(|sg8| ctx.find_segments_at(&[(tx_group, tx), ("SG8", sg8)], "SEQ"))
        .filter(|seq| seq_codes.contains(&seq.get_component(0, 0)))
        .map(|seq| seq.get_component(1, 0).to_string())
        .collect();
    // Not sent at all: the required-group check's business.
    if named.is_empty() {
        return Vec::new();
    }
    let mut seen: Vec<&String> = Vec::new();
    ids.iter()
        .filter(|id| {
            let first = !seen.contains(id);
            seen.push(id);
            first
        })
        .map(|id| (id.clone(), named.iter().filter(|n| *n == id).count()))
        .filter(|(_, count)| if exactly { *count != 1 } else { *count == 0 })
        .collect()
}