automapper-validation 0.20.0

AHB condition expression parsing, evaluation, and EDIFACT validation
Documentation
//! Group instances of one UTILMD SG4 compared with each other.
//!
//! Some conditions compare *this* SG8 (or SG6) with another instance of the
//! same transaction: "wie ein RFF+Z18 aus einer SG8 SEQ+Z01", "in einem
//! weiteren SG8", "mit identischer Zeitraum-ID wie in dieser SG8". These read
//! the instances of the SG4 that encloses the scope; every path is a scope path
//! as [`EvaluationContext::scopes_under`] returns it.

use mig_types::segment::OwnedSegment;

use super::EvaluationContext;

/// A scope path: `[("SG4", 0), ("SG8", 3)]`.
pub type Path<'a> = Vec<(&'a str, usize)>;

/// The SG8s of the enclosing SG4, the own one included; empty when the scope
/// does not reach an SG4.
pub fn sg8s<'a>(ctx: &EvaluationContext<'a>) -> Vec<Path<'a>> {
    ctx.scopes_under("SG4", "SG8").unwrap_or_default()
}

/// The code (DE1229) and the Zeitraum-ID (DE1050) of the SEQ at `path`.
pub fn seq_at(ctx: &EvaluationContext, path: &[(&str, usize)]) -> Option<(String, String)> {
    ctx.find_segments_at(path, "SEQ").first().map(|s| {
        (
            s.get_element(0).to_string(),
            s.get_component(1, 0).to_string(),
        )
    })
}

/// The RFF+`qualifier` segments at `path`.
pub fn rffs_at(
    ctx: &EvaluationContext,
    path: &[(&str, usize)],
    qualifier: &str,
) -> Vec<OwnedSegment> {
    ctx.find_segments_at(path, "RFF")
        .into_iter()
        .filter(|s| s.get_element(0) == qualifier)
        .collect()
}

/// The non-empty references (DE1154) of the RFF+`qualifier` at `path`.
pub fn rff_values_at(
    ctx: &EvaluationContext,
    path: &[(&str, usize)],
    qualifier: &str,
) -> Vec<String> {
    rffs_at(ctx, path, qualifier)
        .iter()
        .map(|s| s.get_component(0, 1).to_string())
        .filter(|v| !v.is_empty())
        .collect()
}

/// Whether one SG10 of the SG8 at `path` — its CCI and CAV segments —
/// satisfies `pred`.
pub fn sg10_at(
    ctx: &EvaluationContext,
    path: &[(&str, usize)],
    pred: impl Fn(&[OwnedSegment]) -> bool,
) -> bool {
    ctx.child_segments_at(path, "SG10", &["CCI", "CAV"])
        .iter()
        .any(|segs| pred(segs))
}

/// A CCI with Klassentyp (DE7059) `klassentyp` — any when empty — and
/// Merkmal (DE7037) `merkmal` among `segs`.
pub fn has_cci(segs: &[OwnedSegment], klassentyp: &str, merkmal: &str) -> bool {
    segs.iter().any(|s| {
        s.id == "CCI"
            && (klassentyp.is_empty() || s.get_element(0) == klassentyp)
            && s.get_component(2, 0) == merkmal
    })
}

/// A CAV with Merkmalswert (DE7111) `code` among `segs`.
pub fn has_cav(segs: &[OwnedSegment], code: &str) -> bool {
    segs.iter()
        .any(|s| s.id == "CAV" && s.get_element(0) == code)
}

/// The MaLo IDs of the enclosing SG4's SG5 LOC+Z16.
pub fn malo_ids(ctx: &EvaluationContext) -> Vec<String> {
    ctx.scopes_under("SG4", "SG5")
        .unwrap_or_default()
        .iter()
        .flat_map(|path| ctx.find_segments_at(path, "LOC"))
        .filter(|s| s.get_element(0) == "Z16")
        .map(|s| s.get_component(1, 0).to_string())
        .filter(|v| !v.is_empty())
        .collect()
}

/// "… auf das gleiche SG5 LOC+Z16, wie ein RFF+Z18 aus einer SG8 SEQ+Z01, in
/// dem SG10 … vorhanden": whether one of the MaLos `refs` names — a LOC+Z16 of
/// this SG4 — has a SEQ+Z01 SG8 with an SG10 satisfying `pred`.
pub fn z01_of_malo_has(
    ctx: &EvaluationContext,
    refs: &[String],
    pred: impl Fn(&[OwnedSegment]) -> bool,
) -> bool {
    let malos = malo_ids(ctx);
    let refs: Vec<&String> = refs.iter().filter(|r| malos.contains(r)).collect();
    if refs.is_empty() {
        return false;
    }
    sg8s(ctx).iter().any(|path| {
        seq_at(ctx, path).is_some_and(|(code, _)| code == "Z01")
            && rff_values_at(ctx, path, "Z18")
                .iter()
                .any(|v| refs.contains(&v))
            && sg10_at(ctx, path, &pred)
    })
}

