Skip to main content

automapper_validation/generated/fv2504/
utilts_conditions_fv2504.rs

1// <auto-generated>
2// Generated by automapper-generator generate-conditions
3// AHB: xml-migs-and-ahbs/FV2504/UTILTS_AHB_1_0_Fehlerkorrektur_20250218.xml
4// Generated: 2026-03-12T10:04:46Z
5// </auto-generated>
6
7// LLM-produced bodies aren't idiomatic Rust — silence stylistic lints at
8// file scope so `cargo clippy -D warnings` stays green. Correctness,
9// suspicious, and perf categories stay on so real bugs still surface.
10#![allow(clippy::style, clippy::complexity)]
11
12#[allow(unused_imports)]
13use crate::eval::format_validators::*;
14use crate::eval::{Cluster, ConditionEvaluator, ConditionResult, EvaluationContext};
15// HAND-EDITED: shared cluster helper for condition [61].
16// Preserve across regeneration — see CLAUDE.md "Codelist Providers".
17use crate::eval::ebd_cluster::any_sts_e01_in_cluster;
18
19/// Generated condition evaluator for UTILTS FV2504.
20pub struct UtiltsConditionEvaluatorFV2504 {
21    // External condition IDs that require runtime context.
22    external_conditions: std::collections::HashSet<u32>,
23}
24
25impl Default for UtiltsConditionEvaluatorFV2504 {
26    fn default() -> Self {
27        let mut external_conditions = std::collections::HashSet::new();
28        external_conditions.insert(1);
29        external_conditions.insert(22);
30        external_conditions.insert(25);
31        external_conditions.insert(26);
32        external_conditions.insert(37);
33        // HAND-EDITED: [61] is no longer external — EBD cluster lookup handles it.
34        // external_conditions.insert(61);
35        external_conditions.insert(62);
36        Self {
37            external_conditions,
38        }
39    }
40}
41
42impl ConditionEvaluator for UtiltsConditionEvaluatorFV2504 {
43    fn message_type(&self) -> &str {
44        "UTILTS"
45    }
46
47    fn format_version(&self) -> &str {
48        "FV2504"
49    }
50
51    fn evaluate(&self, condition: u32, ctx: &EvaluationContext) -> ConditionResult {
52        match condition {
53            1 => self.evaluate_1(ctx),
54            2 => self.evaluate_2(ctx),
55            5 => self.evaluate_5(ctx),
56            6 => self.evaluate_6(ctx),
57            7 => self.evaluate_7(ctx),
58            8 => self.evaluate_8(ctx),
59            9 => self.evaluate_9(ctx),
60            10 => self.evaluate_10(ctx),
61            11 => self.evaluate_11(ctx),
62            12 => self.evaluate_12(ctx),
63            13 => self.evaluate_13(ctx),
64            14 => self.evaluate_14(ctx),
65            15 => self.evaluate_15(ctx),
66            21 => self.evaluate_21(ctx),
67            22 => self.evaluate_22(ctx),
68            24 => self.evaluate_24(ctx),
69            25 => self.evaluate_25(ctx),
70            26 => self.evaluate_26(ctx),
71            27 => self.evaluate_27(ctx),
72            29 => self.evaluate_29(ctx),
73            30 => self.evaluate_30(ctx),
74            31 => self.evaluate_31(ctx),
75            32 => self.evaluate_32(ctx),
76            33 => self.evaluate_33(ctx),
77            34 => self.evaluate_34(ctx),
78            36 => self.evaluate_36(ctx),
79            37 => self.evaluate_37(ctx),
80            41 => self.evaluate_41(ctx),
81            42 => self.evaluate_42(ctx),
82            43 => self.evaluate_43(ctx),
83            44 => self.evaluate_44(ctx),
84            46 => self.evaluate_46(ctx),
85            47 => self.evaluate_47(ctx),
86            48 => self.evaluate_48(ctx),
87            49 => self.evaluate_49(ctx),
88            50 => self.evaluate_50(ctx),
89            53 => self.evaluate_53(ctx),
90            54 => self.evaluate_54(ctx),
91            55 => self.evaluate_55(ctx),
92            56 => self.evaluate_56(ctx),
93            57 => self.evaluate_57(ctx),
94            58 => self.evaluate_58(ctx),
95            59 => self.evaluate_59(ctx),
96            61 => self.evaluate_61(ctx),
97            62 => self.evaluate_62(ctx),
98            490 => self.evaluate_490(ctx),
99            491 => self.evaluate_491(ctx),
100            494 => self.evaluate_494(ctx),
101            501 => self.evaluate_501(ctx),
102            502 => self.evaluate_502(ctx),
103            504 => self.evaluate_504(ctx),
104            505 => self.evaluate_505(ctx),
105            506 => self.evaluate_506(ctx),
106            507 => self.evaluate_507(ctx),
107            508 => self.evaluate_508(ctx),
108            509 => self.evaluate_509(ctx),
109            510 => self.evaluate_510(ctx),
110            511 => self.evaluate_511(ctx),
111            512 => self.evaluate_512(ctx),
112            513 => self.evaluate_513(ctx),
113            514 => self.evaluate_514(ctx),
114            515 => self.evaluate_515(ctx),
115            516 => self.evaluate_516(ctx),
116            517 => self.evaluate_517(ctx),
117            518 => self.evaluate_518(ctx),
118            519 => self.evaluate_519(ctx),
119            520 => self.evaluate_520(ctx),
120            521 => self.evaluate_521(ctx),
121            522 => self.evaluate_522(ctx),
122            523 => self.evaluate_523(ctx),
123            524 => self.evaluate_524(ctx),
124            525 => self.evaluate_525(ctx),
125            526 => self.evaluate_526(ctx),
126            527 => self.evaluate_527(ctx),
127            528 => self.evaluate_528(ctx),
128            529 => self.evaluate_529(ctx),
129            530 => self.evaluate_530(ctx),
130            531 => self.evaluate_531(ctx),
131            532 => self.evaluate_532(ctx),
132            533 => self.evaluate_533(ctx),
133            534 => self.evaluate_534(ctx),
134            912 => self.evaluate_912(ctx),
135            913 => self.evaluate_913(ctx),
136            914 => self.evaluate_914(ctx),
137            915 => self.evaluate_915(ctx),
138            930 => self.evaluate_930(ctx),
139            931 => self.evaluate_931(ctx),
140            932 => self.evaluate_932(ctx),
141            933 => self.evaluate_933(ctx),
142            937 => self.evaluate_937(ctx),
143            939 => self.evaluate_939(ctx),
144            940 => self.evaluate_940(ctx),
145            947 => self.evaluate_947(ctx),
146            950 => self.evaluate_950(ctx),
147            951 => self.evaluate_951(ctx),
148            960 => self.evaluate_960(ctx),
149            963 => self.evaluate_963(ctx),
150            964 => self.evaluate_964(ctx),
151            965 => self.evaluate_965(ctx),
152            969 => self.evaluate_969(ctx),
153            2001 => self.evaluate_2001(ctx),
154            2002 => self.evaluate_2002(ctx),
155            2004 => self.evaluate_2004(ctx),
156            2005 => self.evaluate_2005(ctx),
157            2006 => self.evaluate_2006(ctx),
158            2007 => self.evaluate_2007(ctx),
159            _ => ConditionResult::Unknown,
160        }
161    }
162
163    fn is_external(&self, condition: u32) -> bool {
164        self.external_conditions.contains(&condition)
165    }
166    fn is_known(&self, condition: u32) -> bool {
167        matches!(
168            condition,
169            1 | 2
170                | 5
171                | 6
172                | 7
173                | 8
174                | 9
175                | 10
176                | 11
177                | 12
178                | 13
179                | 14
180                | 15
181                | 21
182                | 22
183                | 24
184                | 25
185                | 26
186                | 27
187                | 29
188                | 30
189                | 31
190                | 32
191                | 33
192                | 34
193                | 36
194                | 37
195                | 41
196                | 42
197                | 43
198                | 44
199                | 46
200                | 47
201                | 48
202                | 49
203                | 50
204                | 53
205                | 54
206                | 55
207                | 56
208                | 57
209                | 58
210                | 59
211                | 61
212                | 62
213                | 490
214                | 491
215                | 494
216                | 501
217                | 502
218                | 504
219                | 505
220                | 506
221                | 507
222                | 508
223                | 509
224                | 510
225                | 511
226                | 512
227                | 513
228                | 514
229                | 515
230                | 516
231                | 517
232                | 518
233                | 519
234                | 520
235                | 521
236                | 522
237                | 523
238                | 524
239                | 525
240                | 526
241                | 527
242                | 528
243                | 529
244                | 530
245                | 531
246                | 532
247                | 533
248                | 534
249                | 912
250                | 913
251                | 914
252                | 915
253                | 930
254                | 931
255                | 932
256                | 933
257                | 937
258                | 939
259                | 940
260                | 947
261                | 950
262                | 951
263                | 960
264                | 963
265                | 964
266                | 965
267                | 969
268                | 2001
269                | 2002
270                | 2004
271                | 2005
272                | 2006
273                | 2007
274        )
275    }
276}
277
278impl UtiltsConditionEvaluatorFV2504 {
279    /// [8] Rechenschrittidentifikator aus einem SG8 SEQ+Z37 (Bestandteil des Rechenschritts) DE1050 desselben SG5 IDE+24 und derselben Zeitraum-ID wie bei diesem SG8
280    // REVIEW: Validates that RFF+Z23 references in SG8 SEQ+Z37 instances point to existing SEQ+Z37 DE1050 values. Follows the Example 27 pattern with SG5/SG8 group path. The Zeitraum-ID same-scope constraint and same-SG5 IDE+24 scoping are not fully enforced — cross-SG5 step IDs could be accepted — but within a single-transaction UTILTS message this is typically equivalent. RFF+Z23 is the standard Rechenschrittidentifikator reference qualifier based on Example 27. (medium confidence)
281    fn evaluate_8(&self, ctx: &EvaluationContext) -> ConditionResult {
282        let nav = match ctx.navigator() {
283            Some(n) => n,
284            None => return ConditionResult::Unknown,
285        };
286        let sg8_count = nav.group_instance_count(&["SG5", "SG8"]);
287        let mut valid_ids: std::collections::HashSet<String> = std::collections::HashSet::new();
288        for i in 0..sg8_count {
289            let seq_segs = nav.find_segments_in_group("SEQ", &["SG5", "SG8"], i);
290            for seq in &seq_segs {
291                if seq
292                    .elements
293                    .first()
294                    .and_then(|e| e.first())
295                    .is_some_and(|v| v == "Z37")
296                {
297                    if let Some(val) = seq.elements.get(1).and_then(|e| e.first()) {
298                        if !val.is_empty() {
299                            valid_ids.insert(val.clone());
300                        }
301                    }
302                }
303            }
304        }
305        if valid_ids.is_empty() {
306            return ConditionResult::Unknown;
307        }
308        for i in 0..sg8_count {
309            let rff_segs = nav.find_segments_in_group("RFF", &["SG5", "SG8"], i);
310            for rff in &rff_segs {
311                if rff
312                    .elements
313                    .first()
314                    .and_then(|e| e.first())
315                    .is_some_and(|v| v == "Z23")
316                {
317                    if let Some(ref_val) = rff.elements.first().and_then(|e| e.get(1)) {
318                        if !ref_val.is_empty() && !valid_ids.contains(ref_val) {
319                            return ConditionResult::False;
320                        }
321                    }
322                }
323            }
324        }
325        ConditionResult::True
326    }
327
328    /// [9] Der hier angegebene Rechenschrittidentifikator darf nicht identisch mit dem Rechenschrittidentifikator aus diesem SG8 SEQ+Z37 DE1050 sein
329    // REVIEW: Checks that within each SG8 SEQ+Z37 instance, the RFF+Z23 reference value does not equal the current SG8's own SEQ DE1050 Rechenschrittidentifikator (no self-reference). Iterates all SG8 instances globally and returns False on first self-referential cycle detected. (medium confidence)
330    fn evaluate_9(&self, ctx: &EvaluationContext) -> ConditionResult {
331        let nav = match ctx.navigator() {
332            Some(n) => n,
333            None => return ConditionResult::Unknown,
334        };
335        let sg8_count = nav.group_instance_count(&["SG5", "SG8"]);
336        for i in 0..sg8_count {
337            let seq_segs = nav.find_segments_in_group("SEQ", &["SG5", "SG8"], i);
338            let own_id = seq_segs
339                .iter()
340                .find(|s| {
341                    s.elements
342                        .first()
343                        .and_then(|e| e.first())
344                        .is_some_and(|v| v == "Z37")
345                })
346                .and_then(|s| s.elements.get(1))
347                .and_then(|e| e.first())
348                .filter(|v| !v.is_empty())
349                .cloned();
350            let Some(own_id) = own_id else {
351                continue;
352            };
353            let rff_segs = nav.find_segments_in_group("RFF", &["SG5", "SG8"], i);
354            for rff in &rff_segs {
355                if rff
356                    .elements
357                    .first()
358                    .and_then(|e| e.first())
359                    .is_some_and(|v| v == "Z23")
360                {
361                    if let Some(ref_val) = rff.elements.first().and_then(|e| e.get(1)) {
362                        if ref_val == &own_id {
363                            return ConditionResult::False;
364                        }
365                    }
366                }
367            }
368        }
369        ConditionResult::True
370    }
371
372    /// [11] Wenn in SG8 SEQ+Z37 SG9 CCI+++Z86 CAV+Z69/Z70 (Addition / Subtraktion) vorhanden, darf es in dem Vorgang beliebig viele weitere SG8 SEQ+Z37 mit identischem Rechenschrittidentifikator mit derselben ...
373    // REVIEW: When multiple SG8 SEQ+Z37 share the same Rechenschrittidentifikator (duplicates), all operator CAV codes across those instances must be exclusively Z69 (Addition) or Z70 (Subtraktion). Navigates SG9 children of each SG8 to collect operator codes, groups by step ID, and checks the constraint for duplicates. Zeitraum-ID same-scope matching is not enforced due to API limitations — treats global step_id grouping as approximation. (medium confidence)
374    fn evaluate_11(&self, ctx: &EvaluationContext) -> ConditionResult {
375        let nav = match ctx.navigator() {
376            Some(n) => n,
377            None => return ConditionResult::Unknown,
378        };
379        let sg8_count = nav.group_instance_count(&["SG5", "SG8"]);
380        if sg8_count == 0 {
381            return ConditionResult::Unknown;
382        }
383        // Map step_id -> (instance_count, all_operator_codes_across_all_instances)
384        let mut step_data: std::collections::HashMap<String, (usize, Vec<String>)> =
385            std::collections::HashMap::new();
386        for i in 0..sg8_count {
387            let seq_segs = nav.find_segments_in_group("SEQ", &["SG5", "SG8"], i);
388            let step_id = seq_segs
389                .iter()
390                .find(|s| {
391                    s.elements
392                        .first()
393                        .and_then(|e| e.first())
394                        .is_some_and(|v| v == "Z37")
395                })
396                .and_then(|s| s.elements.get(1))
397                .and_then(|e| e.first())
398                .filter(|v| !v.is_empty())
399                .cloned();
400            let Some(step_id) = step_id else {
401                continue;
402            };
403            let sg9_count = nav.child_group_instance_count(&["SG5", "SG8"], i, "SG9");
404            let mut ops: Vec<String> = Vec::new();
405            for j in 0..sg9_count {
406                let cavs = nav.find_segments_in_child_group("CAV", &["SG5", "SG8"], i, "SG9", j);
407                for cav in &cavs {
408                    if let Some(code) = cav.elements.first().and_then(|e| e.first()) {
409                        if matches!(code.as_str(), "Z69" | "Z70" | "Z80" | "Z81" | "Z82" | "Z83") {
410                            ops.push(code.clone());
411                        }
412                    }
413                }
414            }
415            let entry = step_data.entry(step_id).or_insert((0, Vec::new()));
416            entry.0 += 1;
417            entry.1.extend(ops);
418        }
419        // HAND-EDITED: [11] is the step sharing its Rechenschrittidentifikator
420        // with further SEQ+Z37 — the case `X [11] ⊻ [15]` (Z69) sets against the
421        // step standing alone ([15]), as [12] does for Z83. It holds where the
422        // identifier occurs more than once and every operator of those steps
423        // is Z69/Z70: the step of the enclosing SG8, or any step without one.
424        let shared = |count: usize, operators: &[String]| {
425            ConditionResult::from(
426                count > 1 && operators.iter().all(|op| op == "Z69" || op == "Z70"),
427            )
428        };
429        if let Some(sg8) = ctx.enclosing_scope("SG8") {
430            let own = sg8.last().map(|(_, i)| *i).unwrap_or_default();
431            let own_id = nav
432                .find_segments_in_group("SEQ", &["SG5", "SG8"], own)
433                .iter()
434                .find(|s| s.get_element(0) == "Z37")
435                .map(|s| s.get_component(1, 0).to_string())
436                .filter(|v| !v.is_empty());
437            return match own_id.and_then(|id| step_data.get(&id)) {
438                Some((count, operators)) => shared(*count, operators),
439                None => ConditionResult::Unknown,
440            };
441        }
442        if step_data
443            .values()
444            .any(|(count, operators)| shared(*count, operators).is_true())
445        {
446            return ConditionResult::True;
447        }
448        ConditionResult::False
449    }
450
451    /// [12] Wenn in SG8 SEQ+Z37 SG9 CCI+++Z86 CAV+Z83 (Positivwert) vorhanden, darf es in dem Vorgang keine weitere SG8 SEQ+Z37 mit identischem Rechenschrittidentifikator und derselben Zeitraum-ID geben
452    // REVIEW: When a calculation step SG8 SEQ+Z37 contains the Z83 (Positivwert) operator in its SG9 CAV, no other SG8 SEQ+Z37 may share the same Rechenschrittidentifikator within the Vorgang. Groups step IDs by count and checks that any step with Z83 appears exactly once. Zeitraum-ID scoping approximated by global step_id grouping. (medium confidence)
453    fn evaluate_12(&self, ctx: &EvaluationContext) -> ConditionResult {
454        let nav = match ctx.navigator() {
455            Some(n) => n,
456            None => return ConditionResult::Unknown,
457        };
458        let sg8_count = nav.group_instance_count(&["SG5", "SG8"]);
459        if sg8_count == 0 {
460            return ConditionResult::Unknown;
461        }
462        // Map step_id -> (instance_count, has_z83_operator)
463        let mut step_data: std::collections::HashMap<String, (usize, bool)> =
464            std::collections::HashMap::new();
465        for i in 0..sg8_count {
466            let seq_segs = nav.find_segments_in_group("SEQ", &["SG5", "SG8"], i);
467            let step_id = seq_segs
468                .iter()
469                .find(|s| {
470                    s.elements
471                        .first()
472                        .and_then(|e| e.first())
473                        .is_some_and(|v| v == "Z37")
474                })
475                .and_then(|s| s.elements.get(1))
476                .and_then(|e| e.first())
477                .filter(|v| !v.is_empty())
478                .cloned();
479            let Some(step_id) = step_id else {
480                continue;
481            };
482            let sg9_count = nav.child_group_instance_count(&["SG5", "SG8"], i, "SG9");
483            let mut has_z83 = false;
484            for j in 0..sg9_count {
485                let cavs = nav.find_segments_in_child_group("CAV", &["SG5", "SG8"], i, "SG9", j);
486                if cavs.iter().any(|c| {
487                    c.elements
488                        .first()
489                        .and_then(|e| e.first())
490                        .is_some_and(|v| v == "Z83")
491                }) {
492                    has_z83 = true;
493                }
494            }
495            let entry = step_data.entry(step_id).or_insert((0, false));
496            entry.0 += 1;
497            entry.1 |= has_z83;
498        }
499        // Rule: if Z83 (Positivwert) operator is present for a step_id, that step_id must be unique
500        for (_step_id, (count, has_z83)) in &step_data {
501            if *has_z83 && *count > 1 {
502                return ConditionResult::False;
503            }
504        }
505        ConditionResult::True
506    }
507
508    /// [25] Wenn MP-ID in SG2 NAD+MR (Nachrichtenempfänger) in der Rolle LF
509    /// EXTERNAL: Requires context from outside the message.
510    fn evaluate_25(&self, ctx: &EvaluationContext) -> ConditionResult {
511        ctx.external.evaluate("recipient_is_lf")
512    }
513
514    /// [26] sofern per ORDERS reklamiert
515    /// EXTERNAL: Requires context from outside the message.
516    // REVIEW: Cannot be determined from EDIFACT message content alone — requires external knowledge of whether a prior ORDERS message was used to make a claim. Depends on business process context outside the current message. (medium confidence)
517    fn evaluate_26(&self, ctx: &EvaluationContext) -> ConditionResult {
518        ctx.external.evaluate("claimed_via_orders")
519    }
520
521    /// [37] Wenn ein Gültigkeitsende bereits angegeben werden kann.
522    /// EXTERNAL: Requires context from outside the message.
523    // REVIEW: Whether a validity end date can already be specified is an organizational business decision at message creation time. Checking DTM+Z35 presence would be circular (condition guards whether to include that very field). Must be resolved from business context external to the message. (medium confidence)
524    fn evaluate_37(&self, ctx: &EvaluationContext) -> ConditionResult {
525        ctx.external.evaluate("validity_end_known")
526    }
527
528    /// [42] Der in diesem Datenlement angegebene Code der Schaltzeitdefinition muss innerhalb eines Vorgangs (IDE) eindeutig sein.
529    // REVIEW: Uniqueness of Schaltzeitdefinition codes within an IDE (SG5 group). The only Z44-keyed element in the provided SG8 schema is DTM+Z44 (Schaltzeitänderungszeitpunkt), so uniqueness is enforced on the DTM value within each SG5 instance. Medium confidence because the schema reference only shows DTM segments for SG8 — the actual 'Code' data element could reside in a CCI/SEQ segment not shown here. (medium confidence)
530    fn evaluate_42(&self, ctx: &EvaluationContext) -> ConditionResult {
531        let nav = match ctx.navigator() {
532            Some(n) => n,
533            None => return ConditionResult::Unknown,
534        };
535        let sg5_count = nav.group_instance_count(&["SG5"]);
536        for i in 0..sg5_count {
537            let sg8_count = nav.child_group_instance_count(&["SG5"], i, "SG8");
538            let mut seen = std::collections::HashSet::new();
539            for j in 0..sg8_count {
540                let dtms = nav.find_segments_in_child_group("DTM", &["SG5"], i, "SG8", j);
541                for dtm in &dtms {
542                    if dtm
543                        .elements
544                        .first()
545                        .and_then(|e| e.first())
546                        .is_some_and(|v| v == "Z44")
547                    {
548                        if let Some(val) = dtm.elements.first().and_then(|e| e.get(1)) {
549                            if !val.is_empty() && !seen.insert(val.clone()) {
550                                return ConditionResult::False;
551                            }
552                        }
553                    }
554                }
555            }
556        }
557        ConditionResult::True
558    }
559
560    /// [43] Der in diesem Datenlement angegebene Code der Leistungskurvendefinition muss innerhalb eines Vorgangs (IDE) eindeutig sein.
561    // REVIEW: Uniqueness of Leistungskurvendefinition codes within an IDE (SG5 group). Mirrors condition 42 but targets DTM+Z45 (Leistungskurvenänderungszeitpunkt). Same caveat: the actual 'Code' element might live in a CCI/SEQ segment absent from the provided schema reference — medium confidence. (medium confidence)
562    fn evaluate_43(&self, ctx: &EvaluationContext) -> ConditionResult {
563        let nav = match ctx.navigator() {
564            Some(n) => n,
565            None => return ConditionResult::Unknown,
566        };
567        let sg5_count = nav.group_instance_count(&["SG5"]);
568        for i in 0..sg5_count {
569            let sg8_count = nav.child_group_instance_count(&["SG5"], i, "SG8");
570            let mut seen = std::collections::HashSet::new();
571            for j in 0..sg8_count {
572                let dtms = nav.find_segments_in_child_group("DTM", &["SG5"], i, "SG8", j);
573                for dtm in &dtms {
574                    if dtm
575                        .elements
576                        .first()
577                        .and_then(|e| e.first())
578                        .is_some_and(|v| v == "Z45")
579                    {
580                        if let Some(val) = dtm.elements.first().and_then(|e| e.get(1)) {
581                            if !val.is_empty() && !seen.insert(val.clone()) {
582                                return ConditionResult::False;
583                            }
584                        }
585                    }
586                }
587            }
588        }
589        ConditionResult::True
590    }
591
592    /// [56] Wenn dieses DTM+Z25 (Verwendung der Daten ab) im SG6 RFF (Verwendungszeitraum der Daten) mit der Zeitraum ID "1" im DE1156 ist, muss das Datum der darauffolgende oder ein älterer Tag 0:00 Uhr deut...
593    // REVIEW: Checks that DTM+Z25 (Verwendung der Daten ab) in any SG6 whose associated RFF has Zeitraum-ID '1' in DE1156 (C506 component [0][2]) is at most the day following DTM+137 (message date) at 0:00. Date arithmetic is implemented inline without external crates. Timezone nuance ('0:00 Uhr deutscher Zeit' = CET/CEST midnight, not UTC midnight) is approximated — threshold uses CCYYMMDD+10000 of the next calendar day in UTC, which introduces up to 2-hour margin of error around DST transitions. The Zeitraum-ID '1' is checked at elements[0][2] per the explicit DE1156 reference in the condition text. (medium confidence)
594    fn evaluate_56(&self, ctx: &EvaluationContext) -> ConditionResult {
595        let msg_dtm_segs = ctx.find_segments_with_qualifier("DTM", 0, "137");
596        let msg_date_val = match msg_dtm_segs
597            .first()
598            .and_then(|s| s.elements.first())
599            .and_then(|e| e.get(1))
600        {
601            Some(v) => v.clone(),
602            None => return ConditionResult::Unknown,
603        };
604        if msg_date_val.len() < 8 {
605            return ConditionResult::Unknown;
606        }
607        let year: u32 = match msg_date_val[..4].parse::<u32>() {
608            Ok(v) => v,
609            Err(_) => return ConditionResult::Unknown,
610        };
611        let month: u32 = match msg_date_val[4..6].parse::<u32>() {
612            Ok(v) => v,
613            Err(_) => return ConditionResult::Unknown,
614        };
615        let day: u32 = match msg_date_val[6..8].parse::<u32>() {
616            Ok(v) => v,
617            Err(_) => return ConditionResult::Unknown,
618        };
619        let days_in_month: u32 = match month {
620            1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
621            4 | 6 | 9 | 11 => 30,
622            2 => {
623                if year % 400 == 0 || (year % 4 == 0 && year % 100 != 0) {
624                    29
625                } else {
626                    28
627                }
628            }
629            _ => return ConditionResult::Unknown,
630        };
631        let (ny, nm, nd): (u32, u32, u32) = if day >= days_in_month {
632            if month == 12 {
633                (year + 1, 1, 1)
634            } else {
635                (year, month + 1, 1)
636            }
637        } else {
638            (year, month, day + 1)
639        };
640        let threshold = format!("{:04}{:02}{:02}0000", ny, nm, nd);
641        let nav = match ctx.navigator() {
642            Some(n) => n,
643            None => return ConditionResult::Unknown,
644        };
645        let sg5_count = nav.group_instance_count(&["SG5"]);
646        for i in 0..sg5_count {
647            let sg6_count = nav.child_group_instance_count(&["SG5"], i, "SG6");
648            for j in 0..sg6_count {
649                let rffs = nav.find_segments_in_child_group("RFF", &["SG5"], i, "SG6", j);
650                let has_zeitraum_1 = rffs.iter().any(|rff| {
651                    rff.elements
652                        .first()
653                        .and_then(|e| e.get(2))
654                        .is_some_and(|v| v == "1")
655                });
656                if has_zeitraum_1 {
657                    let dtms = nav.find_segments_in_child_group("DTM", &["SG5"], i, "SG6", j);
658                    for dtm in &dtms {
659                        if dtm
660                            .elements
661                            .first()
662                            .and_then(|e| e.first())
663                            .is_some_and(|v| v == "Z25")
664                        {
665                            if let Some(dtm_val) = dtm.elements.first().and_then(|e| e.get(1)) {
666                                if dtm_val.as_str() > threshold.as_str() {
667                                    return ConditionResult::False;
668                                }
669                            }
670                        }
671                    }
672                }
673            }
674        }
675        ConditionResult::True
676    }
677
678    /// [57] Wenn dieses DTM+Z25 (Verwendung der Daten ab) nicht im SG6 RFF+Z49/ Z53 (Verwendungszeitraum der Daten: Gültige Daten/ Keine Daten) mit der Zeitraum ID "1" im DE1156 ist, muss das Datum dem DTM+Z2...
679    // REVIEW: Complex cross-SG6 ordering condition: for each SG6 with Zeitraum-ID != 1, its DTM+Z25 must equal the DTM+Z26 of the SG6 with the next lower Zeitraum-ID. Implemented via navigator collecting (id, z25, z26) tuples per SG6 instance, then verifying the sequential boundary constraint. (medium confidence)
680    fn evaluate_57(&self, ctx: &EvaluationContext) -> ConditionResult {
681        let nav = match ctx.navigator() {
682            Some(n) => n,
683            None => return ConditionResult::Unknown,
684        };
685        let sg6_path: &[&str] = &["SG5", "SG6"];
686        let sg6_count = nav.group_instance_count(sg6_path);
687        if sg6_count == 0 {
688            return ConditionResult::Unknown;
689        }
690        // Build (zeitraum_id, dtm_z25, dtm_z26) per SG6
691        let mut entries: Vec<(u32, Option<String>, Option<String>)> = Vec::new();
692        for i in 0..sg6_count {
693            let rff_segs = nav.find_segments_in_group("RFF", sg6_path, i);
694            let mut zeitraum_id: Option<u32> = None;
695            for rff in &rff_segs {
696                let qual = rff
697                    .elements
698                    .first()
699                    .and_then(|e| e.first())
700                    .map(|s| s.as_str());
701                if matches!(qual, Some("Z49") | Some("Z53")) {
702                    if let Some(id_str) = rff.elements.first().and_then(|e| e.get(2)) {
703                        if let Ok(id) = id_str.parse::<u32>() {
704                            zeitraum_id = Some(id);
705                        }
706                    }
707                }
708            }
709            let id = match zeitraum_id {
710                Some(id) => id,
711                None => continue,
712            };
713            let dtm_segs = nav.find_segments_in_group("DTM", sg6_path, i);
714            let mut dtm_z25: Option<String> = None;
715            let mut dtm_z26: Option<String> = None;
716            for dtm in &dtm_segs {
717                let qual = dtm
718                    .elements
719                    .first()
720                    .and_then(|e| e.first())
721                    .map(|s| s.as_str());
722                let val = dtm
723                    .elements
724                    .first()
725                    .and_then(|e| e.get(1))
726                    .filter(|v| !v.is_empty())
727                    .cloned();
728                match qual {
729                    Some("Z25") => dtm_z25 = val,
730                    Some("Z26") => dtm_z26 = val,
731                    _ => {}
732                }
733            }
734            entries.push((id, dtm_z25, dtm_z26));
735        }
736        // For non-ID-1 entries, check DTM+Z25 == DTM+Z26 of next-lower Zeitraum-ID
737        for (id, dtm_z25, _) in &entries {
738            if *id == 1 {
739                continue;
740            }
741            let current_z25 = match dtm_z25 {
742                Some(v) => v,
743                None => continue,
744            };
745            let next_lower = entries
746                .iter()
747                .filter(|(other_id, _, _)| *other_id < *id)
748                .map(|(other_id, _, _)| *other_id)
749                .max();
750            let lower_id = match next_lower {
751                Some(lid) => lid,
752                None => return ConditionResult::Unknown,
753            };
754            match entries.iter().find(|(eid, _, _)| *eid == lower_id) {
755                Some((_, _, Some(lower_z26))) => {
756                    if current_z25 != lower_z26 {
757                        return ConditionResult::False;
758                    }
759                }
760                Some((_, _, None)) => return ConditionResult::Unknown,
761                None => return ConditionResult::Unknown,
762            }
763        }
764        ConditionResult::True
765    }
766
767    /// [490] wenn Wert in diesem DE, an der Stelle CCYYMMDD ein Datum aus dem angegeben Zeitraum der Tabelle Kapitel 3.5 „Prozesszeitpunkt bei MESZ mit UTC“ ist
768    fn evaluate_490(&self, ctx: &EvaluationContext) -> ConditionResult {
769        let dtm_segs = ctx.find_segments_with_qualifier("DTM", 0, "137");
770        match dtm_segs
771            .first()
772            .and_then(|s| s.elements.first())
773            .and_then(|e| e.get(1))
774        {
775            Some(val) => is_mesz_utc(val),
776            None => ConditionResult::False, // segment absent → condition not applicable
777        }
778    }
779
780    /// [491] wenn Wert in diesem DE, an der Stelle CCYYMMDD ein Datum aus dem angegeben Zeitraum der Tabelle Kapitel 3.6 „Prozesszeitpunkt bei MEZ mit UTC“ ist
781    fn evaluate_491(&self, ctx: &EvaluationContext) -> ConditionResult {
782        let dtm_segs = ctx.find_segments_with_qualifier("DTM", 0, "137");
783        match dtm_segs
784            .first()
785            .and_then(|s| s.elements.first())
786            .and_then(|e| e.get(1))
787        {
788            Some(val) => is_mez_utc(val),
789            None => ConditionResult::False, // segment absent → condition not applicable
790        }
791    }
792
793    /// [1] Nur MP-ID aus Sparte Strom
794    /// EXTERNAL: Requires context from outside the message.
795    // REVIEW: Whether the MP-ID belongs to the electricity sector (Sparte Strom) cannot be determined from the EDIFACT message alone — requires external market participant registry lookup. (medium confidence)
796    fn evaluate_1(&self, ctx: &EvaluationContext) -> ConditionResult {
797        ctx.external.evaluate("mp_id_is_strom_sector")
798    }
799
800    /// [2] Wenn SG5 STS+Z23+Z34 (Berechnungsformel muss beim Absender angefragt werden) in einem SG5 IDE vorhanden
801    fn evaluate_2(&self, ctx: &EvaluationContext) -> ConditionResult {
802        ctx.any_group_has_qualified_value("STS", 0, "Z23", 1, 0, &["Z34"], &["SG5"])
803    }
804
805    /// [5] Wenn das SG8 RFF+Z19 (Referenz auf eine Messlokation) in derselben SG8 SEQ+Z37 nicht vorhanden
806    fn evaluate_5(&self, ctx: &EvaluationContext) -> ConditionResult {
807        // HAND-EDITED: "RFF+Z19 in derselben SG8 SEQ+Z37 nicht vorhanden" — the enclosing SG8 only, when the scope
808        // reaches one; the message as before otherwise.
809        if let Some(scope) = ctx.enclosing_scope("SG8") {
810            return ConditionResult::from(
811                ctx.find_segments_at(scope, "SEQ")
812                    .iter()
813                    .any(|s| s.get_element(0) == "Z37")
814                    && !ctx
815                        .find_segments_at(scope, "RFF")
816                        .iter()
817                        .any(|s| s.get_element(0) == "Z19"),
818            );
819        }
820        ctx.any_group_has_qualifier_without("RFF", 0, "Z19", "SEQ", 0, "Z37", &["SG4", "SG8"])
821    }
822
823    /// [6] Wenn das SG8 RFF+Z23 (Referenz auf Rechenschritt) in derselben SG8 SEQ+Z37 nicht vorhanden
824    fn evaluate_6(&self, ctx: &EvaluationContext) -> ConditionResult {
825        // HAND-EDITED: "RFF+Z23 in derselben SG8 SEQ+Z37 nicht vorhanden" — the enclosing SG8 only, when the scope
826        // reaches one; the message as before otherwise.
827        if let Some(scope) = ctx.enclosing_scope("SG8") {
828            return ConditionResult::from(
829                ctx.find_segments_at(scope, "SEQ")
830                    .iter()
831                    .any(|s| s.get_element(0) == "Z37")
832                    && !ctx
833                        .find_segments_at(scope, "RFF")
834                        .iter()
835                        .any(|s| s.get_element(0) == "Z23"),
836            );
837        }
838        ctx.any_group_has_qualifier_without("RFF", 0, "Z23", "SEQ", 0, "Z37", &["SG4", "SG8"])
839    }
840
841    /// [7] Wenn in derselben SG8 SEQ+Z37 das SG8 RFF+Z19 (Referenz auf eine Messlokation) vorhanden
842    fn evaluate_7(&self, ctx: &EvaluationContext) -> ConditionResult {
843        // HAND-EDITED: "in derselben SG8 SEQ+Z37 das SG8 RFF+Z19 vorhanden" — the enclosing SG8 only, when the scope
844        // reaches one; the message as before otherwise.
845        if let Some(scope) = ctx.enclosing_scope("SG8") {
846            return ConditionResult::from(
847                ctx.find_segments_at(scope, "SEQ")
848                    .iter()
849                    .any(|s| s.get_element(0) == "Z37")
850                    && ctx
851                        .find_segments_at(scope, "RFF")
852                        .iter()
853                        .any(|s| s.get_element(0) == "Z19"),
854            );
855        }
856        ctx.any_group_has_co_occurrence("SEQ", 0, &["Z37"], "RFF", 0, 0, &["Z19"], &["SG4", "SG8"])
857    }
858
859    /// [10] wenn vorhanden
860    fn evaluate_10(&self, _ctx: &EvaluationContext) -> ConditionResult {
861        // "wenn vorhanden" — conditional modifier meaning the associated rule applies
862        // only when the element is present. As a standalone boolean predicate this
863        // always evaluates to True: the condition itself imposes no additional
864        // constraint beyond the field's own optionality.
865        ConditionResult::True
866    }
867
868    /// [13] Wenn in SG8 SEQ+Z37 SG9 CCI+++Z86 CAV+Z80/Z81 (Divisor / Dividend) vorhanden, muss in diesem Vorgang genau eine zweite SG8 SEQ+Z37 mit identischen Rechenschrittidentifikator und derselben Zeitraum-...
869    // REVIEW: Complex cross-SG8 pairing invariant: for each Z80 (Divisor) or Z81 (Dividend) operator in SG9 CCI+Z86, there must be exactly one matching SG8 with the same Rechenschrittidentifikator (SEQ.elements[1][0]) and Zeitraum-ID (RFF+Z46) that carries the complementary operator. Requires full navigator traversal. (medium confidence)
870    fn evaluate_13(&self, ctx: &EvaluationContext) -> ConditionResult {
871        {
872            let nav = match ctx.navigator() {
873                Some(n) => n,
874                None => return ConditionResult::Unknown,
875            };
876
877            let sg8_count = nav.group_instance_count(&["SG5", "SG8"]);
878            // Collect (rs_id, zt_id, operator) for all Z37 SG8s that have CCI+++Z86 and CAV+Z80/Z81
879            let mut div_instances: Vec<(String, String, String)> = Vec::new();
880
881            for i in 0..sg8_count {
882                let seqs = nav.find_segments_in_group("SEQ", &["SG5", "SG8"], i);
883                let seq_z37 = seqs.iter().find(|s| {
884                    s.elements
885                        .first()
886                        .and_then(|e| e.first())
887                        .is_some_and(|v| v == "Z37")
888                });
889                let seq_z37 = match seq_z37 {
890                    Some(s) => s,
891                    None => continue,
892                };
893
894                let rs_id = seq_z37
895                    .elements
896                    .get(1)
897                    .and_then(|e| e.first())
898                    .cloned()
899                    .unwrap_or_default();
900
901                let rffs = nav.find_segments_in_group("RFF", &["SG5", "SG8"], i);
902                let zt_id = rffs
903                    .iter()
904                    .find(|s| {
905                        s.elements
906                            .first()
907                            .and_then(|e| e.first())
908                            .is_some_and(|v| v == "Z46")
909                    })
910                    .and_then(|s| s.elements.first())
911                    .and_then(|e| e.get(1))
912                    .cloned()
913                    .unwrap_or_default();
914
915                let sg9_count = nav.child_group_instance_count(&["SG5", "SG8"], i, "SG9");
916                for j in 0..sg9_count {
917                    let ccis =
918                        nav.find_segments_in_child_group("CCI", &["SG5", "SG8"], i, "SG9", j);
919                    let has_z86 = ccis.iter().any(|s| {
920                        s.elements
921                            .get(2)
922                            .and_then(|e| e.first())
923                            .is_some_and(|v| v == "Z86")
924                    });
925                    if !has_z86 {
926                        continue;
927                    }
928                    let cavs =
929                        nav.find_segments_in_child_group("CAV", &["SG5", "SG8"], i, "SG9", j);
930                    for cav in &cavs {
931                        let op = cav
932                            .elements
933                            .first()
934                            .and_then(|e| e.first())
935                            .map(|s| s.as_str());
936                        if matches!(op, Some("Z80") | Some("Z81")) {
937                            div_instances.push((
938                                rs_id.clone(),
939                                zt_id.clone(),
940                                op.unwrap().to_string(),
941                            ));
942                        }
943                    }
944                }
945            }
946
947            if div_instances.is_empty() {
948                return ConditionResult::Unknown;
949            }
950
951            // Each Z80 must have exactly one Z81 counterpart with same IDs, and vice versa
952            for (rs_id, zt_id, op) in &div_instances {
953                let counterpart = if op == "Z80" { "Z81" } else { "Z80" };
954                let count = div_instances
955                    .iter()
956                    .filter(|(rs, zt, o)| rs == rs_id && zt == zt_id && o == counterpart)
957                    .count();
958                if count != 1 {
959                    return ConditionResult::False;
960                }
961            }
962
963            ConditionResult::True
964        }
965    }
966
967    /// [14] Wenn in SG8 SEQ+Z37 SG9 CCI+++Z86 CAV+Z82 (Faktor) vorhanden, darf es in dem Vorgang beliebig viele weitere SG8 SEQ+Z37 mit identischem Rechenschrittidentifikator und derselben Zeitraum-ID geben, d...
968    // REVIEW: When a Z82 (Faktor/Multiplikation) operator is present in a group of SG8s sharing the same Rechenschrittidentifikator and Zeitraum-ID, ALL members of that group must exclusively carry Z82 — no mixing with Z80/Z81. Returns True when this homogeneity constraint holds. (medium confidence)
969    fn evaluate_14(&self, ctx: &EvaluationContext) -> ConditionResult {
970        {
971            let nav = match ctx.navigator() {
972                Some(n) => n,
973                None => return ConditionResult::Unknown,
974            };
975
976            let sg8_count = nav.group_instance_count(&["SG5", "SG8"]);
977            // Collect (rs_id, zt_id, operators) for all Z37 SG8s with CCI+++Z86
978            let mut instances: Vec<(String, String, Vec<String>)> = Vec::new();
979
980            for i in 0..sg8_count {
981                let seqs = nav.find_segments_in_group("SEQ", &["SG5", "SG8"], i);
982                let seq_z37 = seqs.iter().find(|s| {
983                    s.elements
984                        .first()
985                        .and_then(|e| e.first())
986                        .is_some_and(|v| v == "Z37")
987                });
988                let seq_z37 = match seq_z37 {
989                    Some(s) => s,
990                    None => continue,
991                };
992
993                let rs_id = seq_z37
994                    .elements
995                    .get(1)
996                    .and_then(|e| e.first())
997                    .cloned()
998                    .unwrap_or_default();
999
1000                let rffs = nav.find_segments_in_group("RFF", &["SG5", "SG8"], i);
1001                let zt_id = rffs
1002                    .iter()
1003                    .find(|s| {
1004                        s.elements
1005                            .first()
1006                            .and_then(|e| e.first())
1007                            .is_some_and(|v| v == "Z46")
1008                    })
1009                    .and_then(|s| s.elements.first())
1010                    .and_then(|e| e.get(1))
1011                    .cloned()
1012                    .unwrap_or_default();
1013
1014                let sg9_count = nav.child_group_instance_count(&["SG5", "SG8"], i, "SG9");
1015                let mut ops: Vec<String> = Vec::new();
1016                for j in 0..sg9_count {
1017                    let ccis =
1018                        nav.find_segments_in_child_group("CCI", &["SG5", "SG8"], i, "SG9", j);
1019                    if !ccis.iter().any(|s| {
1020                        s.elements
1021                            .get(2)
1022                            .and_then(|e| e.first())
1023                            .is_some_and(|v| v == "Z86")
1024                    }) {
1025                        continue;
1026                    }
1027                    let cavs =
1028                        nav.find_segments_in_child_group("CAV", &["SG5", "SG8"], i, "SG9", j);
1029                    for cav in &cavs {
1030                        if let Some(op) = cav.elements.first().and_then(|e| e.first()) {
1031                            ops.push(op.clone());
1032                        }
1033                    }
1034                }
1035                if !ops.is_empty() {
1036                    instances.push((rs_id, zt_id, ops));
1037                }
1038            }
1039
1040            // Find any (rs_id, zt_id) key that has a Z82 operator
1041            let z82_keys: Vec<(&str, &str)> = instances
1042                .iter()
1043                .filter(|(_, _, ops)| ops.iter().any(|op| op == "Z82"))
1044                .map(|(rs, zt, _)| (rs.as_str(), zt.as_str()))
1045                .collect();
1046
1047            if z82_keys.is_empty() {
1048                return ConditionResult::Unknown;
1049            }
1050
1051            // For each group keyed by (rs_id, zt_id) that has Z82, all operators must be exclusively Z82
1052            for (rs_id, zt_id) in &z82_keys {
1053                for (rs, zt, ops) in &instances {
1054                    if rs.as_str() == *rs_id && zt.as_str() == *zt_id {
1055                        if ops.iter().any(|op| op != "Z82") {
1056                            return ConditionResult::False;
1057                        }
1058                    }
1059                }
1060            }
1061
1062            ConditionResult::True
1063        }
1064    }
1065
1066    /// [15] Wenn in einem SG5 IDE+24 nur eine SEQ+Z37 mit einer SG8 RFF+Z19 (Messlokation) und der selben Zeitraum-ID vorhanden ist
1067    // REVIEW: Checks that within the current SG5 (IDE+24) transaction group there is exactly one SG8 that contains both SEQ+Z37 (Bestandteil des Rechenschritts) and RFF+Z19 (Messlokation reference), and that all such matching SG8s reference the same Zeitraum-ID via RFF+Z46. (medium confidence)
1068    fn evaluate_15(&self, ctx: &EvaluationContext) -> ConditionResult {
1069        {
1070            let nav = match ctx.navigator() {
1071                Some(n) => n,
1072                None => return ConditionResult::Unknown,
1073            };
1074
1075            let sg8_count = nav.group_instance_count(&["SG5", "SG8"]);
1076            let mut matching_count = 0usize;
1077            let mut first_zt_id: Option<String> = None;
1078
1079            for i in 0..sg8_count {
1080                let seqs = nav.find_segments_in_group("SEQ", &["SG5", "SG8"], i);
1081                let has_z37 = seqs.iter().any(|s| {
1082                    s.elements
1083                        .first()
1084                        .and_then(|e| e.first())
1085                        .is_some_and(|v| v == "Z37")
1086                });
1087                if !has_z37 {
1088                    continue;
1089                }
1090
1091                let rffs = nav.find_segments_in_group("RFF", &["SG5", "SG8"], i);
1092                let has_z19 = rffs.iter().any(|s| {
1093                    s.elements
1094                        .first()
1095                        .and_then(|e| e.first())
1096                        .is_some_and(|v| v == "Z19")
1097                });
1098                if !has_z19 {
1099                    continue;
1100                }
1101
1102                // Verify Zeitraum-ID consistency across all matching instances
1103                let zt_id = rffs
1104                    .iter()
1105                    .find(|s| {
1106                        s.elements
1107                            .first()
1108                            .and_then(|e| e.first())
1109                            .is_some_and(|v| v == "Z46")
1110                    })
1111                    .and_then(|s| s.elements.first())
1112                    .and_then(|e| e.get(1))
1113                    .cloned();
1114
1115                if let Some(ref zt) = zt_id {
1116                    match &first_zt_id {
1117                        None => first_zt_id = Some(zt.clone()),
1118                        Some(first) => {
1119                            if first != zt {
1120                                return ConditionResult::False;
1121                            }
1122                        }
1123                    }
1124                }
1125
1126                matching_count += 1;
1127            }
1128
1129            ConditionResult::from(matching_count == 1)
1130        }
1131    }
1132
1133    /// [21] Wenn in dieser CAV+ZD3 der Wert im DE7110 mit Z32 (sonstiger Zählzeitdefinitionstyp) vorhanden ist
1134    fn evaluate_21(&self, ctx: &EvaluationContext) -> ConditionResult {
1135        ctx.has_qualified_value("CAV", 0, "ZD3", 0, 3, &["Z32"])
1136    }
1137
1138    /// [22] Wenn MP-ID in SG2 NAD+MS (Nachrichtenabsender) in der Rolle NB
1139    /// EXTERNAL: Requires context from outside the message.
1140    // REVIEW: Whether the MP-ID in NAD+MS (Nachrichtenabsender) holds the role of NB (Netzbetreiber) cannot be determined from the EDIFACT message alone — requires external market participant role registry. (medium confidence)
1141    fn evaluate_22(&self, ctx: &EvaluationContext) -> ConditionResult {
1142        ctx.external.evaluate("sender_is_nb")
1143    }
1144
1145    /// [24] Wenn SG5 STS+Z36+Z45 (Definitionen werden verwendet) vorhanden
1146    fn evaluate_24(&self, ctx: &EvaluationContext) -> ConditionResult {
1147        ctx.has_qualified_value("STS", 0, "Z36", 1, 0, &["Z45"])
1148    }
1149
1150    /// [27] Wenn in SG9 CAV+ZD4+Z26 (keine Verwendung des Hochlastzeitfensters) vorhanden
1151    fn evaluate_27(&self, ctx: &EvaluationContext) -> ConditionResult {
1152        ctx.has_qualified_value("CAV", 0, "ZD4", 0, 3, &["Z26"])
1153    }
1154
1155    /// [29] Wenn in SG8 SEQ+Z43 DTM+Z33 (Zählzeitänderungszeitpunkt) im DE2379 der Code 303 vorhanden
1156    // REVIEW: In the same SG8, SEQ+Z43 (Zählzeitdefinition) must be present and DTM must have format code 303 at elements[0][2]. In a SEQ+Z43 SG8, the only DTM present is DTM+Z33, so checking format code 303 without qualifying by Z33 is safe. (medium confidence)
1157    fn evaluate_29(&self, ctx: &EvaluationContext) -> ConditionResult {
1158        ctx.any_group_has_co_occurrence("SEQ", 0, &["Z43"], "DTM", 0, 2, &["303"], &["SG5", "SG8"])
1159    }
1160
1161    /// [30] Der Wert von CCYY in diesem DE muss genau um eins höher sein, als der Wert CCYY des SG5 DTM+Z34 (Gültigkeitsbeginn) DE2380
1162    // REVIEW: Compares CCYY of DTM+Z35 (Gültigkeitsende) to CCYY of DTM+Z34 (Gültigkeitsbeginn), checking that the end year is exactly start year + 1. 'Diesem DE' most plausibly refers to DTM+Z35 since it is the natural counterpart to DTM+Z34 and its year is constrained relative to Z34. (medium confidence)
1163    fn evaluate_30(&self, ctx: &EvaluationContext) -> ConditionResult {
1164        // CCYY in this DE must be exactly one higher than CCYY of SG5 DTM+Z34 DE2380
1165        // This applies to DTM+Z35 (Gültigkeitsende) whose year must be Z34 year + 1
1166        let z34_segs = ctx.find_segments_with_qualifier("DTM", 0, "Z34");
1167        let z35_segs = ctx.find_segments_with_qualifier("DTM", 0, "Z35");
1168        let z34_year = z34_segs
1169            .first()
1170            .and_then(|s| s.elements.first())
1171            .and_then(|e| e.get(1))
1172            .and_then(|v| v.get(..4))
1173            .and_then(|y| y.parse::<u32>().ok());
1174        let this_year = z35_segs
1175            .first()
1176            .and_then(|s| s.elements.first())
1177            .and_then(|e| e.get(1))
1178            .and_then(|v| v.get(..4))
1179            .and_then(|y| y.parse::<u32>().ok());
1180        match (z34_year, this_year) {
1181            (Some(start), Some(end)) => ConditionResult::from(end == start + 1),
1182            _ => ConditionResult::Unknown,
1183        }
1184    }
1185
1186    /// [31] Wenn im DE2379 dieses Segments der Code 303 vorhanden
1187    // REVIEW: Checks if any DTM segment has format code 303 at elements[0][2] (DE2379). Applies to DTM+Z33/Z44/Z45 which can carry format codes 303 or 401. Message-wide check is sufficient since these are the only DTMs with variable format codes. (medium confidence)
1188    fn evaluate_31(&self, ctx: &EvaluationContext) -> ConditionResult {
1189        ConditionResult::from(ctx.find_segments("DTM").iter().any(|s| {
1190            s.elements
1191                .first()
1192                .and_then(|e| e.get(2))
1193                .map(|v| v == "303")
1194                .unwrap_or(false)
1195        }))
1196    }
1197
1198    /// [32] Der Zeitpunkt in diesem DE muss ≥ dem Zeitpunkt aus dem DE2380 des Gültigkeitsbeginn der ausgerollten Definition (SG5 DTM+Z34) sein
1199    // REVIEW: Validates that SG8 change timestamps (Z33/Z44/Z45) are >= the SG5 DTM+Z34 validity start. 'Diesem DE' refers to the change-point timestamp being validated in SG8. First 12 characters (YYYYMMDDHHmm) are used for consistent lexicographic date comparison in format 303. (medium confidence)
1200    fn evaluate_32(&self, ctx: &EvaluationContext) -> ConditionResult {
1201        // Timestamp in this DE >= DTM+Z34 (Gültigkeitsbeginn der ausgerollten Definition)
1202        // Applies to SG8 change-point timestamps (Z33 Zählzeit, Z44 Schaltzeit, Z45 Leistungskurve)
1203        let z34_segs = ctx.find_segments_with_qualifier("DTM", 0, "Z34");
1204        let z34_value = match z34_segs
1205            .first()
1206            .and_then(|s| s.elements.first())
1207            .and_then(|e| e.get(1))
1208        {
1209            Some(v) => v.clone(),
1210            None => return ConditionResult::Unknown,
1211        };
1212        let threshold = z34_value.get(..12).unwrap_or(z34_value.as_str());
1213        for qual in &["Z33", "Z44", "Z45"] {
1214            let segs = ctx.find_segments_with_qualifier("DTM", 0, qual);
1215            for seg in &segs {
1216                if let Some(val) = seg.elements.first().and_then(|e| e.get(1)) {
1217                    let v = val.get(..12).unwrap_or(val);
1218                    if v < threshold {
1219                        return ConditionResult::False;
1220                    }
1221                }
1222            }
1223        }
1224        ConditionResult::True
1225    }
1226
1227    /// [33] Der Zeitpunkt in diesem DE muss ≤ dem Zeitpunkt aus dem DE2380 des Gültigkeitsende der ausgerollten Definition (SG5 DTM+Z35) sein
1228    // REVIEW: Validates that SG8 change timestamps (Z33/Z44/Z45) are <= the SG5 DTM+Z35 validity end. Symmetric counterpart to condition 32. First 12 characters used for format-303 string comparison. (medium confidence)
1229    fn evaluate_33(&self, ctx: &EvaluationContext) -> ConditionResult {
1230        // Timestamp in this DE <= DTM+Z35 (Gültigkeitsende der ausgerollten Definition)
1231        // Applies to SG8 change-point timestamps (Z33 Zählzeit, Z44 Schaltzeit, Z45 Leistungskurve)
1232        let z35_segs = ctx.find_segments_with_qualifier("DTM", 0, "Z35");
1233        let z35_value = match z35_segs
1234            .first()
1235            .and_then(|s| s.elements.first())
1236            .and_then(|e| e.get(1))
1237        {
1238            Some(v) => v.clone(),
1239            None => return ConditionResult::Unknown,
1240        };
1241        let threshold = z35_value.get(..12).unwrap_or(z35_value.as_str());
1242        for qual in &["Z33", "Z44", "Z45"] {
1243            let segs = ctx.find_segments_with_qualifier("DTM", 0, qual);
1244            for seg in &segs {
1245                if let Some(val) = seg.elements.first().and_then(|e| e.get(1)) {
1246                    let v = val.get(..12).unwrap_or(val);
1247                    if v > threshold {
1248                        return ConditionResult::False;
1249                    }
1250                }
1251            }
1252        }
1253        ConditionResult::True
1254    }
1255
1256    /// [34] Wenn im DE2379 dieses Segments der Code 401 vorhanden
1257    // REVIEW: Checks if any DTM segment has format code 401 at elements[0][2] (DE2379). Counterpart to condition [31] for the 401 format code (point-in-time without timezone vs. UTC offset). (medium confidence)
1258    fn evaluate_34(&self, ctx: &EvaluationContext) -> ConditionResult {
1259        ConditionResult::from(ctx.find_segments("DTM").iter().any(|s| {
1260            s.elements
1261                .first()
1262                .and_then(|e| e.get(2))
1263                .map(|v| v == "401")
1264                .unwrap_or(false)
1265        }))
1266    }
1267
1268    /// [36] Wenn in SG8 SEQ+Z43 DTM+Z33 (Zählzeitänderungszeitpunkt) im DE2379 der Code 401 vorhanden
1269    // REVIEW: In the same SG8, SEQ+Z43 (Zählzeitdefinition) must be present and DTM+Z33 must have format code 401 at elements[0][2]. Counterpart to condition [29] for the 401 format code. (medium confidence)
1270    fn evaluate_36(&self, ctx: &EvaluationContext) -> ConditionResult {
1271        ctx.any_group_has_co_occurrence("SEQ", 0, &["Z43"], "DTM", 0, 2, &["401"], &["SG5", "SG8"])
1272    }
1273
1274    /// [41] Wenn SG8 SEQ+Z42 (Zählzeitdefinition) vorhanden
1275    fn evaluate_41(&self, ctx: &EvaluationContext) -> ConditionResult {
1276        ctx.any_group_has_qualifier("SEQ", 0, "Z42", &["SG5", "SG8"])
1277    }
1278
1279    /// [44] Der in diesem Datenlement angegebene Code der Zählzeitdefinition muss innerhalb eines Vorgangs (IDE) eindeutig sein.
1280    // REVIEW: RFF+Z27 (elements[0][1]) in SG8 holds the Zählzeitdefinition code per the segment reference. Collects all Z27 reference values message-wide and checks for duplicates. Returns True when all codes are distinct (unique), False when a duplicate exists, Unknown when no Z27 segments found. Group navigator would be needed to properly scope to SG5 boundaries — approximated as message-wide. (medium confidence)
1281    fn evaluate_44(&self, ctx: &EvaluationContext) -> ConditionResult {
1282        // RFF+Z27 in SG8 holds the Zählzeitdefinition code — must be unique within the IDE (SG5) scope.
1283        // Approximate with message-wide check: all Z27 values must be distinct.
1284        let rff_segs = ctx.find_segments("RFF");
1285        let z27_values: Vec<&str> = rff_segs
1286            .iter()
1287            .filter(|s| {
1288                s.elements
1289                    .first()
1290                    .and_then(|e| e.first())
1291                    .is_some_and(|q| q == "Z27")
1292            })
1293            .filter_map(|s| {
1294                s.elements
1295                    .first()
1296                    .and_then(|e| e.get(1))
1297                    .map(|s| s.as_str())
1298            })
1299            .collect();
1300        if z27_values.is_empty() {
1301            return ConditionResult::Unknown;
1302        }
1303        let unique_count: std::collections::HashSet<&str> = z27_values.iter().copied().collect();
1304        ConditionResult::from(unique_count.len() == z27_values.len())
1305    }
1306
1307    /// [46] Wenn in SG8 SEQ+Z73 DTM+Z44 (Schaltzeitänderungszeitpunkt) im DE2379 der Code 303 vorhanden
1308    fn evaluate_46(&self, ctx: &EvaluationContext) -> ConditionResult {
1309        ctx.any_group_has_co_occurrence("SEQ", 0, &["Z73"], "DTM", 0, 2, &["303"], &["SG5", "SG8"])
1310    }
1311
1312    /// [47] Wenn in SG8 SEQ+Z73 DTM+Z44 (Schaltzeitänderungszeitpunkt) im DE2379 der Code 401 vorhanden
1313    fn evaluate_47(&self, ctx: &EvaluationContext) -> ConditionResult {
1314        ctx.any_group_has_co_occurrence("SEQ", 0, &["Z73"], "DTM", 0, 2, &["401"], &["SG5", "SG8"])
1315    }
1316
1317    /// [48] Wenn in SG8 SEQ+Z74 DTM+Z45 (Leistungskurvenänderungszeitpunkt) im DE2379 der Code 303 vorhanden
1318    fn evaluate_48(&self, ctx: &EvaluationContext) -> ConditionResult {
1319        ctx.any_group_has_co_occurrence("SEQ", 0, &["Z74"], "DTM", 0, 2, &["303"], &["SG5", "SG8"])
1320    }
1321
1322    /// [49] Wenn in SG8 SEQ+Z74 DTM+Z45 (Leistungskurvenänderungszeitpunkt) im DE2379 der Code 401 vorhanden
1323    fn evaluate_49(&self, ctx: &EvaluationContext) -> ConditionResult {
1324        ctx.any_group_has_co_occurrence("SEQ", 0, &["Z74"], "DTM", 0, 2, &["401"], &["SG5", "SG8"])
1325    }
1326
1327    /// [50] In jedem DE2379 dieses DTM-Segments innerhalb eines IDE+24 (Vorgangs) muss der gleiche Code angegeben werden
1328    // REVIEW: Checks that all DTM segments in the message use the same DE2379 format code. Full per-IDE+24 scoping would require a navigator, but message-wide consistency is a practical approximation — in well-formed UTILTS messages a single IDE is common. Returns Unknown when no DTMs are found. (medium confidence)
1329    fn evaluate_50(&self, ctx: &EvaluationContext) -> ConditionResult {
1330        // HAND-EDITED (#379): "dieses DTM-Segments innerhalb eines IDE+24" —
1331        // the DTMs with this one's qualifier in this Vorgang (SG5), not every
1332        // DTM of the message (DTM+137 is 303, DTM+293 304).
1333        let Some(qualifier) = ctx
1334            .resolved_segment
1335            .and_then(|e| e.first())
1336            .and_then(|c| c.first())
1337            .filter(|q| !q.is_empty())
1338        else {
1339            return ConditionResult::Unknown;
1340        };
1341        let dtms = ctx.scoped_find_segments_in("SG5", "DTM");
1342        let codes: Vec<&str> = dtms
1343            .iter()
1344            .filter(|s| s.get_component(0, 0) == qualifier.as_str())
1345            .map(|s| s.get_component(0, 2))
1346            .filter(|c| !c.is_empty())
1347            .collect();
1348        match codes.first() {
1349            None => ConditionResult::Unknown,
1350            Some(first) => ConditionResult::from(codes.iter().all(|c| c == first)),
1351        }
1352    }
1353
1354    /// [53] Wenn im DE3155 in demselben COM der Code EM vorhanden ist
1355    fn evaluate_53(&self, ctx: &EvaluationContext) -> ConditionResult {
1356        {
1357            let coms = ctx.find_segments("COM");
1358            ConditionResult::from(coms.iter().any(|s| {
1359                s.elements
1360                    .first()
1361                    .and_then(|e| e.get(1))
1362                    .is_some_and(|v| v == "EM")
1363            }))
1364        }
1365    }
1366
1367    /// [54] Wenn im DE3155 in demselben COM der Code TE / FX / AJ / AL vorhanden ist
1368    fn evaluate_54(&self, ctx: &EvaluationContext) -> ConditionResult {
1369        {
1370            let coms = ctx.find_segments("COM");
1371            ConditionResult::from(coms.iter().any(|s| {
1372                s.elements
1373                    .first()
1374                    .and_then(|e| e.get(1))
1375                    .is_some_and(|v| matches!(v.as_str(), "TE" | "FX" | "AJ" | "AL"))
1376            }))
1377        }
1378    }
1379
1380    /// [55] Es ist der Wert einzutragen, der sich aus der Wiederholungshäufigkeit des SG6 RFF+Z49/ Z53 (Verwendungszeitraum der Daten: Gültige Daten/ Keine Daten) ergibt. Bedeutet: Das erste SG6 RFF+Z49/ Z53...
1381    fn evaluate_55(&self, _ctx: &EvaluationContext) -> ConditionResult {
1382        // Hinweis: The value to enter is derived from the repetition index of SG6 RFF+Z49/Z53:
1383        // first occurrence = "1", second = "2", third = "3", etc.
1384        // This is an informational annotation describing how to populate the data element.
1385        ConditionResult::True
1386    }
1387
1388    /// [58] Wenn im selben SG6 RFF+Z49/ Z53 (Verwendungszeitraum der Daten: Gültige Daten/ Keine Daten) im DE1156 (Zeitraum-ID) eine Zeitraum ID genannt ist, die kleiner ist als in einem anderen SG6 RFF+Z49/ ...
1389    // HAND-EDITED: judges this SG6 against the other Z49/Z53 SG6 of its SG5.
1390    // The generated code was true message-wide once two Zeitraum-IDs differed,
1391    // so the last, open Zeitraum had to carry DTM+Z26 (#476).
1392    fn evaluate_58(&self, ctx: &EvaluationContext) -> ConditionResult {
1393        crate::eval::periods::followed_by_later_period(ctx, "SG5", &["Z49", "Z53"])
1394    }
1395
1396    /// [59] Es ist die Zeitraum-ID vom DE1156 aus einem passenden SG6 RFF+Z49 (Verwendungszeitraum der Daten) einzutragen
1397    // REVIEW: Cross-group Zeitraum-ID correlation: SG8 RFF+Z46 DE1154 (reference to Zeitraum-ID) must match a DE1156 value from SG6 RFF+Z49 (Verwendungszeitraum der Daten). Collects DE1156 values from all RFF+Z49 instances and checks whether any RFF+Z46 reference value matches. Returns Unknown when either set is empty. (medium confidence)
1398    fn evaluate_59(&self, ctx: &EvaluationContext) -> ConditionResult {
1399        {
1400            let rffs = ctx.find_segments("RFF");
1401            let sg6_ids: Vec<String> = rffs
1402                .iter()
1403                .filter(|s| {
1404                    s.elements
1405                        .first()
1406                        .and_then(|e| e.first())
1407                        .is_some_and(|q| q == "Z49")
1408                })
1409                .filter_map(|s| s.elements.first()?.get(2).cloned())
1410                .filter(|v| !v.is_empty())
1411                .collect();
1412            if sg6_ids.is_empty() {
1413                return ConditionResult::Unknown;
1414            }
1415            let sg8_refs: Vec<String> = rffs
1416                .iter()
1417                .filter(|s| {
1418                    s.elements
1419                        .first()
1420                        .and_then(|e| e.first())
1421                        .is_some_and(|q| q == "Z46")
1422                })
1423                .filter_map(|s| s.elements.first()?.get(1).cloned())
1424                .filter(|v| !v.is_empty())
1425                .collect();
1426            if sg8_refs.is_empty() {
1427                return ConditionResult::Unknown;
1428            }
1429            ConditionResult::from(sg8_refs.iter().any(|r| sg6_ids.contains(r)))
1430        }
1431    }
1432
1433    /// [61] Wenn in einem STS+E01 im DE9013 (Status der Antwort) ein Antwortcode aus dem Cluster Ablehnung vorhanden ist
1434    // HAND-EDITED: driven by per-EBD cluster data (mako_prozesse).
1435    // The condition is existential — any Ablehnung STS+E01 → True.
1436    fn evaluate_61(&self, ctx: &EvaluationContext) -> ConditionResult {
1437        any_sts_e01_in_cluster(ctx, Cluster::is_ablehnung)
1438    }
1439
1440    /// [62] Wenn MP-ID in SG2 NAD+MR (Nachrichtenempfänger) in der Rolle MSB
1441    /// EXTERNAL: Requires context from outside the message.
1442    fn evaluate_62(&self, ctx: &EvaluationContext) -> ConditionResult {
1443        ctx.external.evaluate("recipient_is_msb")
1444    }
1445
1446    /// [494] Das hier genannte Datum muss der Zeitpunkt sein, zu dem das Dokument erstellt wurde, oder ein Zeitpunkt, der davor liegt.
1447    fn evaluate_494(&self, ctx: &EvaluationContext) -> ConditionResult {
1448        // HAND-EDITED: subject is the current DTM's DE2380 (via ctx.resolved_value),
1449        // compared with the message date (DTM+137). Lexicographic compare is valid
1450        // for fixed-width CCYYMMDDHHMM+ZZ (format 303).
1451        let current = match ctx.resolved_value {
1452            Some(v) if !v.is_empty() => v,
1453            _ => return ConditionResult::Unknown,
1454        };
1455        let threshold = match ctx
1456            .find_segments_with_qualifier("DTM", 0, "137")
1457            .first()
1458            .and_then(|s| s.elements.first())
1459            .and_then(|e| e.get(1))
1460        {
1461            Some(v) if !v.is_empty() => v.clone(),
1462            _ => return ConditionResult::Unknown,
1463        };
1464        ConditionResult::from(current <= threshold.as_str())
1465    }
1466
1467    /// [501] Hinweis: Verwendung der ID der Marktlokation
1468    fn evaluate_501(&self, _ctx: &EvaluationContext) -> ConditionResult {
1469        ConditionResult::True
1470    }
1471
1472    /// [502] Hinweis: Verwendung der ID der Messlokation
1473    fn evaluate_502(&self, _ctx: &EvaluationContext) -> ConditionResult {
1474        // Hinweis: Verwendung der ID der Messlokation — informational note, always applies
1475        ConditionResult::True
1476    }
1477
1478    /// [504] Hinweis: Wert aus BGM+Z55 DE1004 der ORDERS mit der die Reklamation einer Definition erfolgt ist
1479    fn evaluate_504(&self, _ctx: &EvaluationContext) -> ConditionResult {
1480        // Hinweis: Wert aus BGM+Z55 DE1004 der ORDERS mit der die Reklamation einer Definition erfolgt ist — informational note, always applies
1481        ConditionResult::True
1482    }
1483
1484    /// [505] Hinweis: Jede ausgerollte Zählzeitdefinition ist in einem eigenen IDE anzugeben
1485    fn evaluate_505(&self, _ctx: &EvaluationContext) -> ConditionResult {
1486        // Hinweis: Jede ausgerollte Zählzeitdefinition ist in einem eigenen IDE anzugeben — informational note, always applies
1487        ConditionResult::True
1488    }
1489
1490    /// [506] Hinweis: Zeitpunkt, ab dem die Übersicht der Zählzeitdefinitionen gültig ist
1491    fn evaluate_506(&self, _ctx: &EvaluationContext) -> ConditionResult {
1492        // Hinweis: Zeitpunkt, ab dem die Übersicht der Zählzeitdefinitionen gültig ist — informational note, always applies
1493        ConditionResult::True
1494    }
1495
1496    /// [507] Hinweis: Es ist die Zeit nach der deutschen gesetzlichen Zeit anzugeben
1497    fn evaluate_507(&self, _ctx: &EvaluationContext) -> ConditionResult {
1498        // Hinweis: Es ist die Zeit nach der deutschen gesetzlichen Zeit anzugeben — informational note, always applies
1499        ConditionResult::True
1500    }
1501
1502    /// [508] Hinweis: Zeitpunkt, ab dem die Übersicht der Schaltzeitdefinitionen gültig ist
1503    fn evaluate_508(&self, _ctx: &EvaluationContext) -> ConditionResult {
1504        // Hinweis: Zeitpunkt, ab dem die Übersicht der Schaltzeitdefinitionen gültig ist — informational note, always applies
1505        ConditionResult::True
1506    }
1507
1508    /// [509] Hinweis: Zeitpunkt, ab dem die Übersicht der Leistungskurvendefinition gültig ist
1509    fn evaluate_509(&self, _ctx: &EvaluationContext) -> ConditionResult {
1510        // Hinweis: Zeitpunkt, ab dem die Übersicht der Leistungskurvendefinition gültig ist — informational note, always applies
1511        ConditionResult::True
1512    }
1513
1514    /// [510] Hinweis: Für jeden Zählzeitänderungszeitpunkt (SG8 DTM+Z33) ist diese Sementgruppe einmal anzugeben
1515    fn evaluate_510(&self, _ctx: &EvaluationContext) -> ConditionResult {
1516        // Hinweis: Für jeden Zählzeitänderungszeitpunkt (SG8 DTM+Z33) ist diese Segmentgruppe einmal anzugeben — informational note, always applies
1517        ConditionResult::True
1518    }
1519
1520    /// [511] Hinweis: Der Zählzeitänderungszeitpunkt (SG8DTM+Z33) dieser SG8 darf in keiner anderen SG8 „Zählzeitdefinition“ wiederholt werden
1521    // REVIEW: Checks that no DTM+Z33 DE2380 value is repeated across SG8 instances. Collects all Z33 qualifier DTM segments and verifies uniqueness. Medium confidence because SG8-scoped iteration falls back to message-wide, which is correct here since each DTM+Z33 belongs to a distinct SG8. (medium confidence)
1522    fn evaluate_511(&self, ctx: &EvaluationContext) -> ConditionResult {
1523        // HAND-EDITED: "DTM+Z33 dieser SG8 darf in keiner anderen SG8 „Zählzeitdefinition“ wiederholt werden" — compared with the other instances when the
1524        // scope reaches an SG8; the message as before otherwise.
1525        if let Some(scope) = ctx.enclosing_scope("SG8") {
1526            let z33 = |path: &[(&str, usize)]| -> Vec<String> {
1527                ctx.find_segments_at(path, "DTM")
1528                    .iter()
1529                    .filter(|s| s.get_element(0) == "Z33")
1530                    .map(|s| s.get_component(0, 1).to_string())
1531                    .filter(|v| !v.is_empty())
1532                    .collect()
1533            };
1534            let own = z33(scope);
1535            if own.is_empty() {
1536                return ConditionResult::Unknown;
1537            }
1538            let others = ctx.scopes_under("SG5", "SG8").unwrap_or_default();
1539            return ConditionResult::from(!others.iter().any(|p| {
1540                p.as_slice() != scope
1541                    && ctx
1542                        .find_segments_at(p, "SEQ")
1543                        .iter()
1544                        .any(|s| s.get_element(0) == "Z42")
1545                    && z33(p).iter().any(|v| own.contains(v))
1546            }));
1547        }
1548        // Uniqueness check: no DTM+Z33 value in DE2380 may appear in more than one SG8
1549        let dtm_z33_segments = ctx.find_segments_with_qualifier("DTM", 0, "Z33");
1550        let values: Vec<&str> = dtm_z33_segments
1551            .iter()
1552            .filter_map(|s| s.elements.first()?.get(1).map(|v| v.as_str()))
1553            .filter(|v| !v.is_empty())
1554            .collect();
1555        if values.is_empty() {
1556            return ConditionResult::Unknown;
1557        }
1558        // Check for duplicates: if all values are unique, condition is satisfied (True)
1559        let mut seen = std::collections::HashSet::new();
1560        let all_unique = values.iter().all(|v| seen.insert(*v));
1561        ConditionResult::from(all_unique)
1562    }
1563
1564    /// [512] Hinweis: Wenn der Code 303 im DE2379 des Zählzeitänderungszeitpunkt (SG8 DTM+Z33) genutzt wird, muss genau ein Wert im DE2380 des Zählzeitänderungszeitpunkt (SG8 DTM+Z33) identisch mit dem Wert...
1565    // REVIEW: When DTM+Z33 uses format code 303 in elements[0][2], exactly one DE2380 value (elements[0][1]) across all Z33 instances must equal the DE2380 value of DTM+Z34 from SG5. Medium confidence due to cross-SG group scoping — the message-wide fallback gives correct semantics here since we're correlating across SG5 and SG8 boundaries. (medium confidence)
1566    fn evaluate_512(&self, ctx: &EvaluationContext) -> ConditionResult {
1567        // When format code 303 is used in DTM+Z33 DE2379,
1568        // exactly one DE2380 value of DTM+Z33 must match the DE2380 of SG5 DTM+Z34
1569        let dtm_z33_segments = ctx.find_segments_with_qualifier("DTM", 0, "Z33");
1570        // Only consider Z33 segments that use format code 303
1571        let z33_303_values: Vec<&str> = dtm_z33_segments
1572            .iter()
1573            .filter(|s| {
1574                s.elements
1575                    .first()
1576                    .and_then(|e| e.get(2))
1577                    .is_some_and(|v| v == "303")
1578            })
1579            .filter_map(|s| s.elements.first()?.get(1).map(|v| v.as_str()))
1580            .filter(|v| !v.is_empty())
1581            .collect();
1582        if z33_303_values.is_empty() {
1583            // No Z33 with format 303 present — condition not applicable
1584            return ConditionResult::Unknown;
1585        }
1586        // Collect DE2380 values from SG5 DTM+Z34
1587        let dtm_z34_segments = ctx.find_segments_with_qualifier("DTM", 0, "Z34");
1588        let z34_values: Vec<&str> = dtm_z34_segments
1589            .iter()
1590            .filter_map(|s| s.elements.first()?.get(1).map(|v| v.as_str()))
1591            .filter(|v| !v.is_empty())
1592            .collect();
1593        if z34_values.is_empty() {
1594            return ConditionResult::Unknown;
1595        }
1596        // Exactly one Z33 DE2380 value must match a Z34 DE2380 value
1597        let match_count = z33_303_values
1598            .iter()
1599            .filter(|v| z34_values.contains(v))
1600            .count();
1601        ConditionResult::from(match_count == 1)
1602    }
1603
1604    /// [513] Hinweis: Wenn der Code 401 im DE2379 des Zählzeitänderungszeitpunkt (SG8 DTM+Z33) genutzt wird, muss genau ein Wert = 0000 im DE2380 des Zählzeitänderungszeitpunkt (SG8 DTM+Z33) sein
1605    fn evaluate_513(&self, ctx: &EvaluationContext) -> ConditionResult {
1606        // When format code 401 is used in DTM+Z33 DE2379,
1607        // exactly one DE2380 value must equal "0000"
1608        let dtm_z33_segments = ctx.find_segments_with_qualifier("DTM", 0, "Z33");
1609        // Only consider Z33 segments that use format code 401
1610        let z33_401_values: Vec<&str> = dtm_z33_segments
1611            .iter()
1612            .filter(|s| {
1613                s.elements
1614                    .first()
1615                    .and_then(|e| e.get(2))
1616                    .is_some_and(|v| v == "401")
1617            })
1618            .filter_map(|s| s.elements.first()?.get(1).map(|v| v.as_str()))
1619            .filter(|v| !v.is_empty())
1620            .collect();
1621        if z33_401_values.is_empty() {
1622            // No Z33 with format 401 present — condition not applicable
1623            return ConditionResult::Unknown;
1624        }
1625        // Exactly one value must equal "0000"
1626        let zero_count = z33_401_values.iter().filter(|v| **v == "0000").count();
1627        ConditionResult::from(zero_count == 1)
1628    }
1629
1630    /// [514] Hinweis: Für jeden Schaltzeitänderungszeitpunkt (SG8 DTM+Z44) ist diese Sementgruppe einmal anzugeben
1631    fn evaluate_514(&self, _ctx: &EvaluationContext) -> ConditionResult {
1632        // Hinweis: Für jeden Schaltzeitänderungszeitpunkt (SG8 DTM+Z44) ist diese Segmentgruppe einmal anzugeben — informational note, always applies
1633        ConditionResult::True
1634    }
1635
1636    /// [515] Hinweis: Kein Schaltzeitänderungszeitpunkt (SG8 DTM+Z44) darf mehrfach vorkommen
1637    fn evaluate_515(&self, ctx: &EvaluationContext) -> ConditionResult {
1638        let dtm_z44 = ctx.find_segments_with_qualifier("DTM", 0, "Z44");
1639        let mut values = std::collections::HashSet::new();
1640        for seg in &dtm_z44 {
1641            let c507 = match seg.elements.first() {
1642                Some(e) => e,
1643                None => continue,
1644            };
1645            let value = c507.get(1).map(|s| s.as_str()).unwrap_or("");
1646            if !value.is_empty() {
1647                if !values.insert(value.to_string()) {
1648                    return ConditionResult::False;
1649                }
1650            }
1651        }
1652        if dtm_z44.is_empty() {
1653            ConditionResult::Unknown
1654        } else {
1655            ConditionResult::True
1656        }
1657    }
1658
1659    /// [516] Hinweis: Wenn der Code 303 im DE2379 des Schaltzeitänderungszeitpunkt (SG8 DTM+Z44) genutzt wird, muss genau ein Wert im DE2380 des Schaltzeitänderungszeitpunkt (SG8 DTM+Z44) identisch mit dem We...
1660    // REVIEW: When format code 303 is used in DTM+Z44 DE2379, at least one DTM+Z44 DE2380 value must match a SG5 DTM+Z34 DE2380 value. Cross-group check; medium confidence due to SG5 context dependency. (medium confidence)
1661    fn evaluate_516(&self, ctx: &EvaluationContext) -> ConditionResult {
1662        let dtm_z44 = ctx.find_segments_with_qualifier("DTM", 0, "Z44");
1663        let dtm_z34 = ctx.find_segments_with_qualifier("DTM", 0, "Z34");
1664        let mut has_303 = false;
1665        let mut z34_values: std::collections::HashSet<String> = std::collections::HashSet::new();
1666        for seg in &dtm_z34 {
1667            let c507 = match seg.elements.first() {
1668                Some(e) => e,
1669                None => continue,
1670            };
1671            let value = c507.get(1).map(|s| s.clone()).unwrap_or_default();
1672            if !value.is_empty() {
1673                z34_values.insert(value);
1674            }
1675        }
1676        for seg in &dtm_z44 {
1677            let c507 = match seg.elements.first() {
1678                Some(e) => e,
1679                None => continue,
1680            };
1681            let format_code = c507.get(2).map(|s| s.as_str()).unwrap_or("");
1682            if format_code == "303" {
1683                has_303 = true;
1684                let value = c507.get(1).map(|s| s.as_str()).unwrap_or("");
1685                if z34_values.contains(value) {
1686                    return ConditionResult::True;
1687                }
1688            }
1689        }
1690        if has_303 {
1691            ConditionResult::False
1692        } else {
1693            ConditionResult::Unknown
1694        }
1695    }
1696
1697    /// [517] Hinweis: Wenn der Code 401 im DE2379 des Schaltzeitänderungszeitpunkt (SG8 DTM+Z44) genutzt wird, muss genau ein Wert = 0000 im DE2380 des Schaltzeitänderungszeitpunkt (SG8 DTM+Z44) sein
1698    fn evaluate_517(&self, ctx: &EvaluationContext) -> ConditionResult {
1699        let dtm_z44 = ctx.find_segments_with_qualifier("DTM", 0, "Z44");
1700        let mut has_401 = false;
1701        for seg in &dtm_z44 {
1702            let c507 = match seg.elements.first() {
1703                Some(e) => e,
1704                None => continue,
1705            };
1706            let format_code = c507.get(2).map(|s| s.as_str()).unwrap_or("");
1707            if format_code == "401" {
1708                has_401 = true;
1709                let value = c507.get(1).map(|s| s.as_str()).unwrap_or("");
1710                if value == "0000" {
1711                    return ConditionResult::True;
1712                }
1713            }
1714        }
1715        if has_401 {
1716            ConditionResult::False
1717        } else {
1718            ConditionResult::Unknown
1719        }
1720    }
1721
1722    /// [518] Hinweis: Für jeden Leistungskurvenänderungszeitpunkt (SG8 DTM+Z45) ist diese Sementgruppe einmal anzugeben
1723    fn evaluate_518(&self, _ctx: &EvaluationContext) -> ConditionResult {
1724        // Hinweis: Für jeden Leistungskurvenänderungszeitpunkt (SG8 DTM+Z45) ist diese Segmentgruppe einmal anzugeben — informational note, always applies
1725        ConditionResult::True
1726    }
1727
1728    /// [519] Hinweis: Kein Leistungskurvenänderungszeitpunkt (SG8 DTM+Z45) darf mehrfach vorkommen
1729    fn evaluate_519(&self, ctx: &EvaluationContext) -> ConditionResult {
1730        let dtm_z45 = ctx.find_segments_with_qualifier("DTM", 0, "Z45");
1731        let mut values = std::collections::HashSet::new();
1732        for seg in &dtm_z45 {
1733            let c507 = match seg.elements.first() {
1734                Some(e) => e,
1735                None => continue,
1736            };
1737            let value = c507.get(1).map(|s| s.as_str()).unwrap_or("");
1738            if !value.is_empty() {
1739                if !values.insert(value.to_string()) {
1740                    return ConditionResult::False;
1741                }
1742            }
1743        }
1744        if dtm_z45.is_empty() {
1745            ConditionResult::Unknown
1746        } else {
1747            ConditionResult::True
1748        }
1749    }
1750
1751    /// [520] Hinweis: Wenn der Code 303 im DE2379 des Leistungskurvenänderungszeitpunkt (SG8 DTM+Z45) genutzt wird, muss genau ein Wert im DE2380 des Leistungskurvenänderungszeitpunkt (SG8 DTM+Z45) identisch ...
1752    // REVIEW: When format code 303 is used in DTM+Z45 DE2379, at least one DTM+Z45 DE2380 value must match a SG5 DTM+Z34 DE2380 value. Same pattern as 516 but for Z45. (medium confidence)
1753    fn evaluate_520(&self, ctx: &EvaluationContext) -> ConditionResult {
1754        let dtm_z45 = ctx.find_segments_with_qualifier("DTM", 0, "Z45");
1755        let dtm_z34 = ctx.find_segments_with_qualifier("DTM", 0, "Z34");
1756        let mut has_303 = false;
1757        let mut z34_values: std::collections::HashSet<String> = std::collections::HashSet::new();
1758        for seg in &dtm_z34 {
1759            let c507 = match seg.elements.first() {
1760                Some(e) => e,
1761                None => continue,
1762            };
1763            let value = c507.get(1).map(|s| s.clone()).unwrap_or_default();
1764            if !value.is_empty() {
1765                z34_values.insert(value);
1766            }
1767        }
1768        for seg in &dtm_z45 {
1769            let c507 = match seg.elements.first() {
1770                Some(e) => e,
1771                None => continue,
1772            };
1773            let format_code = c507.get(2).map(|s| s.as_str()).unwrap_or("");
1774            if format_code == "303" {
1775                has_303 = true;
1776                let value = c507.get(1).map(|s| s.as_str()).unwrap_or("");
1777                if z34_values.contains(value) {
1778                    return ConditionResult::True;
1779                }
1780            }
1781        }
1782        if has_303 {
1783            ConditionResult::False
1784        } else {
1785            ConditionResult::Unknown
1786        }
1787    }
1788
1789    /// [521] Hinweis: Wenn der Code 401 im DE2379 des Leistungskurvenänderungszeitpunkt (SG8 DTM+Z45)
1790    // REVIEW: Incomplete condition text but follows the same pattern as 517 (DTM+Z44/401/'0000') but for DTM+Z45. Medium confidence due to incomplete specification. (medium confidence)
1791    fn evaluate_521(&self, ctx: &EvaluationContext) -> ConditionResult {
1792        let dtm_z45 = ctx.find_segments_with_qualifier("DTM", 0, "Z45");
1793        let mut has_401 = false;
1794        for seg in &dtm_z45 {
1795            let c507 = match seg.elements.first() {
1796                Some(e) => e,
1797                None => continue,
1798            };
1799            let format_code = c507.get(2).map(|s| s.as_str()).unwrap_or("");
1800            if format_code == "401" {
1801                has_401 = true;
1802                let value = c507.get(1).map(|s| s.as_str()).unwrap_or("");
1803                if value == "0000" {
1804                    return ConditionResult::True;
1805                }
1806            }
1807        }
1808        if has_401 {
1809            ConditionResult::False
1810        } else {
1811            ConditionResult::Unknown
1812        }
1813    }
1814
1815    /// [522] Hinweis: Jede ausgerollte Schaltzeitdefinition ist in einem eigenen IDE anzugeben
1816    fn evaluate_522(&self, _ctx: &EvaluationContext) -> ConditionResult {
1817        // Hinweis: Jede ausgerollte Schaltzeitdefinition ist in einem eigenen IDE anzugeben — informational note, always applies
1818        ConditionResult::True
1819    }
1820
1821    /// [523] Hinweis: Jede ausgerollte Leistungskurvendefinition ist in einem eigenen IDE anzugeben
1822    fn evaluate_523(&self, _ctx: &EvaluationContext) -> ConditionResult {
1823        // Hinweis: Jede ausgerollte Leistungskurvendefinition ist in einem eigenen IDE anzugeben — informational note, always applies
1824        ConditionResult::True
1825    }
1826
1827    /// [524] Hinweis: Es ist der Code einer Zählzeitdefinition anzugeben
1828    fn evaluate_524(&self, _ctx: &EvaluationContext) -> ConditionResult {
1829        // Hinweis: Es ist der Code einer Zählzeitdefinition anzugeben — informational note, always applies
1830        ConditionResult::True
1831    }
1832
1833    /// [525] Hinweis: Es ist der Code einer Schaltzeitdefinition anzugeben
1834    fn evaluate_525(&self, _ctx: &EvaluationContext) -> ConditionResult {
1835        // Hinweis: Es ist der Code einer Schaltzeitdefinition anzugeben — informational note, always applies
1836        ConditionResult::True
1837    }
1838
1839    /// [526] Hinweis: Es ist der Code einer Leistungskurvendefinition anzugeben
1840    fn evaluate_526(&self, _ctx: &EvaluationContext) -> ConditionResult {
1841        // Hinweis: Es ist der Code einer Leistungskurvendefinition anzugeben — informational note, always applies
1842        ConditionResult::True
1843    }
1844
1845    /// [527] Hinweis: Dieser Code ist anzugeben, wenn es sich um eine einmalig zu übermittelnde Definition handelt
1846    fn evaluate_527(&self, _ctx: &EvaluationContext) -> ConditionResult {
1847        // Hinweis: Dieser Code ist anzugeben, wenn es sich um eine einmalig zu übermittelnde Definition handelt — informational note, always applies
1848        ConditionResult::True
1849    }
1850
1851    /// [528] Hinweis: Dieser Code ist anzugeben, wenn es sich um eine jährlich zu übermittelnde Definition handelt
1852    fn evaluate_528(&self, _ctx: &EvaluationContext) -> ConditionResult {
1853        // Hinweis: Dieser Code ist anzugeben, wenn es sich um eine jährlich zu übermittelnde Definition handelt — informational note, always applies
1854        ConditionResult::True
1855    }
1856
1857    /// [529] Hinweis: Verwendung der ID der Netzlokation
1858    fn evaluate_529(&self, _ctx: &EvaluationContext) -> ConditionResult {
1859        // Hinweis: Verwendung der ID der Netzlokation — informational note, always applies
1860        ConditionResult::True
1861    }
1862
1863    /// [530] Hinweis: Es darf nur eine Information im DE3148 übermittelt werden
1864    fn evaluate_530(&self, _ctx: &EvaluationContext) -> ConditionResult {
1865        // Hinweis: Es darf nur eine Information im DE3148 übermittelt werden — informational note, always applies
1866        ConditionResult::True
1867    }
1868
1869    /// [531] Hinweis: Für weitere Details siehe Kapitel 4.1 "Übermittlung einer Vielzahl von Berechnungsformeln in einem Vorgang"
1870    fn evaluate_531(&self, _ctx: &EvaluationContext) -> ConditionResult {
1871        // Hinweis: Für weitere Details siehe Kapitel 4.1 "Übermittlung einer Vielzahl von Berechnungsformeln in einem Vorgang" — informational note, always applies
1872        ConditionResult::True
1873    }
1874
1875    /// [532] Hinweis: Es ist die Zeitraum-ID vom DE1156 aus einem passenden SG6 RFF+Z49/Z53 (Verwendungszeitraum der Daten: "Gültige Daten", "Keine Daten") aus der Übermittlung der Berechnungsformel aus SG6 R...
1876    // REVIEW: Cross-group Zeitraum-ID matching: collect DE1156 values from SG6 RFF+Z49/Z53 segments, then verify that SG8 RFF+Z46 references (DE1154) point to one of those IDs. Medium confidence because the condition is a hint about correct data linkage across groups, and the exact scoping (per-SG5/SG6 instance vs message-wide) may require navigator-based logic for full correctness. (medium confidence)
1877    fn evaluate_532(&self, ctx: &EvaluationContext) -> ConditionResult {
1878        {
1879            // Collect Zeitraum-IDs (DE1156 = elements[0][2]) from SG6 RFF+Z49 and RFF+Z53
1880            let rff_segments = ctx.find_segments("RFF");
1881            let zeitraum_ids: Vec<String> = rff_segments
1882                .iter()
1883                .filter(|s| {
1884                    s.elements
1885                        .first()
1886                        .and_then(|e| e.first())
1887                        .map(|q| q == "Z49" || q == "Z53")
1888                        .unwrap_or(false)
1889                })
1890                .filter_map(|s| {
1891                    s.elements
1892                        .first()
1893                        .and_then(|e| e.get(2))
1894                        .filter(|v| !v.is_empty())
1895                        .cloned()
1896                })
1897                .collect();
1898
1899            if zeitraum_ids.is_empty() {
1900                return ConditionResult::Unknown;
1901            }
1902
1903            // Check SG8 RFF+Z46 references (DE1154 = elements[0][1]) against collected Zeitraum-IDs
1904            let rff_z46_segments = ctx.find_segments_with_qualifier("RFF", 0, "Z46");
1905            if rff_z46_segments.is_empty() {
1906                return ConditionResult::Unknown;
1907            }
1908
1909            let any_match = rff_z46_segments.iter().any(|s| {
1910                s.elements
1911                    .first()
1912                    .and_then(|e| e.get(1))
1913                    .map(|ref_id| zeitraum_ids.iter().any(|zid| zid == ref_id))
1914                    .unwrap_or(false)
1915            });
1916
1917            ConditionResult::from(any_match)
1918        }
1919    }
1920
1921    /// [533] Hinweis: Für jeden übermittelten Zeitraum aus der Übermittlung der Berechnungsformel ist genau einmal das Segement anzugeben
1922    fn evaluate_533(&self, _ctx: &EvaluationContext) -> ConditionResult {
1923        // Hinweis: Für jeden übermittelten Zeitraum aus der Übermittlung der Berechnungsformel ist genau einmal das Segment anzugeben — informational note, always applies
1924        ConditionResult::True
1925    }
1926
1927    /// [534] Hinweis: Wert aus SG5 IDE+24 DE7402 mit der die Übermitt-lung der Berechnungsformel erfolgt ist.
1928    fn evaluate_534(&self, _ctx: &EvaluationContext) -> ConditionResult {
1929        // Hinweis: Wert aus SG5 IDE+24 DE7402 — value comes from the Vorgangsnummer of the Berechnungsformel transmission; informational note, always applies
1930        ConditionResult::True
1931    }
1932
1933    /// [912] Format: Wert kann mit maximal 6 Nachkommastellen angegeben werden
1934    // REVIEW: Format condition: max 6 decimal places. Applies to a numeric quantity value; QTY is the standard numeric segment in UTILTS. The AHB row context is not in the structure reference but QTY.elements[0][1] is the standard value position. (medium confidence)
1935    fn evaluate_912(&self, ctx: &EvaluationContext) -> ConditionResult {
1936        ctx.format_check("QTY", 0, 1, |val| validate_max_decimal_places(val, 6))
1937    }
1938
1939    /// [913] Format: Mögliche Werte: 1 bis 99999
1940    // REVIEW: Format condition: value must be in range [1, 99999]. Both bounds checked with validate_numeric and combined with AND logic. Applied to QTY value element as the most common numeric data element in UTILTS. (medium confidence)
1941    fn evaluate_913(&self, ctx: &EvaluationContext) -> ConditionResult {
1942        // HAND-EDITED: the value being validated (SEQ C286 1050, RFF 1154 …),
1943        // not the message's first QTY.
1944        ctx.format_check("QTY", 0, 1, |val| {
1945            match (
1946                validate_numeric(val, ">=", 1.0),
1947                validate_numeric(val, "<=", 99999.0),
1948            ) {
1949                (ConditionResult::True, ConditionResult::True) => ConditionResult::True,
1950                (ConditionResult::False, _) | (_, ConditionResult::False) => ConditionResult::False,
1951                _ => ConditionResult::Unknown,
1952            }
1953        })
1954    }
1955
1956    /// [914] Format: Möglicher Wert: &gt; 0
1957    // REVIEW: Format condition: value must be strictly greater than 0. Applied to QTY value element; validate_numeric with '>' operator handles this directly. (medium confidence)
1958    fn evaluate_914(&self, ctx: &EvaluationContext) -> ConditionResult {
1959        ctx.format_check("QTY", 0, 1, |val| validate_numeric(val, ">", 0.0))
1960    }
1961
1962    /// [915] Format: Möglicher Wert: ≠ 1
1963    // REVIEW: Format condition: value must not equal 1. Applied to the QTY quantity value (element[0][1]) as this is the standard numeric data element in UTILTS. Medium confidence because the exact segment/element this applies to is inferred from context rather than stated explicitly. (medium confidence)
1964    fn evaluate_915(&self, ctx: &EvaluationContext) -> ConditionResult {
1965        ctx.format_check("QTY", 0, 1, |val| validate_numeric(val, "!=", 1.0))
1966    }
1967
1968    /// [930] Format: max. 2 Nachkommastellen
1969    fn evaluate_930(&self, ctx: &EvaluationContext) -> ConditionResult {
1970        ctx.format_check("QTY", 0, 1, |val| validate_max_decimal_places(val, 2))
1971    }
1972
1973    /// [931] Format: ZZZ = +00
1974    fn evaluate_931(&self, ctx: &EvaluationContext) -> ConditionResult {
1975        ctx.format_check("DTM", 0, 1, validate_timezone_utc)
1976    }
1977
1978    /// [932] Format: HHMM = 2200
1979    fn evaluate_932(&self, ctx: &EvaluationContext) -> ConditionResult {
1980        ctx.format_check("DTM", 0, 1, |val| validate_hhmm_equals(val, "2200"))
1981    }
1982
1983    /// [933] Format: HHMM = 2300
1984    fn evaluate_933(&self, ctx: &EvaluationContext) -> ConditionResult {
1985        ctx.format_check("DTM", 0, 1, |val| validate_hhmm_equals(val, "2300"))
1986    }
1987
1988    /// [937] Format: keine Nachkommastelle
1989    fn evaluate_937(&self, ctx: &EvaluationContext) -> ConditionResult {
1990        ctx.format_check("QTY", 0, 1, |val| validate_max_decimal_places(val, 0))
1991    }
1992
1993    /// [939] Format: Die Zeichenkette muss die Zeichen @ und . enthalten
1994    fn evaluate_939(&self, ctx: &EvaluationContext) -> ConditionResult {
1995        ctx.format_check("COM", 0, 0, validate_email)
1996    }
1997
1998    /// [940] Format: Die Zeichenkette muss mit dem Zeichen + beginnen und danach dürfen nur noch Ziffern folgen
1999    fn evaluate_940(&self, ctx: &EvaluationContext) -> ConditionResult {
2000        ctx.format_check("COM", 0, 0, validate_phone)
2001    }
2002
2003    /// [947] Format: MMDDHHMM = 12312300
2004    fn evaluate_947(&self, ctx: &EvaluationContext) -> ConditionResult {
2005        ctx.format_check("DTM", 0, 1, |val| validate_mmddhhmm_equals(val, "12312300"))
2006    }
2007
2008    /// [950] Format: Marktlokations-ID
2009    // HAND-EDITED (#373): the condition sits on LOC+172 and RFF C506 1154, not on
2010    // LOC+Z16; judge the value it is attached to.
2011    fn evaluate_950(&self, ctx: &EvaluationContext) -> ConditionResult {
2012        ctx.format_check_resolved(validate_malo_id)
2013    }
2014
2015    /// [951] Format: Zählpunktbezeichnung
2016    // REVIEW: Zählpunktbezeichnung (metering point designation) is a 33-character alphanumeric ID. The validate_zahlpunkt helper checks this format. The segment is typically LOC with a metering point qualifier (Z17 for Messlokation or Z19 for SteuerbareRessource in UTILTS). Using both as fallback. (medium confidence)
2017    // HAND-EDITED (#373): the condition sits on LOC+172 and RFF C506 1154, not on
2018    // LOC+Z19; judge the value it is attached to.
2019    fn evaluate_951(&self, ctx: &EvaluationContext) -> ConditionResult {
2020        ctx.format_check_resolved(validate_zahlpunkt)
2021    }
2022
2023    /// [960] Format: Netzlokations-ID
2024    // HAND-EDITED: a Netzlokations-ID is "E" + ten alphanumerics (`E1688117482`),
2025    // not MaLo-shaped; `validate_malo_id` rejected every real one (#173).
2026    // HAND-EDITED (#373): the condition sits on LOC+172 and RFF C506 1154, not on
2027    // LOC+Z18; judge the value it is attached to.
2028    fn evaluate_960(&self, ctx: &EvaluationContext) -> ConditionResult {
2029        ctx.format_check_resolved(validate_nelo_id)
2030    }
2031
2032    /// [963] Format: Möglicher Wert: ≤ 100
2033    // REVIEW: Format condition: value must be <= 100. Applied to QTY segment value (element 0, component 1). Uses validate_numeric helper. (medium confidence)
2034    fn evaluate_963(&self, ctx: &EvaluationContext) -> ConditionResult {
2035        ctx.format_check("QTY", 0, 1, |val| validate_numeric(val, "<=", 100.0))
2036    }
2037
2038    /// [964] Format: HHMM ≥ 0000
2039    // REVIEW: Format condition: HHMM >= 0000. Combined with condition 965 (HHMM <= 2359), together they validate a valid time range. Using validate_hhmm_range covering both bounds. Applied to DTM segment time value. (medium confidence)
2040    fn evaluate_964(&self, ctx: &EvaluationContext) -> ConditionResult {
2041        ctx.format_check("DTM", 0, 1, |val| validate_hhmm_range(val, "0000", "2359"))
2042    }
2043
2044    /// [965] Format: HHMM ≤ 2359
2045    // REVIEW: Format condition: HHMM <= 2359. Paired with condition 964 (HHMM >= 0000). Together they validate a valid HHMM time. Using validate_hhmm_range for both bounds on the DTM segment. (medium confidence)
2046    fn evaluate_965(&self, ctx: &EvaluationContext) -> ConditionResult {
2047        ctx.format_check("DTM", 0, 1, |val| validate_hhmm_range(val, "0000", "2359"))
2048    }
2049
2050    /// [969] Format: Möglicher Wer: ≤ 1
2051    // REVIEW: 900-series format condition: 'Möglicher Wert: ≤ 1' means the value must be <= 1. Applied to QTY quantity value (most common numeric value in UTILTS). validate_numeric with "<=" operator handles this. Medium confidence because the exact target segment is inferred from context. (medium confidence)
2052    fn evaluate_969(&self, ctx: &EvaluationContext) -> ConditionResult {
2053        ctx.format_check("QTY", 0, 1, |val| validate_numeric(val, "<=", 1.0))
2054    }
2055
2056    /// [2001] Segment bzw. Segmentgruppe ist genau einmal anzugeben
2057    fn evaluate_2001(&self, _ctx: &EvaluationContext) -> ConditionResult {
2058        // Hinweis: Segment bzw. Segmentgruppe ist genau einmal anzugeben — informational cardinality note, always applies
2059        ConditionResult::True
2060    }
2061
2062    /// [2002] Für jeden Code der Zählzeit aus SG8 SEQ+Z42 (Zählzeitdefinition) SG9 CCI+Z39 (Code der Zählzeitdefinition) sind mindestens zwei Register anzugeben, bei denen in dieser SG8 das SG8 RFF+Z27 mit d...
2063    // REVIEW: For each CCI+Z39 code (Zählzeitdefinition code, elements[2][0]) found in SG9 groups, at least 2 SG8 instances must have RFF+Z27 (elements[0][0]=Z27, elements[0][1]=code value) matching that code. Uses message-wide scan since navigator API does not expose a simple find-in-group method for non-child groups. (medium confidence)
2064    fn evaluate_2002(&self, ctx: &EvaluationContext) -> ConditionResult {
2065        // HAND-EDITED: "mindestens zwei Register, bei denen … RFF+Z27 mit diesem Code gefüllt ist" — compared with the other instances when the
2066        // scope reaches an SG8; the message as before otherwise.
2067        if let Some(scope) = ctx.enclosing_scope("SG8") {
2068            let codes = |path: &[(&str, usize)]| -> Vec<String> {
2069                ctx.find_segments_at(path, "RFF")
2070                    .iter()
2071                    .filter(|s| s.get_element(0) == "Z27")
2072                    .map(|s| s.get_component(0, 1).to_string())
2073                    .filter(|v| !v.is_empty())
2074                    .collect()
2075            };
2076            let own = codes(scope);
2077            if own.is_empty() {
2078                return ConditionResult::Unknown;
2079            }
2080            let sg8s = ctx.scopes_under("SG5", "SG8").unwrap_or_default();
2081            return ConditionResult::from(
2082                own.iter()
2083                    .all(|code| sg8s.iter().filter(|p| codes(p).contains(code)).count() >= 2),
2084            );
2085        }
2086        // Collect CCI+Z39 codes from SG9 (Code der Zählzeitdefinition) message-wide
2087        let cci_segments = ctx.find_segments("CCI");
2088        let codes: Vec<String> = cci_segments
2089            .iter()
2090            .filter(|s| {
2091                s.elements
2092                    .first()
2093                    .and_then(|e| e.first())
2094                    .is_some_and(|v| v == "Z39")
2095            })
2096            .filter_map(|s| s.elements.get(2).and_then(|e| e.first()).cloned())
2097            .filter(|c| !c.is_empty())
2098            .collect();
2099        if codes.is_empty() {
2100            return ConditionResult::Unknown;
2101        }
2102        // For each code, at least 2 SG8 instances must have RFF+Z27 referencing that code
2103        let rff_z27 = ctx.find_segments_with_qualifier("RFF", 0, "Z27");
2104        for code in &codes {
2105            let count = rff_z27
2106                .iter()
2107                .filter(|s| {
2108                    s.elements
2109                        .first()
2110                        .and_then(|e| e.get(1))
2111                        .is_some_and(|v| v == code)
2112                })
2113                .count();
2114            if count < 2 {
2115                return ConditionResult::False;
2116            }
2117        }
2118        ConditionResult::True
2119    }
2120
2121    /// [2004] Segment ist genau einmal für jede Zeitraum-ID aus dem DE1156 der SG6 RFF+Z49 (Verwendungszeitraum der Daten: "Gültige Daten") anzugeben
2122    // REVIEW: For each Zeitraum-ID from SG6 RFF+Z49 DE1156 (elements[0][2]), exactly one SG5 STS must reference it. STS+E01 (Status der Antwort) stores Zeitraum-ID at elements[2][3] (DE9012). STS+Z23 (Status der Berechnungsformel) stores it at elements[2][0] (DE9013). Checks count == 1 for each Zeitraum-ID. (medium confidence)
2123    fn evaluate_2004(&self, ctx: &EvaluationContext) -> ConditionResult {
2124        // Collect Zeitraum-IDs from SG6 RFF+Z49 DE1156 (elements[0][2])
2125        let rff_z49 = ctx.find_segments_with_qualifier("RFF", 0, "Z49");
2126        if rff_z49.is_empty() {
2127            return ConditionResult::Unknown;
2128        }
2129        let zeitraum_ids: Vec<String> = rff_z49
2130            .iter()
2131            .filter_map(|s| s.elements.first().and_then(|e| e.get(2)).cloned())
2132            .filter(|id| !id.is_empty())
2133            .collect();
2134        if zeitraum_ids.is_empty() {
2135            return ConditionResult::Unknown;
2136        }
2137        let sts_segments = ctx.find_segments("STS");
2138        for zid in &zeitraum_ids {
2139            // STS+E01: Zeitraum-ID at elements[2][3] (DE9012)
2140            // STS+Z23: Zeitraum-ID at elements[2][0] (DE9013)
2141            let count = sts_segments
2142                .iter()
2143                .filter(|s| {
2144                    let qual = s
2145                        .elements
2146                        .first()
2147                        .and_then(|e| e.first())
2148                        .map(|v| v.as_str())
2149                        .unwrap_or("");
2150                    match qual {
2151                        "E01" => s
2152                            .elements
2153                            .get(2)
2154                            .and_then(|e| e.get(3))
2155                            .is_some_and(|v| v == zid),
2156                        "Z23" => s
2157                            .elements
2158                            .get(2)
2159                            .and_then(|e| e.first())
2160                            .is_some_and(|v| v == zid),
2161                        _ => false,
2162                    }
2163                })
2164                .count();
2165            if count != 1 {
2166                return ConditionResult::False;
2167            }
2168        }
2169        ConditionResult::True
2170    }
2171
2172    /// [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 ("Sontiges") enthalten ist
2173    // REVIEW: Finds STS+E01 segments where DE9013 (Code des Prüfschritts, elements[2][0]) = 'A99' (Sonstiges), then collects Zeitraum-IDs from DE9012 (elements[2][3]). For each such ID, checks that exactly one SG8 RFF+Z46 (elements[0][1]) references it, confirming the annotated segment appears exactly once per qualifying Zeitraum-ID. (medium confidence)
2174    fn evaluate_2005(&self, ctx: &EvaluationContext) -> ConditionResult {
2175        // Collect Zeitraum-IDs from STS+E01 where DE9013 (elements[2][0]) = A99 (Sonstiges)
2176        let sts_e01 = ctx.find_segments_with_qualifier("STS", 0, "E01");
2177        let zeitraum_ids: Vec<String> = sts_e01
2178            .iter()
2179            .filter(|s| {
2180                s.elements
2181                    .get(2)
2182                    .and_then(|e| e.first())
2183                    .is_some_and(|v| v == "A99")
2184            })
2185            .filter_map(|s| s.elements.get(2).and_then(|e| e.get(3)).cloned())
2186            .filter(|id| !id.is_empty())
2187            .collect();
2188        if zeitraum_ids.is_empty() {
2189            return ConditionResult::Unknown;
2190        }
2191        // For each Zeitraum-ID, exactly one SG8 RFF+Z46 (Referenz auf Zeitraum-ID) must reference it
2192        let rff_z46 = ctx.find_segments_with_qualifier("RFF", 0, "Z46");
2193        for zid in &zeitraum_ids {
2194            let count = rff_z46
2195                .iter()
2196                .filter(|s| {
2197                    s.elements
2198                        .first()
2199                        .and_then(|e| e.get(1))
2200                        .is_some_and(|v| v == zid)
2201                })
2202                .count();
2203            if count != 1 {
2204                return ConditionResult::False;
2205            }
2206        }
2207        ConditionResult::True
2208    }
2209
2210    /// [2006] Segmentgruppe ist mindestens einmal für jede Zeitraum-ID aus dem DE9013 der SG5 STS+Z23+Z33 (Berechnungsformel angefügt) anzugeben
2211    // REVIEW: Finds STS+Z23 (Status der Berechnungsformel) where status code (elements[1][0]) = 'Z33' (formula attached), collects Zeitraum-IDs from DE9013 (elements[2][0]). For each such ID, checks that at least one SG8 RFF+Z46 (elements[0][1]) references it, confirming the calculation formula group appears at minimum once per qualifying Zeitraum-ID. (medium confidence)
2212    fn evaluate_2006(&self, ctx: &EvaluationContext) -> ConditionResult {
2213        // Collect Zeitraum-IDs from STS+Z23 (Berechnungsformel) where status code = Z33 (angefügt)
2214        let sts_z23 = ctx.find_segments_with_qualifier("STS", 0, "Z23");
2215        let zeitraum_ids: Vec<String> = sts_z23
2216            .iter()
2217            .filter(|s| {
2218                s.elements
2219                    .get(1)
2220                    .and_then(|e| e.first())
2221                    .is_some_and(|v| v == "Z33")
2222            })
2223            .filter_map(|s| s.elements.get(2).and_then(|e| e.first()).cloned())
2224            .filter(|id| !id.is_empty())
2225            .collect();
2226        if zeitraum_ids.is_empty() {
2227            return ConditionResult::Unknown;
2228        }
2229        // For each Zeitraum-ID, at least one SG8 RFF+Z46 must reference it
2230        let rff_z46 = ctx.find_segments_with_qualifier("RFF", 0, "Z46");
2231        for zid in &zeitraum_ids {
2232            let count = rff_z46
2233                .iter()
2234                .filter(|s| {
2235                    s.elements
2236                        .first()
2237                        .and_then(|e| e.get(1))
2238                        .is_some_and(|v| v == zid)
2239                })
2240                .count();
2241            if count < 1 {
2242                return ConditionResult::False;
2243            }
2244        }
2245        ConditionResult::True
2246    }
2247
2248    /// [2007] Segmentgruppe ist genau einmal für jede Zeitraum-ID aus dem DE9013 der SG5 STS+Z23+Z33 (Berechnungsformel angefügt) anzugeben
2249    // REVIEW: The condition says the segment group must appear exactly once for each Zeitraum-ID from SG5 STS+Z23+Z33 (Berechnungsformel angefügt). From the MIG reference, STS with Statuskategorie Z23 has elements[0][0]=Z23, elements[1][0]=status code (Z33=angefügt), and elements[2][0]=Zeitraum-ID. The evaluator returns True when at least one such STS+Z23+Z33 is present in the message, which is when the cardinality rule triggers. Full cardinality counting (exactly one group per Zeitraum-ID) would require navigator-level group instance counting and cross-referencing, which goes beyond what the boolean ConditionResult can express — the condition is really a presence trigger for the group requirement. (medium confidence)
2250    fn evaluate_2007(&self, ctx: &EvaluationContext) -> ConditionResult {
2251        // Condition 2007: Segment group required exactly once per Zeitraum-ID from SG5 STS+Z23+Z33
2252        // Evaluates to True when at least one STS in SG5 has Statuskategorie=Z23 and Status=Z33
2253        // (Berechnungsformel angefügt), indicating the group must be present for that Zeitraum-ID.
2254        // STS Z23 structure: elements[0][0]=Z23 (Statuskategorie), elements[1][0]=Z33 (Status), elements[2][0]=Zeitraum-ID
2255        let sts_segments = ctx.find_segments("STS");
2256        let has_z23_z33 = sts_segments.iter().any(|s| {
2257            s.elements
2258                .first()
2259                .and_then(|e| e.first())
2260                .is_some_and(|v| v == "Z23")
2261                && s.elements
2262                    .get(1)
2263                    .and_then(|e| e.first())
2264                    .is_some_and(|v| v == "Z33")
2265        });
2266        ConditionResult::from(has_z23_z33)
2267    }
2268}