Skip to main content

automapper_validation/eval/
sg4_instances.rs

1//! Group instances of one UTILMD SG4 compared with each other.
2//!
3//! Some conditions compare *this* SG8 (or SG6) with another instance of the
4//! same transaction: "wie ein RFF+Z18 aus einer SG8 SEQ+Z01", "in einem
5//! weiteren SG8", "mit identischer Zeitraum-ID wie in dieser SG8". These read
6//! the instances of the SG4 that encloses the scope; every path is a scope path
7//! as [`EvaluationContext::scopes_under`] returns it.
8
9use mig_types::segment::OwnedSegment;
10
11use super::EvaluationContext;
12
13/// A scope path: `[("SG4", 0), ("SG8", 3)]`.
14pub type Path<'a> = Vec<(&'a str, usize)>;
15
16/// The SG8s of the enclosing SG4, the own one included; empty when the scope
17/// does not reach an SG4.
18pub fn sg8s<'a>(ctx: &EvaluationContext<'a>) -> Vec<Path<'a>> {
19    ctx.scopes_under("SG4", "SG8").unwrap_or_default()
20}
21
22/// The code (DE1229) and the Zeitraum-ID (DE1050) of the SEQ at `path`.
23pub fn seq_at(ctx: &EvaluationContext, path: &[(&str, usize)]) -> Option<(String, String)> {
24    ctx.find_segments_at(path, "SEQ").first().map(|s| {
25        (
26            s.get_element(0).to_string(),
27            s.get_component(1, 0).to_string(),
28        )
29    })
30}
31
32/// The RFF+`qualifier` segments at `path`.
33pub fn rffs_at(
34    ctx: &EvaluationContext,
35    path: &[(&str, usize)],
36    qualifier: &str,
37) -> Vec<OwnedSegment> {
38    ctx.find_segments_at(path, "RFF")
39        .into_iter()
40        .filter(|s| s.get_element(0) == qualifier)
41        .collect()
42}
43
44/// The non-empty references (DE1154) of the RFF+`qualifier` at `path`.
45pub fn rff_values_at(
46    ctx: &EvaluationContext,
47    path: &[(&str, usize)],
48    qualifier: &str,
49) -> Vec<String> {
50    rffs_at(ctx, path, qualifier)
51        .iter()
52        .map(|s| s.get_component(0, 1).to_string())
53        .filter(|v| !v.is_empty())
54        .collect()
55}
56
57/// Whether one SG10 of the SG8 at `path` — its CCI and CAV segments —
58/// satisfies `pred`.
59pub fn sg10_at(
60    ctx: &EvaluationContext,
61    path: &[(&str, usize)],
62    pred: impl Fn(&[OwnedSegment]) -> bool,
63) -> bool {
64    ctx.child_segments_at(path, "SG10", &["CCI", "CAV"])
65        .iter()
66        .any(|segs| pred(segs))
67}
68
69/// A CCI with Klassentyp (DE7059) `klassentyp` — any when empty — and
70/// Merkmal (DE7037) `merkmal` among `segs`.
71pub fn has_cci(segs: &[OwnedSegment], klassentyp: &str, merkmal: &str) -> bool {
72    segs.iter().any(|s| {
73        s.id == "CCI"
74            && (klassentyp.is_empty() || s.get_element(0) == klassentyp)
75            && s.get_component(2, 0) == merkmal
76    })
77}
78
79/// A CAV with Merkmalswert (DE7111) `code` among `segs`.
80pub fn has_cav(segs: &[OwnedSegment], code: &str) -> bool {
81    segs.iter()
82        .any(|s| s.id == "CAV" && s.get_element(0) == code)
83}
84
85/// The MaLo IDs of the enclosing SG4's SG5 LOC+Z16.
86pub fn malo_ids(ctx: &EvaluationContext) -> Vec<String> {
87    ctx.scopes_under("SG4", "SG5")
88        .unwrap_or_default()
89        .iter()
90        .flat_map(|path| ctx.find_segments_at(path, "LOC"))
91        .filter(|s| s.get_element(0) == "Z16")
92        .map(|s| s.get_component(1, 0).to_string())
93        .filter(|v| !v.is_empty())
94        .collect()
95}
96
97/// "… auf das gleiche SG5 LOC+Z16, wie ein RFF+Z18 aus einer SG8 SEQ+Z01, in
98/// dem SG10 … vorhanden": whether one of the MaLos `refs` names — a LOC+Z16 of
99/// this SG4 — has a SEQ+Z01 SG8 with an SG10 satisfying `pred`.
100pub fn z01_of_malo_has(
101    ctx: &EvaluationContext,
102    refs: &[String],
103    pred: impl Fn(&[OwnedSegment]) -> bool,
104) -> bool {
105    let malos = malo_ids(ctx);
106    let refs: Vec<&String> = refs.iter().filter(|r| malos.contains(r)).collect();
107    if refs.is_empty() {
108        return false;
109    }
110    sg8s(ctx).iter().any(|path| {
111        seq_at(ctx, path).is_some_and(|(code, _)| code == "Z01")
112            && rff_values_at(ctx, path, "Z18")
113                .iter()
114                .any(|v| refs.contains(&v))
115            && sg10_at(ctx, path, &pred)
116    })
117}
118
119/// The SG8s of the enclosing SG4 other than `own`.
120pub fn other_sg8s<'a>(ctx: &EvaluationContext<'a>, own: &[(&str, usize)]) -> Vec<Path<'a>> {
121    sg8s(ctx)
122        .into_iter()
123        .filter(|path| path.as_slice() != own)
124        .collect()
125}
126
127/// "Wenn eine SG8 SEQ+Z02 mit dem RFF+Z18 auf die gleiche ID einer
128/// Marktlokation referenziert wie das RFF+Z18 in dieser SG8, mit einer
129/// OBIS-Kennzahl … im PIA+5 vorhanden": another SG8 SEQ+Z02 of `own`'s MaLo
130/// whose PIA+5 carries an OBIS code satisfying `obis`.
131pub fn z02_of_same_malo_has(
132    ctx: &EvaluationContext,
133    own: &[(&str, usize)],
134    obis: impl Fn(&str) -> bool,
135) -> bool {
136    let refs = rff_values_at(ctx, own, "Z18");
137    !refs.is_empty()
138        && other_sg8s(ctx, own).iter().any(|path| {
139            seq_at(ctx, path).is_some_and(|(code, _)| code == "Z02")
140                && rff_values_at(ctx, path, "Z18")
141                    .iter()
142                    .any(|v| refs.contains(v))
143                && ctx
144                    .find_segments_at(path, "PIA")
145                    .iter()
146                    .any(|s| s.get_element(0) == "5" && obis(s.get_component(1, 0)))
147        })
148}
149
150/// "Wenn der Objektcode im RFF+Z33 im DE1154 derselben SG8 SEQ+`kinds` in
151/// einem weiteren SG8 SEQ+`kinds` … vorhanden ist": another SG8 with the same
152/// SEQ code (DE1229) carries `own`'s Objektcode — with the same fortlaufende
153/// Nummer (DE1156) when `same_nummer`, the same Zeitraum-ID (DE1050) when
154/// `same_zeitraum`.
155pub fn objektcode_elsewhere(
156    ctx: &EvaluationContext,
157    own: &[(&str, usize)],
158    kinds: &[&str],
159    same_nummer: bool,
160    same_zeitraum: bool,
161) -> bool {
162    let Some((code, zeitraum)) = seq_at(ctx, own).filter(|(c, _)| kinds.contains(&c.as_str()))
163    else {
164        return false;
165    };
166    let objektcodes = |path: &[(&str, usize)]| -> Vec<(String, String)> {
167        rffs_at(ctx, path, "Z33")
168            .iter()
169            .map(|s| {
170                (
171                    s.get_component(0, 1).to_string(),
172                    s.get_component(0, 2).to_string(),
173                )
174            })
175            .filter(|(c, _)| !c.is_empty())
176            .collect()
177    };
178    let mine = objektcodes(own);
179    !mine.is_empty()
180        && other_sg8s(ctx, own).iter().any(|path| {
181            seq_at(ctx, path).is_some_and(|(c, z)| c == code && (!same_zeitraum || z == zeitraum))
182                && objektcodes(path).iter().any(|(c, n)| {
183                    mine.iter()
184                        .any(|(mc, mn)| c == mc && (!same_nummer || n == mn))
185                })
186        })
187}
188
189/// The SG8 SEQ+Z01 of the enclosing SG4 with `own`'s Zeitraum-ID (DE1050);
190/// none when `own` has none.
191pub fn z01_of_zeitraum<'a>(ctx: &EvaluationContext<'a>, own: &[(&str, usize)]) -> Vec<Path<'a>> {
192    let Some((_, zeitraum)) = seq_at(ctx, own).filter(|(_, z)| !z.is_empty()) else {
193        return Vec::new();
194    };
195    sg8s(ctx)
196        .into_iter()
197        .filter(|path| seq_at(ctx, path).is_some_and(|(c, z)| c == "Z01" && z == zeitraum))
198        .collect()
199}
200
201/// An SG5 LOC+Z22 (ruhende Marktlokation) of the enclosing SG4 with Zeitraum-ID
202/// (DE3224) `zeitraum`.
203pub fn ruhende_malo_with_zeitraum(ctx: &EvaluationContext, zeitraum: &str) -> bool {
204    ctx.scopes_under("SG4", "SG5")
205        .unwrap_or_default()
206        .iter()
207        .flat_map(|path| ctx.find_segments_at(path, "LOC"))
208        .any(|s| s.get_element(0) == "Z22" && s.get_component(1, 3) == zeitraum)
209}