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 SG8s of the enclosing SG4 when the scope reaches one, else those of
23/// every SG4 of the message; empty without a navigator.
24pub fn sg8s_or_all<'a>(ctx: &EvaluationContext<'a>) -> Vec<Path<'a>> {
25    if let Some(own) = ctx.scopes_under("SG4", "SG8") {
26        return own;
27    }
28    let Some(nav) = ctx.navigator() else {
29        return Vec::new();
30    };
31    (0..nav.group_instance_count(&["SG4"]))
32        .flat_map(|i| {
33            (0..nav.child_instance_count(&[("SG4", i)], "SG8"))
34                .map(move |j| vec![("SG4", i), ("SG8", j)])
35        })
36        .collect()
37}
38
39/// "Wenn SG8 SEQ+Z01 (Daten der Marktlokation) SG10 CCI+Z30++Z07 vorhanden":
40/// an SG8 whose SEQ code is one of `seq_codes` with an SG10 carrying CCI
41/// `klassentyp`/`merkmal` (see [`has_cci`]), among [`sg8s_or_all`].
42pub fn seq_with_cci(
43    ctx: &EvaluationContext,
44    seq_codes: &[&str],
45    klassentyp: &str,
46    merkmal: &str,
47) -> bool {
48    sg8s_or_all(ctx).iter().any(|path| {
49        seq_at(ctx, path).is_some_and(|(code, _)| seq_codes.contains(&code.as_str()))
50            && sg10_at(ctx, path, |segs| has_cci(segs, klassentyp, merkmal))
51    })
52}
53
54/// The code (DE1229) and the Zeitraum-ID (DE1050) of the SEQ at `path`.
55pub fn seq_at(ctx: &EvaluationContext, path: &[(&str, usize)]) -> Option<(String, String)> {
56    ctx.find_segments_at(path, "SEQ").first().map(|s| {
57        (
58            s.get_element(0).to_string(),
59            s.get_component(1, 0).to_string(),
60        )
61    })
62}
63
64/// The RFF+`qualifier` segments at `path`.
65pub fn rffs_at(
66    ctx: &EvaluationContext,
67    path: &[(&str, usize)],
68    qualifier: &str,
69) -> Vec<OwnedSegment> {
70    ctx.find_segments_at(path, "RFF")
71        .into_iter()
72        .filter(|s| s.get_element(0) == qualifier)
73        .collect()
74}
75
76/// The non-empty references (DE1154) of the RFF+`qualifier` at `path`.
77pub fn rff_values_at(
78    ctx: &EvaluationContext,
79    path: &[(&str, usize)],
80    qualifier: &str,
81) -> Vec<String> {
82    rffs_at(ctx, path, qualifier)
83        .iter()
84        .map(|s| s.get_component(0, 1).to_string())
85        .filter(|v| !v.is_empty())
86        .collect()
87}
88
89/// Whether one SG10 of the SG8 at `path` — its CCI and CAV segments —
90/// satisfies `pred`.
91pub fn sg10_at(
92    ctx: &EvaluationContext,
93    path: &[(&str, usize)],
94    pred: impl Fn(&[OwnedSegment]) -> bool,
95) -> bool {
96    ctx.child_segments_at(path, "SG10", &["CCI", "CAV"])
97        .iter()
98        .any(|segs| pred(segs))
99}
100
101/// A CCI with Klassentyp (DE7059) `klassentyp` — any when empty — and
102/// Merkmal (DE7037) `merkmal` among `segs`.
103pub fn has_cci(segs: &[OwnedSegment], klassentyp: &str, merkmal: &str) -> bool {
104    segs.iter().any(|s| {
105        s.id == "CCI"
106            && (klassentyp.is_empty() || s.get_element(0) == klassentyp)
107            && s.get_component(2, 0) == merkmal
108    })
109}
110
111/// A CAV with Merkmalswert (DE7111) `code` among `segs`.
112pub fn has_cav(segs: &[OwnedSegment], code: &str) -> bool {
113    segs.iter()
114        .any(|s| s.id == "CAV" && s.get_element(0) == code)
115}
116
117/// The MaLo IDs of the enclosing SG4's SG5 LOC+Z16.
118pub fn malo_ids(ctx: &EvaluationContext) -> Vec<String> {
119    ctx.scopes_under("SG4", "SG5")
120        .unwrap_or_default()
121        .iter()
122        .flat_map(|path| ctx.find_segments_at(path, "LOC"))
123        .filter(|s| s.get_element(0) == "Z16")
124        .map(|s| s.get_component(1, 0).to_string())
125        .filter(|v| !v.is_empty())
126        .collect()
127}
128
129/// "… auf das gleiche SG5 LOC+Z16, wie ein RFF+Z18 aus einer SG8 SEQ+Z01, in
130/// dem SG10 … vorhanden": whether one of the MaLos `refs` names — a LOC+Z16 of
131/// this SG4 — has a SEQ+Z01 SG8 with an SG10 satisfying `pred`.
132pub fn z01_of_malo_has(
133    ctx: &EvaluationContext,
134    refs: &[String],
135    pred: impl Fn(&[OwnedSegment]) -> bool,
136) -> bool {
137    let malos = malo_ids(ctx);
138    let refs: Vec<&String> = refs.iter().filter(|r| malos.contains(r)).collect();
139    if refs.is_empty() {
140        return false;
141    }
142    sg8s(ctx).iter().any(|path| {
143        seq_at(ctx, path).is_some_and(|(code, _)| code == "Z01")
144            && rff_values_at(ctx, path, "Z18")
145                .iter()
146                .any(|v| refs.contains(&v))
147            && sg10_at(ctx, path, &pred)
148    })
149}
150
151/// The SG8s of the enclosing SG4 other than `own`.
152pub fn other_sg8s<'a>(ctx: &EvaluationContext<'a>, own: &[(&str, usize)]) -> Vec<Path<'a>> {
153    sg8s(ctx)
154        .into_iter()
155        .filter(|path| path.as_slice() != own)
156        .collect()
157}
158
159/// "Wenn eine SG8 SEQ+Z02 mit dem RFF+Z18 auf die gleiche ID einer
160/// Marktlokation referenziert wie das RFF+Z18 in dieser SG8, mit einer
161/// OBIS-Kennzahl … im PIA+5 vorhanden": another SG8 SEQ+Z02 of `own`'s MaLo
162/// whose PIA+5 carries an OBIS code satisfying `obis`.
163pub fn z02_of_same_malo_has(
164    ctx: &EvaluationContext,
165    own: &[(&str, usize)],
166    obis: impl Fn(&str) -> bool,
167) -> bool {
168    let refs = rff_values_at(ctx, own, "Z18");
169    !refs.is_empty()
170        && other_sg8s(ctx, own).iter().any(|path| {
171            seq_at(ctx, path).is_some_and(|(code, _)| code == "Z02")
172                && rff_values_at(ctx, path, "Z18")
173                    .iter()
174                    .any(|v| refs.contains(v))
175                && ctx
176                    .find_segments_at(path, "PIA")
177                    .iter()
178                    .any(|s| s.get_element(0) == "5" && obis(s.get_component(1, 0)))
179        })
180}
181
182/// "Wenn der Objektcode im RFF+Z33 im DE1154 derselben SG8 SEQ+`kinds` in
183/// einem weiteren SG8 SEQ+`kinds` … vorhanden ist": another SG8 with the same
184/// SEQ code (DE1229) carries `own`'s Objektcode — with the same fortlaufende
185/// Nummer (DE1156) when `same_nummer`, the same Zeitraum-ID (DE1050) when
186/// `same_zeitraum`.
187pub fn objektcode_elsewhere(
188    ctx: &EvaluationContext,
189    own: &[(&str, usize)],
190    kinds: &[&str],
191    same_nummer: bool,
192    same_zeitraum: bool,
193) -> bool {
194    let Some((code, zeitraum)) = seq_at(ctx, own).filter(|(c, _)| kinds.contains(&c.as_str()))
195    else {
196        return false;
197    };
198    let objektcodes = |path: &[(&str, usize)]| -> Vec<(String, String)> {
199        rffs_at(ctx, path, "Z33")
200            .iter()
201            .map(|s| {
202                (
203                    s.get_component(0, 1).to_string(),
204                    s.get_component(0, 2).to_string(),
205                )
206            })
207            .filter(|(c, _)| !c.is_empty())
208            .collect()
209    };
210    let mine = objektcodes(own);
211    !mine.is_empty()
212        && other_sg8s(ctx, own).iter().any(|path| {
213            seq_at(ctx, path).is_some_and(|(c, z)| c == code && (!same_zeitraum || z == zeitraum))
214                && objektcodes(path).iter().any(|(c, n)| {
215                    mine.iter()
216                        .any(|(mc, mn)| c == mc && (!same_nummer || n == mn))
217                })
218        })
219}
220
221/// The SG8 SEQ+Z01 of the enclosing SG4 with `own`'s Zeitraum-ID (DE1050);
222/// none when `own` has none.
223pub fn z01_of_zeitraum<'a>(ctx: &EvaluationContext<'a>, own: &[(&str, usize)]) -> Vec<Path<'a>> {
224    let Some((_, zeitraum)) = seq_at(ctx, own).filter(|(_, z)| !z.is_empty()) else {
225        return Vec::new();
226    };
227    sg8s(ctx)
228        .into_iter()
229        .filter(|path| seq_at(ctx, path).is_some_and(|(c, z)| c == "Z01" && z == zeitraum))
230        .collect()
231}
232
233/// An SG5 LOC+Z22 (ruhende Marktlokation) of the enclosing SG4 with Zeitraum-ID
234/// (DE3224) `zeitraum`.
235pub fn ruhende_malo_with_zeitraum(ctx: &EvaluationContext, zeitraum: &str) -> bool {
236    ctx.scopes_under("SG4", "SG5")
237        .unwrap_or_default()
238        .iter()
239        .flat_map(|path| ctx.find_segments_at(path, "LOC"))
240        .any(|s| s.get_element(0) == "Z22" && s.get_component(1, 3) == zeitraum)
241}