/// The SG8s of the enclosing SG4 other than `own`.
pub fn other_sg8s<'a>(ctx: &EvaluationContext<'a>, own: &[(&str, usize)]) -> Vec<Path<'a>> {
    sg8s(ctx)
        .into_iter()
        .filter(|path| path.as_slice() != own)
        .collect()
}

/// "Wenn eine SG8 SEQ+Z02 mit dem RFF+Z18 auf die gleiche ID einer
/// Marktlokation referenziert wie das RFF+Z18 in dieser SG8, mit einer
/// OBIS-Kennzahl … im PIA+5 vorhanden": another SG8 SEQ+Z02 of `own`'s MaLo
/// whose PIA+5 carries an OBIS code satisfying `obis`.
pub fn z02_of_same_malo_has(
    ctx: &EvaluationContext,
    own: &[(&str, usize)],
    obis: impl Fn(&str) -> bool,
) -> bool {
    let refs = rff_values_at(ctx, own, "Z18");
    !refs.is_empty()
        && other_sg8s(ctx, own).iter().any(|path| {
            seq_at(ctx, path).is_some_and(|(code, _)| code == "Z02")
                && rff_values_at(ctx, path, "Z18")
                    .iter()
                    .any(|v| refs.contains(v))
                && ctx
                    .find_segments_at(path, "PIA")
                    .iter()
                    .any(|s| s.get_element(0) == "5" && obis(s.get_component(1, 0)))
        })
}

/// "Wenn der Objektcode im RFF+Z33 im DE1154 derselben SG8 SEQ+`kinds` in
/// einem weiteren SG8 SEQ+`kinds` … vorhanden ist": another SG8 with the same
/// SEQ code (DE1229) carries `own`'s Objektcode — with the same fortlaufende
/// Nummer (DE1156) when `same_nummer`, the same Zeitraum-ID (DE1050) when
/// `same_zeitraum`.
pub fn objektcode_elsewhere(
    ctx: &EvaluationContext,
    own: &[(&str, usize)],
    kinds: &[&str],
    same_nummer: bool,
    same_zeitraum: bool,
) -> bool {
    let Some((code, zeitraum)) = seq_at(ctx, own).filter(|(c, _)| kinds.contains(&c.as_str()))
    else {
        return false;
    };
    let objektcodes = |path: &[(&str, usize)]| -> Vec<(String, String)> {
        rffs_at(ctx, path, "Z33")
            .iter()
            .map(|s| {
                (
                    s.get_component(0, 1).to_string(),
                    s.get_component(0, 2).to_string(),
                )
            })
            .filter(|(c, _)| !c.is_empty())
            .collect()
    };
    let mine = objektcodes(own);
    !mine.is_empty()
        && other_sg8s(ctx, own).iter().any(|path| {
            seq_at(ctx, path).is_some_and(|(c, z)| c == code && (!same_zeitraum || z == zeitraum))
                && objektcodes(path).iter().any(|(c, n)| {
                    mine.iter()
                        .any(|(mc, mn)| c == mc && (!same_nummer || n == mn))
                })
        })
}

/// The SG8 SEQ+Z01 of the enclosing SG4 with `own`'s Zeitraum-ID (DE1050);
/// none when `own` has none.
pub fn z01_of_zeitraum<'a>(ctx: &EvaluationContext<'a>, own: &[(&str, usize)]) -> Vec<Path<'a>> {
    let Some((_, zeitraum)) = seq_at(ctx, own).filter(|(_, z)| !z.is_empty()) else {
        return Vec::new();
    };
    sg8s(ctx)
        .into_iter()
        .filter(|path| seq_at(ctx, path).is_some_and(|(c, z)| c == "Z01" && z == zeitraum))
        .collect()
}

/// An SG5 LOC+Z22 (ruhende Marktlokation) of the enclosing SG4 with Zeitraum-ID
/// (DE3224) `zeitraum`.
pub fn ruhende_malo_with_zeitraum(ctx: &EvaluationContext, zeitraum: &str) -> bool {
    ctx.scopes_under("SG4", "SG5")
        .unwrap_or_default()
        .iter()
        .flat_map(|path| ctx.find_segments_at(path, "LOC"))
        .any(|s| s.get_element(0) == "Z22" && s.get_component(1, 3) == zeitraum)
}