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        // Rule: for any step_id with duplicate SG8 instances, ALL operators must be Z69 or Z70
420        for (_step_id, (count, operators)) in &step_data {
421            if *count > 1 && operators.iter().any(|op| op != "Z69" && op != "Z70") {
422                return ConditionResult::False;
423            }
424        }
425        ConditionResult::True
426    }
427
428    /// [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
429    // 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)
430    fn evaluate_12(&self, ctx: &EvaluationContext) -> ConditionResult {
431        let nav = match ctx.navigator() {
432            Some(n) => n,
433            None => return ConditionResult::Unknown,
434        };
435        let sg8_count = nav.group_instance_count(&["SG5", "SG8"]);
436        if sg8_count == 0 {
437            return ConditionResult::Unknown;
438        }
439        // Map step_id -> (instance_count, has_z83_operator)
440        let mut step_data: std::collections::HashMap<String, (usize, bool)> =
441            std::collections::HashMap::new();
442        for i in 0..sg8_count {
443            let seq_segs = nav.find_segments_in_group("SEQ", &["SG5", "SG8"], i);
444            let step_id = seq_segs
445                .iter()
446                .find(|s| {
447                    s.elements
448                        .first()
449                        .and_then(|e| e.first())
450                        .is_some_and(|v| v == "Z37")
451                })
452                .and_then(|s| s.elements.get(1))
453                .and_then(|e| e.first())
454                .filter(|v| !v.is_empty())
455                .cloned();
456            let Some(step_id) = step_id else {
457                continue;
458            };
459            let sg9_count = nav.child_group_instance_count(&["SG5", "SG8"], i, "SG9");
460            let mut has_z83 = false;
461            for j in 0..sg9_count {
462                let cavs = nav.find_segments_in_child_group("CAV", &["SG5", "SG8"], i, "SG9", j);
463                if cavs.iter().any(|c| {
464                    c.elements
465                        .first()
466                        .and_then(|e| e.first())
467                        .is_some_and(|v| v == "Z83")
468                }) {
469                    has_z83 = true;
470                }
471            }
472            let entry = step_data.entry(step_id).or_insert((0, false));
473            entry.0 += 1;
474            entry.1 |= has_z83;
475        }
476        // Rule: if Z83 (Positivwert) operator is present for a step_id, that step_id must be unique
477        for (_step_id, (count, has_z83)) in &step_data {
478            if *has_z83 && *count > 1 {
479                return ConditionResult::False;
480            }
481        }
482        ConditionResult::True
483    }
484
485    /// [25] Wenn MP-ID in SG2 NAD+MR (Nachrichtenempfänger) in der Rolle LF
486    /// EXTERNAL: Requires context from outside the message.
487    fn evaluate_25(&self, ctx: &EvaluationContext) -> ConditionResult {
488        ctx.external.evaluate("recipient_is_lf")
489    }
490
491    /// [26] sofern per ORDERS reklamiert
492    /// EXTERNAL: Requires context from outside the message.
493    // 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)
494    fn evaluate_26(&self, ctx: &EvaluationContext) -> ConditionResult {
495        ctx.external.evaluate("claimed_via_orders")
496    }
497
498    /// [37] Wenn ein Gültigkeitsende bereits angegeben werden kann.
499    /// EXTERNAL: Requires context from outside the message.
500    // 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)
501    fn evaluate_37(&self, ctx: &EvaluationContext) -> ConditionResult {
502        ctx.external.evaluate("validity_end_known")
503    }
504
505    /// [42] Der in diesem Datenlement angegebene Code der Schaltzeitdefinition muss innerhalb eines Vorgangs (IDE) eindeutig sein.
506    // 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)
507    fn evaluate_42(&self, ctx: &EvaluationContext) -> ConditionResult {
508        let nav = match ctx.navigator() {
509            Some(n) => n,
510            None => return ConditionResult::Unknown,
511        };
512        let sg5_count = nav.group_instance_count(&["SG5"]);
513        for i in 0..sg5_count {
514            let sg8_count = nav.child_group_instance_count(&["SG5"], i, "SG8");
515            let mut seen = std::collections::HashSet::new();
516            for j in 0..sg8_count {
517                let dtms = nav.find_segments_in_child_group("DTM", &["SG5"], i, "SG8", j);
518                for dtm in &dtms {
519                    if dtm
520                        .elements
521                        .first()
522                        .and_then(|e| e.first())
523                        .is_some_and(|v| v == "Z44")
524                    {
525                        if let Some(val) = dtm.elements.first().and_then(|e| e.get(1)) {
526                            if !val.is_empty() && !seen.insert(val.clone()) {
527                                return ConditionResult::False;
528                            }
529                        }
530                    }
531                }
532            }
533        }
534        ConditionResult::True
535    }
536
537    /// [43] Der in diesem Datenlement angegebene Code der Leistungskurvendefinition muss innerhalb eines Vorgangs (IDE) eindeutig sein.
538    // 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)
539    fn evaluate_43(&self, ctx: &EvaluationContext) -> ConditionResult {
540        let nav = match ctx.navigator() {
541            Some(n) => n,
542            None => return ConditionResult::Unknown,
543        };
544        let sg5_count = nav.group_instance_count(&["SG5"]);
545        for i in 0..sg5_count {
546            let sg8_count = nav.child_group_instance_count(&["SG5"], i, "SG8");
547            let mut seen = std::collections::HashSet::new();
548            for j in 0..sg8_count {
549                let dtms = nav.find_segments_in_child_group("DTM", &["SG5"], i, "SG8", j);
550                for dtm in &dtms {
551                    if dtm
552                        .elements
553                        .first()
554                        .and_then(|e| e.first())
555                        .is_some_and(|v| v == "Z45")
556                    {
557                        if let Some(val) = dtm.elements.first().and_then(|e| e.get(1)) {
558                            if !val.is_empty() && !seen.insert(val.clone()) {
559                                return ConditionResult::False;
560                            }
561                        }
562                    }
563                }
564            }
565        }
566        ConditionResult::True
567    }
568
569    /// [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...
570    // 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)
571    fn evaluate_56(&self, ctx: &EvaluationContext) -> ConditionResult {
572        let msg_dtm_segs = ctx.find_segments_with_qualifier("DTM", 0, "137");
573        let msg_date_val = match msg_dtm_segs
574            .first()
575            .and_then(|s| s.elements.first())
576            .and_then(|e| e.get(1))
577        {
578            Some(v) => v.clone(),
579            None => return ConditionResult::Unknown,
580        };
581        if msg_date_val.len() < 8 {
582            return ConditionResult::Unknown;
583        }
584        let year: u32 = match msg_date_val[..4].parse::<u32>() {
585            Ok(v) => v,
586            Err(_) => return ConditionResult::Unknown,
587        };
588        let month: u32 = match msg_date_val[4..6].parse::<u32>() {
589            Ok(v) => v,
590            Err(_) => return ConditionResult::Unknown,
591        };
592        let day: u32 = match msg_date_val[6..8].parse::<u32>() {
593            Ok(v) => v,
594            Err(_) => return ConditionResult::Unknown,
595        };
596        let days_in_month: u32 = match month {
597            1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
598            4 | 6 | 9 | 11 => 30,
599            2 => {
600                if year % 400 == 0 || (year % 4 == 0 && year % 100 != 0) {
601                    29
602                } else {
603                    28
604                }
605            }
606            _ => return ConditionResult::Unknown,
607        };
608        let (ny, nm, nd): (u32, u32, u32) = if day >= days_in_month {
609            if month == 12 {
610                (year + 1, 1, 1)
611            } else {
612                (year, month + 1, 1)
613            }
614        } else {
615            (year, month, day + 1)
616        };
617        let threshold = format!("{:04}{:02}{:02}0000", ny, nm, nd);
618        let nav = match ctx.navigator() {
619            Some(n) => n,
620            None => return ConditionResult::Unknown,
621        };
622        let sg5_count = nav.group_instance_count(&["SG5"]);
623        for i in 0..sg5_count {
624            let sg6_count = nav.child_group_instance_count(&["SG5"], i, "SG6");
625            for j in 0..sg6_count {
626                let rffs = nav.find_segments_in_child_group("RFF", &["SG5"], i, "SG6", j);
627                let has_zeitraum_1 = rffs.iter().any(|rff| {
628                    rff.elements
629                        .first()
630                        .and_then(|e| e.get(2))
631                        .is_some_and(|v| v == "1")
632                });
633                if has_zeitraum_1 {
634                    let dtms = nav.find_segments_in_child_group("DTM", &["SG5"], i, "SG6", j);
635                    for dtm in &dtms {
636                        if dtm
637                            .elements
638                            .first()
639                            .and_then(|e| e.first())
640                            .is_some_and(|v| v == "Z25")
641                        {
642                            if let Some(dtm_val) = dtm.elements.first().and_then(|e| e.get(1)) {
643                                if dtm_val.as_str() > threshold.as_str() {
644                                    return ConditionResult::False;
645                                }
646                            }
647                        }
648                    }
649                }
650            }
651        }
652        ConditionResult::True
653    }
654
655    /// [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...
656    // 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)
657    fn evaluate_57(&self, ctx: &EvaluationContext) -> ConditionResult {
658        let nav = match ctx.navigator() {
659            Some(n) => n,
660            None => return ConditionResult::Unknown,
661        };
662        let sg6_path: &[&str] = &["SG5", "SG6"];
663        let sg6_count = nav.group_instance_count(sg6_path);
664        if sg6_count == 0 {
665            return ConditionResult::Unknown;
666        }
667        // Build (zeitraum_id, dtm_z25, dtm_z26) per SG6
668        let mut entries: Vec<(u32, Option<String>, Option<String>)> = Vec::new();
669        for i in 0..sg6_count {
670            let rff_segs = nav.find_segments_in_group("RFF", sg6_path, i);
671            let mut zeitraum_id: Option<u32> = None;
672            for rff in &rff_segs {
673                let qual = rff
674                    .elements
675                    .first()
676                    .and_then(|e| e.first())
677                    .map(|s| s.as_str());
678                if matches!(qual, Some("Z49") | Some("Z53")) {
679                    if let Some(id_str) = rff.elements.first().and_then(|e| e.get(2)) {
680                        if let Ok(id) = id_str.parse::<u32>() {
681                            zeitraum_id = Some(id);
682                        }
683                    }
684                }
685            }
686            let id = match zeitraum_id {
687                Some(id) => id,
688                None => continue,
689            };
690            let dtm_segs = nav.find_segments_in_group("DTM", sg6_path, i);
691            let mut dtm_z25: Option<String> = None;
692            let mut dtm_z26: Option<String> = None;
693            for dtm in &dtm_segs {
694                let qual = dtm
695                    .elements
696                    .first()
697                    .and_then(|e| e.first())
698                    .map(|s| s.as_str());
699                let val = dtm
700                    .elements
701                    .first()
702                    .and_then(|e| e.get(1))
703                    .filter(|v| !v.is_empty())
704                    .cloned();
705                match qual {
706                    Some("Z25") => dtm_z25 = val,
707                    Some("Z26") => dtm_z26 = val,
708                    _ => {}
709                }
710            }
711            entries.push((id, dtm_z25, dtm_z26));
712        }
713        // For non-ID-1 entries, check DTM+Z25 == DTM+Z26 of next-lower Zeitraum-ID
714        for (id, dtm_z25, _) in &entries {
715            if *id == 1 {
716                continue;
717            }
718            let current_z25 = match dtm_z25 {
719                Some(v) => v,
720                None => continue,
721            };
722            let next_lower = entries
723                .iter()
724                .filter(|(other_id, _, _)| *other_id < *id)
725                .map(|(other_id, _, _)| *other_id)
726                .max();
727            let lower_id = match next_lower {
728                Some(lid) => lid,
729                None => return ConditionResult::Unknown,
730            };
731            match entries.iter().find(|(eid, _, _)| *eid == lower_id) {
732                Some((_, _, Some(lower_z26))) => {
733                    if current_z25 != lower_z26 {
734                        return ConditionResult::False;
735                    }
736                }
737                Some((_, _, None)) => return ConditionResult::Unknown,
738                None => return ConditionResult::Unknown,
739            }
740        }
741        ConditionResult::True
742    }
743
744    /// [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
745    fn evaluate_490(&self, ctx: &EvaluationContext) -> ConditionResult {
746        let dtm_segs = ctx.find_segments_with_qualifier("DTM", 0, "137");
747        match dtm_segs
748            .first()
749            .and_then(|s| s.elements.first())
750            .and_then(|e| e.get(1))
751        {
752            Some(val) => is_mesz_utc(val),
753            None => ConditionResult::False, // segment absent → condition not applicable
754        }
755    }
756
757    /// [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
758    fn evaluate_491(&self, ctx: &EvaluationContext) -> ConditionResult {
759        let dtm_segs = ctx.find_segments_with_qualifier("DTM", 0, "137");
760        match dtm_segs
761            .first()
762            .and_then(|s| s.elements.first())
763            .and_then(|e| e.get(1))
764        {
765            Some(val) => is_mez_utc(val),
766            None => ConditionResult::False, // segment absent → condition not applicable
767        }
768    }
769
770    /// [1] Nur MP-ID aus Sparte Strom
771    /// EXTERNAL: Requires context from outside the message.
772    // 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)
773    fn evaluate_1(&self, ctx: &EvaluationContext) -> ConditionResult {
774        ctx.external.evaluate("mp_id_is_strom_sector")
775    }
776
777    /// [2] Wenn SG5 STS+Z23+Z34 (Berechnungsformel muss beim Absender angefragt werden) in einem SG5 IDE vorhanden
778    fn evaluate_2(&self, ctx: &EvaluationContext) -> ConditionResult {
779        ctx.any_group_has_qualified_value("STS", 0, "Z23", 1, 0, &["Z34"], &["SG5"])
780    }
781
782    /// [5] Wenn das SG8 RFF+Z19 (Referenz auf eine Messlokation) in derselben SG8 SEQ+Z37 nicht vorhanden
783    fn evaluate_5(&self, ctx: &EvaluationContext) -> ConditionResult {
784        // HAND-EDITED: "RFF+Z19 in derselben SG8 SEQ+Z37 nicht vorhanden" — the enclosing SG8 only, when the scope
785        // reaches one; the message as before otherwise.
786        if let Some(scope) = ctx.enclosing_scope("SG8") {
787            return ConditionResult::from(
788                ctx.find_segments_at(scope, "SEQ")
789                    .iter()
790                    .any(|s| s.get_element(0) == "Z37")
791                    && !ctx
792                        .find_segments_at(scope, "RFF")
793                        .iter()
794                        .any(|s| s.get_element(0) == "Z19"),
795            );
796        }
797        ctx.any_group_has_qualifier_without("RFF", 0, "Z19", "SEQ", 0, "Z37", &["SG4", "SG8"])
798    }
799
800    /// [6] Wenn das SG8 RFF+Z23 (Referenz auf Rechenschritt) in derselben SG8 SEQ+Z37 nicht vorhanden
801    fn evaluate_6(&self, ctx: &EvaluationContext) -> ConditionResult {
802        // HAND-EDITED: "RFF+Z23 in derselben SG8 SEQ+Z37 nicht vorhanden" — the enclosing SG8 only, when the scope
803        // reaches one; the message as before otherwise.
804        if let Some(scope) = ctx.enclosing_scope("SG8") {
805            return ConditionResult::from(
806                ctx.find_segments_at(scope, "SEQ")
807                    .iter()
808                    .any(|s| s.get_element(0) == "Z37")
809                    && !ctx
810                        .find_segments_at(scope, "RFF")
811                        .iter()
812                        .any(|s| s.get_element(0) == "Z23"),
813            );
814        }
815        ctx.any_group_has_qualifier_without("RFF", 0, "Z23", "SEQ", 0, "Z37", &["SG4", "SG8"])
816    }
817
818    /// [7] Wenn in derselben SG8 SEQ+Z37 das SG8 RFF+Z19 (Referenz auf eine Messlokation) vorhanden
819    fn evaluate_7(&self, ctx: &EvaluationContext) -> ConditionResult {
820        // HAND-EDITED: "in derselben SG8 SEQ+Z37 das SG8 RFF+Z19 vorhanden" — the enclosing SG8 only, when the scope
821        // reaches one; the message as before otherwise.
822        if let Some(scope) = ctx.enclosing_scope("SG8") {
823            return ConditionResult::from(
824                ctx.find_segments_at(scope, "SEQ")
825                    .iter()
826                    .any(|s| s.get_element(0) == "Z37")
827                    && ctx
828                        .find_segments_at(scope, "RFF")
829                        .iter()
830                        .any(|s| s.get_element(0) == "Z19"),
831            );
832        }
833        ctx.any_group_has_co_occurrence("SEQ", 0, &["Z37"], "RFF", 0, 0, &["Z19"], &["SG4", "SG8"])
834    }
835
836    /// [10] wenn vorhanden
837    fn evaluate_10(&self, _ctx: &EvaluationContext) -> ConditionResult {
838        // "wenn vorhanden" — conditional modifier meaning the associated rule applies
839        // only when the element is present. As a standalone boolean predicate this
840        // always evaluates to True: the condition itself imposes no additional
841        // constraint beyond the field's own optionality.
842        ConditionResult::True
843    }
844
845    /// [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-...
846    // 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)
847    fn evaluate_13(&self, ctx: &EvaluationContext) -> ConditionResult {
848        {
849            let nav = match ctx.navigator() {
850                Some(n) => n,
851                None => return ConditionResult::Unknown,
852            };
853
854            let sg8_count = nav.group_instance_count(&["SG5", "SG8"]);
855            // Collect (rs_id, zt_id, operator) for all Z37 SG8s that have CCI+++Z86 and CAV+Z80/Z81
856            let mut div_instances: Vec<(String, String, String)> = Vec::new();
857
858            for i in 0..sg8_count {
859                let seqs = nav.find_segments_in_group("SEQ", &["SG5", "SG8"], i);
860                let seq_z37 = seqs.iter().find(|s| {
861                    s.elements
862                        .first()
863                        .and_then(|e| e.first())
864                        .is_some_and(|v| v == "Z37")
865                });
866                let seq_z37 = match seq_z37 {
867                    Some(s) => s,
868                    None => continue,
869                };
870
871                let rs_id = seq_z37
872                    .elements
873                    .get(1)
874                    .and_then(|e| e.first())
875                    .cloned()
876                    .unwrap_or_default();
877
878                let rffs = nav.find_segments_in_group("RFF", &["SG5", "SG8"], i);
879                let zt_id = rffs
880                    .iter()
881                    .find(|s| {
882                        s.elements
883                            .first()
884                            .and_then(|e| e.first())
885                            .is_some_and(|v| v == "Z46")
886                    })
887                    .and_then(|s| s.elements.first())
888                    .and_then(|e| e.get(1))
889                    .cloned()
890                    .unwrap_or_default();
891
892                let sg9_count = nav.child_group_instance_count(&["SG5", "SG8"], i, "SG9");
893                for j in 0..sg9_count {
894                    let ccis =
895                        nav.find_segments_in_child_group("CCI", &["SG5", "SG8"], i, "SG9", j);
896                    let has_z86 = ccis.iter().any(|s| {
897                        s.elements
898                            .get(2)
899                            .and_then(|e| e.first())
900                            .is_some_and(|v| v == "Z86")
901                    });
902                    if !has_z86 {
903                        continue;
904                    }
905                    let cavs =
906                        nav.find_segments_in_child_group("CAV", &["SG5", "SG8"], i, "SG9", j);
907                    for cav in &cavs {
908                        let op = cav
909                            .elements
910                            .first()
911                            .and_then(|e| e.first())
912                            .map(|s| s.as_str());
913                        if matches!(op, Some("Z80") | Some("Z81")) {
914                            div_instances.push((
915                                rs_id.clone(),
916                                zt_id.clone(),
917                                op.unwrap().to_string(),
918                            ));
919                        }
920                    }
921                }
922            }
923
924            if div_instances.is_empty() {
925                return ConditionResult::Unknown;
926            }
927
928            // Each Z80 must have exactly one Z81 counterpart with same IDs, and vice versa
929            for (rs_id, zt_id, op) in &div_instances {
930                let counterpart = if op == "Z80" { "Z81" } else { "Z80" };
931                let count = div_instances
932                    .iter()
933                    .filter(|(rs, zt, o)| rs == rs_id && zt == zt_id && o == counterpart)
934                    .count();
935                if count != 1 {
936                    return ConditionResult::False;
937                }
938            }
939
940            ConditionResult::True
941        }
942    }
943
944    /// [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...
945    // 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)
946    fn evaluate_14(&self, ctx: &EvaluationContext) -> ConditionResult {
947        {
948            let nav = match ctx.navigator() {
949                Some(n) => n,
950                None => return ConditionResult::Unknown,
951            };
952
953            let sg8_count = nav.group_instance_count(&["SG5", "SG8"]);
954            // Collect (rs_id, zt_id, operators) for all Z37 SG8s with CCI+++Z86
955            let mut instances: Vec<(String, String, Vec<String>)> = Vec::new();
956
957            for i in 0..sg8_count {
958                let seqs = nav.find_segments_in_group("SEQ", &["SG5", "SG8"], i);
959                let seq_z37 = seqs.iter().find(|s| {
960                    s.elements
961                        .first()
962                        .and_then(|e| e.first())
963                        .is_some_and(|v| v == "Z37")
964                });
965                let seq_z37 = match seq_z37 {
966                    Some(s) => s,
967                    None => continue,
968                };
969
970                let rs_id = seq_z37
971                    .elements
972                    .get(1)
973                    .and_then(|e| e.first())
974                    .cloned()
975                    .unwrap_or_default();
976
977                let rffs = nav.find_segments_in_group("RFF", &["SG5", "SG8"], i);
978                let zt_id = rffs
979                    .iter()
980                    .find(|s| {
981                        s.elements
982                            .first()
983                            .and_then(|e| e.first())
984                            .is_some_and(|v| v == "Z46")
985                    })
986                    .and_then(|s| s.elements.first())
987                    .and_then(|e| e.get(1))
988                    .cloned()
989                    .unwrap_or_default();
990
991                let sg9_count = nav.child_group_instance_count(&["SG5", "SG8"], i, "SG9");
992                let mut ops: Vec<String> = Vec::new();
993                for j in 0..sg9_count {
994                    let ccis =
995                        nav.find_segments_in_child_group("CCI", &["SG5", "SG8"], i, "SG9", j);
996                    if !ccis.iter().any(|s| {
997                        s.elements
998                            .get(2)
999                            .and_then(|e| e.first())
1000                            .is_some_and(|v| v == "Z86")
1001                    }) {
1002                        continue;
1003                    }
1004                    let cavs =
1005                        nav.find_segments_in_child_group("CAV", &["SG5", "SG8"], i, "SG9", j);
1006                    for cav in &cavs {
1007                        if let Some(op) = cav.elements.first().and_then(|e| e.first()) {
1008                            ops.push(op.clone());
1009                        }
1010                    }
1011                }
1012                if !ops.is_empty() {
1013                    instances.push((rs_id, zt_id, ops));
1014                }
1015            }
1016
1017            // Find any (rs_id, zt_id) key that has a Z82 operator
1018            let z82_keys: Vec<(&str, &str)> = instances
1019                .iter()
1020                .filter(|(_, _, ops)| ops.iter().any(|op| op == "Z82"))
1021                .map(|(rs, zt, _)| (rs.as_str(), zt.as_str()))
1022                .collect();
1023
1024            if z82_keys.is_empty() {
1025                return ConditionResult::Unknown;
1026            }
1027
1028            // For each group keyed by (rs_id, zt_id) that has Z82, all operators must be exclusively Z82
1029            for (rs_id, zt_id) in &z82_keys {
1030                for (rs, zt, ops) in &instances {
1031                    if rs.as_str() == *rs_id && zt.as_str() == *zt_id {
1032                        if ops.iter().any(|op| op != "Z82") {
1033                            return ConditionResult::False;
1034                        }
1035                    }
1036                }
1037            }
1038
1039            ConditionResult::True
1040        }
1041    }
1042
1043    /// [15] Wenn in einem SG5 IDE+24 nur eine SEQ+Z37 mit einer SG8 RFF+Z19 (Messlokation) und der selben Zeitraum-ID vorhanden ist
1044    // 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)
1045    fn evaluate_15(&self, ctx: &EvaluationContext) -> ConditionResult {
1046        {
1047            let nav = match ctx.navigator() {
1048                Some(n) => n,
1049                None => return ConditionResult::Unknown,
1050            };
1051
1052            let sg8_count = nav.group_instance_count(&["SG5", "SG8"]);
1053            let mut matching_count = 0usize;
1054            let mut first_zt_id: Option<String> = None;
1055
1056            for i in 0..sg8_count {
1057                let seqs = nav.find_segments_in_group("SEQ", &["SG5", "SG8"], i);
1058                let has_z37 = seqs.iter().any(|s| {
1059                    s.elements
1060                        .first()
1061                        .and_then(|e| e.first())
1062                        .is_some_and(|v| v == "Z37")
1063                });
1064                if !has_z37 {
1065                    continue;
1066                }
1067
1068                let rffs = nav.find_segments_in_group("RFF", &["SG5", "SG8"], i);
1069                let has_z19 = rffs.iter().any(|s| {
1070                    s.elements
1071                        .first()
1072                        .and_then(|e| e.first())
1073                        .is_some_and(|v| v == "Z19")
1074                });
1075                if !has_z19 {
1076                    continue;
1077                }
1078
1079                // Verify Zeitraum-ID consistency across all matching instances
1080                let zt_id = rffs
1081                    .iter()
1082                    .find(|s| {
1083                        s.elements
1084                            .first()
1085                            .and_then(|e| e.first())
1086                            .is_some_and(|v| v == "Z46")
1087                    })
1088                    .and_then(|s| s.elements.first())
1089                    .and_then(|e| e.get(1))
1090                    .cloned();
1091
1092                if let Some(ref zt) = zt_id {
1093                    match &first_zt_id {
1094                        None => first_zt_id = Some(zt.clone()),
1095                        Some(first) => {
1096                            if first != zt {
1097                                return ConditionResult::False;
1098                            }
1099                        }
1100                    }
1101                }
1102
1103                matching_count += 1;
1104            }
1105
1106            ConditionResult::from(matching_count == 1)
1107        }
1108    }
1109
1110    /// [21] Wenn in dieser CAV+ZD3 der Wert im DE7110 mit Z32 (sonstiger Zählzeitdefinitionstyp) vorhanden ist
1111    fn evaluate_21(&self, ctx: &EvaluationContext) -> ConditionResult {
1112        ctx.has_qualified_value("CAV", 0, "ZD3", 0, 3, &["Z32"])
1113    }
1114
1115    /// [22] Wenn MP-ID in SG2 NAD+MS (Nachrichtenabsender) in der Rolle NB
1116    /// EXTERNAL: Requires context from outside the message.
1117    // 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)
1118    fn evaluate_22(&self, ctx: &EvaluationContext) -> ConditionResult {
1119        ctx.external.evaluate("sender_is_nb")
1120    }
1121
1122    /// [24] Wenn SG5 STS+Z36+Z45 (Definitionen werden verwendet) vorhanden
1123    fn evaluate_24(&self, ctx: &EvaluationContext) -> ConditionResult {
1124        ctx.has_qualified_value("STS", 0, "Z36", 1, 0, &["Z45"])
1125    }
1126
1127    /// [27] Wenn in SG9 CAV+ZD4+Z26 (keine Verwendung des Hochlastzeitfensters) vorhanden
1128    fn evaluate_27(&self, ctx: &EvaluationContext) -> ConditionResult {
1129        ctx.has_qualified_value("CAV", 0, "ZD4", 0, 3, &["Z26"])
1130    }
1131
1132    /// [29] Wenn in SG8 SEQ+Z43 DTM+Z33 (Zählzeitänderungszeitpunkt) im DE2379 der Code 303 vorhanden
1133    // 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)
1134    fn evaluate_29(&self, ctx: &EvaluationContext) -> ConditionResult {
1135        ctx.any_group_has_co_occurrence("SEQ", 0, &["Z43"], "DTM", 0, 2, &["303"], &["SG5", "SG8"])
1136    }
1137
1138    /// [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
1139    // 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)
1140    fn evaluate_30(&self, ctx: &EvaluationContext) -> ConditionResult {
1141        // CCYY in this DE must be exactly one higher than CCYY of SG5 DTM+Z34 DE2380
1142        // This applies to DTM+Z35 (Gültigkeitsende) whose year must be Z34 year + 1
1143        let z34_segs = ctx.find_segments_with_qualifier("DTM", 0, "Z34");
1144        let z35_segs = ctx.find_segments_with_qualifier("DTM", 0, "Z35");
1145        let z34_year = z34_segs
1146            .first()
1147            .and_then(|s| s.elements.first())
1148            .and_then(|e| e.get(1))
1149            .and_then(|v| v.get(..4))
1150            .and_then(|y| y.parse::<u32>().ok());
1151        let this_year = z35_segs
1152            .first()
1153            .and_then(|s| s.elements.first())
1154            .and_then(|e| e.get(1))
1155            .and_then(|v| v.get(..4))
1156            .and_then(|y| y.parse::<u32>().ok());
1157        match (z34_year, this_year) {
1158            (Some(start), Some(end)) => ConditionResult::from(end == start + 1),
1159            _ => ConditionResult::Unknown,
1160        }
1161    }
1162
1163    /// [31] Wenn im DE2379 dieses Segments der Code 303 vorhanden
1164    // 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)
1165    fn evaluate_31(&self, ctx: &EvaluationContext) -> ConditionResult {
1166        ConditionResult::from(ctx.find_segments("DTM").iter().any(|s| {
1167            s.elements
1168                .first()
1169                .and_then(|e| e.get(2))
1170                .map(|v| v == "303")
1171                .unwrap_or(false)
1172        }))
1173    }
1174
1175    /// [32] Der Zeitpunkt in diesem DE muss ≥ dem Zeitpunkt aus dem DE2380 des Gültigkeitsbeginn der ausgerollten Definition (SG5 DTM+Z34) sein
1176    // 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)
1177    fn evaluate_32(&self, ctx: &EvaluationContext) -> ConditionResult {
1178        // Timestamp in this DE >= DTM+Z34 (Gültigkeitsbeginn der ausgerollten Definition)
1179        // Applies to SG8 change-point timestamps (Z33 Zählzeit, Z44 Schaltzeit, Z45 Leistungskurve)
1180        let z34_segs = ctx.find_segments_with_qualifier("DTM", 0, "Z34");
1181        let z34_value = match z34_segs
1182            .first()
1183            .and_then(|s| s.elements.first())
1184            .and_then(|e| e.get(1))
1185        {
1186            Some(v) => v.clone(),
1187            None => return ConditionResult::Unknown,
1188        };
1189        let threshold = z34_value.get(..12).unwrap_or(z34_value.as_str());
1190        for qual in &["Z33", "Z44", "Z45"] {
1191            let segs = ctx.find_segments_with_qualifier("DTM", 0, qual);
1192            for seg in &segs {
1193                if let Some(val) = seg.elements.first().and_then(|e| e.get(1)) {
1194                    let v = val.get(..12).unwrap_or(val);
1195                    if v < threshold {
1196                        return ConditionResult::False;
1197                    }
1198                }
1199            }
1200        }
1201        ConditionResult::True
1202    }
1203
1204    /// [33] Der Zeitpunkt in diesem DE muss ≤ dem Zeitpunkt aus dem DE2380 des Gültigkeitsende der ausgerollten Definition (SG5 DTM+Z35) sein
1205    // 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)
1206    fn evaluate_33(&self, ctx: &EvaluationContext) -> ConditionResult {
1207        // Timestamp in this DE <= DTM+Z35 (Gültigkeitsende der ausgerollten Definition)
1208        // Applies to SG8 change-point timestamps (Z33 Zählzeit, Z44 Schaltzeit, Z45 Leistungskurve)
1209        let z35_segs = ctx.find_segments_with_qualifier("DTM", 0, "Z35");
1210        let z35_value = match z35_segs
1211            .first()
1212            .and_then(|s| s.elements.first())
1213            .and_then(|e| e.get(1))
1214        {
1215            Some(v) => v.clone(),
1216            None => return ConditionResult::Unknown,
1217        };
1218        let threshold = z35_value.get(..12).unwrap_or(z35_value.as_str());
1219        for qual in &["Z33", "Z44", "Z45"] {
1220            let segs = ctx.find_segments_with_qualifier("DTM", 0, qual);
1221            for seg in &segs {
1222                if let Some(val) = seg.elements.first().and_then(|e| e.get(1)) {
1223                    let v = val.get(..12).unwrap_or(val);
1224                    if v > threshold {
1225                        return ConditionResult::False;
1226                    }
1227                }
1228            }
1229        }
1230        ConditionResult::True
1231    }
1232
1233    /// [34] Wenn im DE2379 dieses Segments der Code 401 vorhanden
1234    // 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)
1235    fn evaluate_34(&self, ctx: &EvaluationContext) -> ConditionResult {
1236        ConditionResult::from(ctx.find_segments("DTM").iter().any(|s| {
1237            s.elements
1238                .first()
1239                .and_then(|e| e.get(2))
1240                .map(|v| v == "401")
1241                .unwrap_or(false)
1242        }))
1243    }
1244
1245    /// [36] Wenn in SG8 SEQ+Z43 DTM+Z33 (Zählzeitänderungszeitpunkt) im DE2379 der Code 401 vorhanden
1246    // 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)
1247    fn evaluate_36(&self, ctx: &EvaluationContext) -> ConditionResult {
1248        ctx.any_group_has_co_occurrence("SEQ", 0, &["Z43"], "DTM", 0, 2, &["401"], &["SG5", "SG8"])
1249    }
1250
1251    /// [41] Wenn SG8 SEQ+Z42 (Zählzeitdefinition) vorhanden
1252    fn evaluate_41(&self, ctx: &EvaluationContext) -> ConditionResult {
1253        ctx.any_group_has_qualifier("SEQ", 0, "Z42", &["SG5", "SG8"])
1254    }
1255
1256    /// [44] Der in diesem Datenlement angegebene Code der Zählzeitdefinition muss innerhalb eines Vorgangs (IDE) eindeutig sein.
1257    // 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)
1258    fn evaluate_44(&self, ctx: &EvaluationContext) -> ConditionResult {
1259        // RFF+Z27 in SG8 holds the Zählzeitdefinition code — must be unique within the IDE (SG5) scope.
1260        // Approximate with message-wide check: all Z27 values must be distinct.
1261        let rff_segs = ctx.find_segments("RFF");
1262        let z27_values: Vec<&str> = rff_segs
1263            .iter()
1264            .filter(|s| {
1265                s.elements
1266                    .first()
1267                    .and_then(|e| e.first())
1268                    .is_some_and(|q| q == "Z27")
1269            })
1270            .filter_map(|s| {
1271                s.elements
1272                    .first()
1273                    .and_then(|e| e.get(1))
1274                    .map(|s| s.as_str())
1275            })
1276            .collect();
1277        if z27_values.is_empty() {
1278            return ConditionResult::Unknown;
1279        }
1280        let unique_count: std::collections::HashSet<&str> = z27_values.iter().copied().collect();
1281        ConditionResult::from(unique_count.len() == z27_values.len())
1282    }
1283
1284    /// [46] Wenn in SG8 SEQ+Z73 DTM+Z44 (Schaltzeitänderungszeitpunkt) im DE2379 der Code 303 vorhanden
1285    fn evaluate_46(&self, ctx: &EvaluationContext) -> ConditionResult {
1286        ctx.any_group_has_co_occurrence("SEQ", 0, &["Z73"], "DTM", 0, 2, &["303"], &["SG5", "SG8"])
1287    }
1288
1289    /// [47] Wenn in SG8 SEQ+Z73 DTM+Z44 (Schaltzeitänderungszeitpunkt) im DE2379 der Code 401 vorhanden
1290    fn evaluate_47(&self, ctx: &EvaluationContext) -> ConditionResult {
1291        ctx.any_group_has_co_occurrence("SEQ", 0, &["Z73"], "DTM", 0, 2, &["401"], &["SG5", "SG8"])
1292    }
1293
1294    /// [48] Wenn in SG8 SEQ+Z74 DTM+Z45 (Leistungskurvenänderungszeitpunkt) im DE2379 der Code 303 vorhanden
1295    fn evaluate_48(&self, ctx: &EvaluationContext) -> ConditionResult {
1296        ctx.any_group_has_co_occurrence("SEQ", 0, &["Z74"], "DTM", 0, 2, &["303"], &["SG5", "SG8"])
1297    }
1298
1299    /// [49] Wenn in SG8 SEQ+Z74 DTM+Z45 (Leistungskurvenänderungszeitpunkt) im DE2379 der Code 401 vorhanden
1300    fn evaluate_49(&self, ctx: &EvaluationContext) -> ConditionResult {
1301        ctx.any_group_has_co_occurrence("SEQ", 0, &["Z74"], "DTM", 0, 2, &["401"], &["SG5", "SG8"])
1302    }
1303
1304    /// [50] In jedem DE2379 dieses DTM-Segments innerhalb eines IDE+24 (Vorgangs) muss der gleiche Code angegeben werden
1305    // 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)
1306    fn evaluate_50(&self, ctx: &EvaluationContext) -> ConditionResult {
1307        {
1308            let dtms = ctx.find_segments("DTM");
1309            let format_codes: Vec<&str> = dtms
1310                .iter()
1311                .filter_map(|s| s.elements.first()?.get(2).map(|s| s.as_str()))
1312                .filter(|s| !s.is_empty())
1313                .collect();
1314            if format_codes.is_empty() {
1315                return ConditionResult::Unknown;
1316            }
1317            let first = format_codes[0];
1318            ConditionResult::from(format_codes.iter().all(|&c| c == first))
1319        }
1320    }
1321
1322    /// [53] Wenn im DE3155 in demselben COM der Code EM vorhanden ist
1323    fn evaluate_53(&self, ctx: &EvaluationContext) -> ConditionResult {
1324        {
1325            let coms = ctx.find_segments("COM");
1326            ConditionResult::from(coms.iter().any(|s| {
1327                s.elements
1328                    .first()
1329                    .and_then(|e| e.get(1))
1330                    .is_some_and(|v| v == "EM")
1331            }))
1332        }
1333    }
1334
1335    /// [54] Wenn im DE3155 in demselben COM der Code TE / FX / AJ / AL vorhanden ist
1336    fn evaluate_54(&self, ctx: &EvaluationContext) -> ConditionResult {
1337        {
1338            let coms = ctx.find_segments("COM");
1339            ConditionResult::from(coms.iter().any(|s| {
1340                s.elements
1341                    .first()
1342                    .and_then(|e| e.get(1))
1343                    .is_some_and(|v| matches!(v.as_str(), "TE" | "FX" | "AJ" | "AL"))
1344            }))
1345        }
1346    }
1347
1348    /// [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...
1349    fn evaluate_55(&self, _ctx: &EvaluationContext) -> ConditionResult {
1350        // Hinweis: The value to enter is derived from the repetition index of SG6 RFF+Z49/Z53:
1351        // first occurrence = "1", second = "2", third = "3", etc.
1352        // This is an informational annotation describing how to populate the data element.
1353        ConditionResult::True
1354    }
1355
1356    /// [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/ ...
1357    // REVIEW: True when at least two SG6 RFF+Z49/Z53 instances exist with different DE1156 Zeitraum-IDs (i.e. min < max across all parsed integer IDs). This satisfies the 'smaller than another' ordering condition. Returns False for single-instance or equal-ID cases. (medium confidence)
1358    fn evaluate_58(&self, ctx: &EvaluationContext) -> ConditionResult {
1359        {
1360            let rffs = ctx.find_segments("RFF");
1361            let ids: Vec<i64> = rffs
1362                .iter()
1363                .filter(|s| {
1364                    s.elements
1365                        .first()
1366                        .and_then(|e| e.first())
1367                        .is_some_and(|q| q == "Z49" || q == "Z53")
1368                })
1369                .filter_map(|s| s.elements.first()?.get(2)?.parse::<i64>().ok())
1370                .collect();
1371            if ids.len() < 2 {
1372                return ConditionResult::False;
1373            }
1374            let min = *ids.iter().min().unwrap();
1375            let max = *ids.iter().max().unwrap();
1376            ConditionResult::from(min < max)
1377        }
1378    }
1379
1380    /// [59] Es ist die Zeitraum-ID vom DE1156 aus einem passenden SG6 RFF+Z49 (Verwendungszeitraum der Daten) einzutragen
1381    // 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)
1382    fn evaluate_59(&self, ctx: &EvaluationContext) -> ConditionResult {
1383        {
1384            let rffs = ctx.find_segments("RFF");
1385            let sg6_ids: Vec<String> = rffs
1386                .iter()
1387                .filter(|s| {
1388                    s.elements
1389                        .first()
1390                        .and_then(|e| e.first())
1391                        .is_some_and(|q| q == "Z49")
1392                })
1393                .filter_map(|s| s.elements.first()?.get(2).cloned())
1394                .filter(|v| !v.is_empty())
1395                .collect();
1396            if sg6_ids.is_empty() {
1397                return ConditionResult::Unknown;
1398            }
1399            let sg8_refs: Vec<String> = rffs
1400                .iter()
1401                .filter(|s| {
1402                    s.elements
1403                        .first()
1404                        .and_then(|e| e.first())
1405                        .is_some_and(|q| q == "Z46")
1406                })
1407                .filter_map(|s| s.elements.first()?.get(1).cloned())
1408                .filter(|v| !v.is_empty())
1409                .collect();
1410            if sg8_refs.is_empty() {
1411                return ConditionResult::Unknown;
1412            }
1413            ConditionResult::from(sg8_refs.iter().any(|r| sg6_ids.contains(r)))
1414        }
1415    }
1416
1417    /// [61] Wenn in einem STS+E01 im DE9013 (Status der Antwort) ein Antwortcode aus dem Cluster Ablehnung vorhanden ist
1418    // HAND-EDITED: driven by per-EBD cluster data (mako_prozesse).
1419    // The condition is existential — any Ablehnung STS+E01 → True.
1420    fn evaluate_61(&self, ctx: &EvaluationContext) -> ConditionResult {
1421        any_sts_e01_in_cluster(ctx, Cluster::is_ablehnung)
1422    }
1423
1424    /// [62] Wenn MP-ID in SG2 NAD+MR (Nachrichtenempfänger) in der Rolle MSB
1425    /// EXTERNAL: Requires context from outside the message.
1426    fn evaluate_62(&self, ctx: &EvaluationContext) -> ConditionResult {
1427        ctx.external.evaluate("recipient_is_msb")
1428    }
1429
1430    /// [494] Das hier genannte Datum muss der Zeitpunkt sein, zu dem das Dokument erstellt wurde, oder ein Zeitpunkt, der davor liegt.
1431    fn evaluate_494(&self, ctx: &EvaluationContext) -> ConditionResult {
1432        // HAND-EDITED: subject is the current DTM's DE2380 (via ctx.resolved_value),
1433        // compared with the message date (DTM+137). Lexicographic compare is valid
1434        // for fixed-width CCYYMMDDHHMM+ZZ (format 303).
1435        let current = match ctx.resolved_value {
1436            Some(v) if !v.is_empty() => v,
1437            _ => return ConditionResult::Unknown,
1438        };
1439        let threshold = match ctx
1440            .find_segments_with_qualifier("DTM", 0, "137")
1441            .first()
1442            .and_then(|s| s.elements.first())
1443            .and_then(|e| e.get(1))
1444        {
1445            Some(v) if !v.is_empty() => v.clone(),
1446            _ => return ConditionResult::Unknown,
1447        };
1448        ConditionResult::from(current <= threshold.as_str())
1449    }
1450
1451    /// [501] Hinweis: Verwendung der ID der Marktlokation
1452    fn evaluate_501(&self, _ctx: &EvaluationContext) -> ConditionResult {
1453        ConditionResult::True
1454    }
1455
1456    /// [502] Hinweis: Verwendung der ID der Messlokation
1457    fn evaluate_502(&self, _ctx: &EvaluationContext) -> ConditionResult {
1458        // Hinweis: Verwendung der ID der Messlokation — informational note, always applies
1459        ConditionResult::True
1460    }
1461
1462    /// [504] Hinweis: Wert aus BGM+Z55 DE1004 der ORDERS mit der die Reklamation einer Definition erfolgt ist
1463    fn evaluate_504(&self, _ctx: &EvaluationContext) -> ConditionResult {
1464        // Hinweis: Wert aus BGM+Z55 DE1004 der ORDERS mit der die Reklamation einer Definition erfolgt ist — informational note, always applies
1465        ConditionResult::True
1466    }
1467
1468    /// [505] Hinweis: Jede ausgerollte Zählzeitdefinition ist in einem eigenen IDE anzugeben
1469    fn evaluate_505(&self, _ctx: &EvaluationContext) -> ConditionResult {
1470        // Hinweis: Jede ausgerollte Zählzeitdefinition ist in einem eigenen IDE anzugeben — informational note, always applies
1471        ConditionResult::True
1472    }
1473
1474    /// [506] Hinweis: Zeitpunkt, ab dem die Übersicht der Zählzeitdefinitionen gültig ist
1475    fn evaluate_506(&self, _ctx: &EvaluationContext) -> ConditionResult {
1476        // Hinweis: Zeitpunkt, ab dem die Übersicht der Zählzeitdefinitionen gültig ist — informational note, always applies
1477        ConditionResult::True
1478    }
1479
1480    /// [507] Hinweis: Es ist die Zeit nach der deutschen gesetzlichen Zeit anzugeben
1481    fn evaluate_507(&self, _ctx: &EvaluationContext) -> ConditionResult {
1482        // Hinweis: Es ist die Zeit nach der deutschen gesetzlichen Zeit anzugeben — informational note, always applies
1483        ConditionResult::True
1484    }
1485
1486    /// [508] Hinweis: Zeitpunkt, ab dem die Übersicht der Schaltzeitdefinitionen gültig ist
1487    fn evaluate_508(&self, _ctx: &EvaluationContext) -> ConditionResult {
1488        // Hinweis: Zeitpunkt, ab dem die Übersicht der Schaltzeitdefinitionen gültig ist — informational note, always applies
1489        ConditionResult::True
1490    }
1491
1492    /// [509] Hinweis: Zeitpunkt, ab dem die Übersicht der Leistungskurvendefinition gültig ist
1493    fn evaluate_509(&self, _ctx: &EvaluationContext) -> ConditionResult {
1494        // Hinweis: Zeitpunkt, ab dem die Übersicht der Leistungskurvendefinition gültig ist — informational note, always applies
1495        ConditionResult::True
1496    }
1497
1498    /// [510] Hinweis: Für jeden Zählzeitänderungszeitpunkt (SG8 DTM+Z33) ist diese Sementgruppe einmal anzugeben
1499    fn evaluate_510(&self, _ctx: &EvaluationContext) -> ConditionResult {
1500        // Hinweis: Für jeden Zählzeitänderungszeitpunkt (SG8 DTM+Z33) ist diese Segmentgruppe einmal anzugeben — informational note, always applies
1501        ConditionResult::True
1502    }
1503
1504    /// [511] Hinweis: Der Zählzeitänderungszeitpunkt (SG8DTM+Z33) dieser SG8 darf in keiner anderen SG8 „Zählzeitdefinition“ wiederholt werden
1505    // 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)
1506    fn evaluate_511(&self, ctx: &EvaluationContext) -> ConditionResult {
1507        // HAND-EDITED: "DTM+Z33 dieser SG8 darf in keiner anderen SG8 „Zählzeitdefinition“ wiederholt werden" — compared with the other instances when the
1508        // scope reaches an SG8; the message as before otherwise.
1509        if let Some(scope) = ctx.enclosing_scope("SG8") {
1510            let z33 = |path: &[(&str, usize)]| -> Vec<String> {
1511                ctx.find_segments_at(path, "DTM")
1512                    .iter()
1513                    .filter(|s| s.get_element(0) == "Z33")
1514                    .map(|s| s.get_component(0, 1).to_string())
1515                    .filter(|v| !v.is_empty())
1516                    .collect()
1517            };
1518            let own = z33(scope);
1519            if own.is_empty() {
1520                return ConditionResult::Unknown;
1521            }
1522            let others = ctx.scopes_under("SG5", "SG8").unwrap_or_default();
1523            return ConditionResult::from(!others.iter().any(|p| {
1524                p.as_slice() != scope
1525                    && ctx
1526                        .find_segments_at(p, "SEQ")
1527                        .iter()
1528                        .any(|s| s.get_element(0) == "Z42")
1529                    && z33(p).iter().any(|v| own.contains(v))
1530            }));
1531        }
1532        // Uniqueness check: no DTM+Z33 value in DE2380 may appear in more than one SG8
1533        let dtm_z33_segments = ctx.find_segments_with_qualifier("DTM", 0, "Z33");
1534        let values: Vec<&str> = dtm_z33_segments
1535            .iter()
1536            .filter_map(|s| s.elements.first()?.get(1).map(|v| v.as_str()))
1537            .filter(|v| !v.is_empty())
1538            .collect();
1539        if values.is_empty() {
1540            return ConditionResult::Unknown;
1541        }
1542        // Check for duplicates: if all values are unique, condition is satisfied (True)
1543        let mut seen = std::collections::HashSet::new();
1544        let all_unique = values.iter().all(|v| seen.insert(*v));
1545        ConditionResult::from(all_unique)
1546    }
1547
1548    /// [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...
1549    // 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)
1550    fn evaluate_512(&self, ctx: &EvaluationContext) -> ConditionResult {
1551        // When format code 303 is used in DTM+Z33 DE2379,
1552        // exactly one DE2380 value of DTM+Z33 must match the DE2380 of SG5 DTM+Z34
1553        let dtm_z33_segments = ctx.find_segments_with_qualifier("DTM", 0, "Z33");
1554        // Only consider Z33 segments that use format code 303
1555        let z33_303_values: Vec<&str> = dtm_z33_segments
1556            .iter()
1557            .filter(|s| {
1558                s.elements
1559                    .first()
1560                    .and_then(|e| e.get(2))
1561                    .is_some_and(|v| v == "303")
1562            })
1563            .filter_map(|s| s.elements.first()?.get(1).map(|v| v.as_str()))
1564            .filter(|v| !v.is_empty())
1565            .collect();
1566        if z33_303_values.is_empty() {
1567            // No Z33 with format 303 present — condition not applicable
1568            return ConditionResult::Unknown;
1569        }
1570        // Collect DE2380 values from SG5 DTM+Z34
1571        let dtm_z34_segments = ctx.find_segments_with_qualifier("DTM", 0, "Z34");
1572        let z34_values: Vec<&str> = dtm_z34_segments
1573            .iter()
1574            .filter_map(|s| s.elements.first()?.get(1).map(|v| v.as_str()))
1575            .filter(|v| !v.is_empty())
1576            .collect();
1577        if z34_values.is_empty() {
1578            return ConditionResult::Unknown;
1579        }
1580        // Exactly one Z33 DE2380 value must match a Z34 DE2380 value
1581        let match_count = z33_303_values
1582            .iter()
1583            .filter(|v| z34_values.contains(v))
1584            .count();
1585        ConditionResult::from(match_count == 1)
1586    }
1587
1588    /// [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
1589    fn evaluate_513(&self, ctx: &EvaluationContext) -> ConditionResult {
1590        // When format code 401 is used in DTM+Z33 DE2379,
1591        // exactly one DE2380 value must equal "0000"
1592        let dtm_z33_segments = ctx.find_segments_with_qualifier("DTM", 0, "Z33");
1593        // Only consider Z33 segments that use format code 401
1594        let z33_401_values: Vec<&str> = dtm_z33_segments
1595            .iter()
1596            .filter(|s| {
1597                s.elements
1598                    .first()
1599                    .and_then(|e| e.get(2))
1600                    .is_some_and(|v| v == "401")
1601            })
1602            .filter_map(|s| s.elements.first()?.get(1).map(|v| v.as_str()))
1603            .filter(|v| !v.is_empty())
1604            .collect();
1605        if z33_401_values.is_empty() {
1606            // No Z33 with format 401 present — condition not applicable
1607            return ConditionResult::Unknown;
1608        }
1609        // Exactly one value must equal "0000"
1610        let zero_count = z33_401_values.iter().filter(|v| **v == "0000").count();
1611        ConditionResult::from(zero_count == 1)
1612    }
1613
1614    /// [514] Hinweis: Für jeden Schaltzeitänderungszeitpunkt (SG8 DTM+Z44) ist diese Sementgruppe einmal anzugeben
1615    fn evaluate_514(&self, _ctx: &EvaluationContext) -> ConditionResult {
1616        // Hinweis: Für jeden Schaltzeitänderungszeitpunkt (SG8 DTM+Z44) ist diese Segmentgruppe einmal anzugeben — informational note, always applies
1617        ConditionResult::True
1618    }
1619
1620    /// [515] Hinweis: Kein Schaltzeitänderungszeitpunkt (SG8 DTM+Z44) darf mehrfach vorkommen
1621    fn evaluate_515(&self, ctx: &EvaluationContext) -> ConditionResult {
1622        let dtm_z44 = ctx.find_segments_with_qualifier("DTM", 0, "Z44");
1623        let mut values = std::collections::HashSet::new();
1624        for seg in &dtm_z44 {
1625            let c507 = match seg.elements.first() {
1626                Some(e) => e,
1627                None => continue,
1628            };
1629            let value = c507.get(1).map(|s| s.as_str()).unwrap_or("");
1630            if !value.is_empty() {
1631                if !values.insert(value.to_string()) {
1632                    return ConditionResult::False;
1633                }
1634            }
1635        }
1636        if dtm_z44.is_empty() {
1637            ConditionResult::Unknown
1638        } else {
1639            ConditionResult::True
1640        }
1641    }
1642
1643    /// [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...
1644    // 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)
1645    fn evaluate_516(&self, ctx: &EvaluationContext) -> ConditionResult {
1646        let dtm_z44 = ctx.find_segments_with_qualifier("DTM", 0, "Z44");
1647        let dtm_z34 = ctx.find_segments_with_qualifier("DTM", 0, "Z34");
1648        let mut has_303 = false;
1649        let mut z34_values: std::collections::HashSet<String> = std::collections::HashSet::new();
1650        for seg in &dtm_z34 {
1651            let c507 = match seg.elements.first() {
1652                Some(e) => e,
1653                None => continue,
1654            };
1655            let value = c507.get(1).map(|s| s.clone()).unwrap_or_default();
1656            if !value.is_empty() {
1657                z34_values.insert(value);
1658            }
1659        }
1660        for seg in &dtm_z44 {
1661            let c507 = match seg.elements.first() {
1662                Some(e) => e,
1663                None => continue,
1664            };
1665            let format_code = c507.get(2).map(|s| s.as_str()).unwrap_or("");
1666            if format_code == "303" {
1667                has_303 = true;
1668                let value = c507.get(1).map(|s| s.as_str()).unwrap_or("");
1669                if z34_values.contains(value) {
1670                    return ConditionResult::True;
1671                }
1672            }
1673        }
1674        if has_303 {
1675            ConditionResult::False
1676        } else {
1677            ConditionResult::Unknown
1678        }
1679    }
1680
1681    /// [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
1682    fn evaluate_517(&self, ctx: &EvaluationContext) -> ConditionResult {
1683        let dtm_z44 = ctx.find_segments_with_qualifier("DTM", 0, "Z44");
1684        let mut has_401 = false;
1685        for seg in &dtm_z44 {
1686            let c507 = match seg.elements.first() {
1687                Some(e) => e,
1688                None => continue,
1689            };
1690            let format_code = c507.get(2).map(|s| s.as_str()).unwrap_or("");
1691            if format_code == "401" {
1692                has_401 = true;
1693                let value = c507.get(1).map(|s| s.as_str()).unwrap_or("");
1694                if value == "0000" {
1695                    return ConditionResult::True;
1696                }
1697            }
1698        }
1699        if has_401 {
1700            ConditionResult::False
1701        } else {
1702            ConditionResult::Unknown
1703        }
1704    }
1705
1706    /// [518] Hinweis: Für jeden Leistungskurvenänderungszeitpunkt (SG8 DTM+Z45) ist diese Sementgruppe einmal anzugeben
1707    fn evaluate_518(&self, _ctx: &EvaluationContext) -> ConditionResult {
1708        // Hinweis: Für jeden Leistungskurvenänderungszeitpunkt (SG8 DTM+Z45) ist diese Segmentgruppe einmal anzugeben — informational note, always applies
1709        ConditionResult::True
1710    }
1711
1712    /// [519] Hinweis: Kein Leistungskurvenänderungszeitpunkt (SG8 DTM+Z45) darf mehrfach vorkommen
1713    fn evaluate_519(&self, ctx: &EvaluationContext) -> ConditionResult {
1714        let dtm_z45 = ctx.find_segments_with_qualifier("DTM", 0, "Z45");
1715        let mut values = std::collections::HashSet::new();
1716        for seg in &dtm_z45 {
1717            let c507 = match seg.elements.first() {
1718                Some(e) => e,
1719                None => continue,
1720            };
1721            let value = c507.get(1).map(|s| s.as_str()).unwrap_or("");
1722            if !value.is_empty() {
1723                if !values.insert(value.to_string()) {
1724                    return ConditionResult::False;
1725                }
1726            }
1727        }
1728        if dtm_z45.is_empty() {
1729            ConditionResult::Unknown
1730        } else {
1731            ConditionResult::True
1732        }
1733    }
1734
1735    /// [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 ...
1736    // 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)
1737    fn evaluate_520(&self, ctx: &EvaluationContext) -> ConditionResult {
1738        let dtm_z45 = ctx.find_segments_with_qualifier("DTM", 0, "Z45");
1739        let dtm_z34 = ctx.find_segments_with_qualifier("DTM", 0, "Z34");
1740        let mut has_303 = false;
1741        let mut z34_values: std::collections::HashSet<String> = std::collections::HashSet::new();
1742        for seg in &dtm_z34 {
1743            let c507 = match seg.elements.first() {
1744                Some(e) => e,
1745                None => continue,
1746            };
1747            let value = c507.get(1).map(|s| s.clone()).unwrap_or_default();
1748            if !value.is_empty() {
1749                z34_values.insert(value);
1750            }
1751        }
1752        for seg in &dtm_z45 {
1753            let c507 = match seg.elements.first() {
1754                Some(e) => e,
1755                None => continue,
1756            };
1757            let format_code = c507.get(2).map(|s| s.as_str()).unwrap_or("");
1758            if format_code == "303" {
1759                has_303 = true;
1760                let value = c507.get(1).map(|s| s.as_str()).unwrap_or("");
1761                if z34_values.contains(value) {
1762                    return ConditionResult::True;
1763                }
1764            }
1765        }
1766        if has_303 {
1767            ConditionResult::False
1768        } else {
1769            ConditionResult::Unknown
1770        }
1771    }
1772
1773    /// [521] Hinweis: Wenn der Code 401 im DE2379 des Leistungskurvenänderungszeitpunkt (SG8 DTM+Z45)
1774    // 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)
1775    fn evaluate_521(&self, ctx: &EvaluationContext) -> ConditionResult {
1776        let dtm_z45 = ctx.find_segments_with_qualifier("DTM", 0, "Z45");
1777        let mut has_401 = false;
1778        for seg in &dtm_z45 {
1779            let c507 = match seg.elements.first() {
1780                Some(e) => e,
1781                None => continue,
1782            };
1783            let format_code = c507.get(2).map(|s| s.as_str()).unwrap_or("");
1784            if format_code == "401" {
1785                has_401 = true;
1786                let value = c507.get(1).map(|s| s.as_str()).unwrap_or("");
1787                if value == "0000" {
1788                    return ConditionResult::True;
1789                }
1790            }
1791        }
1792        if has_401 {
1793            ConditionResult::False
1794        } else {
1795            ConditionResult::Unknown
1796        }
1797    }
1798
1799    /// [522] Hinweis: Jede ausgerollte Schaltzeitdefinition ist in einem eigenen IDE anzugeben
1800    fn evaluate_522(&self, _ctx: &EvaluationContext) -> ConditionResult {
1801        // Hinweis: Jede ausgerollte Schaltzeitdefinition ist in einem eigenen IDE anzugeben — informational note, always applies
1802        ConditionResult::True
1803    }
1804
1805    /// [523] Hinweis: Jede ausgerollte Leistungskurvendefinition ist in einem eigenen IDE anzugeben
1806    fn evaluate_523(&self, _ctx: &EvaluationContext) -> ConditionResult {
1807        // Hinweis: Jede ausgerollte Leistungskurvendefinition ist in einem eigenen IDE anzugeben — informational note, always applies
1808        ConditionResult::True
1809    }
1810
1811    /// [524] Hinweis: Es ist der Code einer Zählzeitdefinition anzugeben
1812    fn evaluate_524(&self, _ctx: &EvaluationContext) -> ConditionResult {
1813        // Hinweis: Es ist der Code einer Zählzeitdefinition anzugeben — informational note, always applies
1814        ConditionResult::True
1815    }
1816
1817    /// [525] Hinweis: Es ist der Code einer Schaltzeitdefinition anzugeben
1818    fn evaluate_525(&self, _ctx: &EvaluationContext) -> ConditionResult {
1819        // Hinweis: Es ist der Code einer Schaltzeitdefinition anzugeben — informational note, always applies
1820        ConditionResult::True
1821    }
1822
1823    /// [526] Hinweis: Es ist der Code einer Leistungskurvendefinition anzugeben
1824    fn evaluate_526(&self, _ctx: &EvaluationContext) -> ConditionResult {
1825        // Hinweis: Es ist der Code einer Leistungskurvendefinition anzugeben — informational note, always applies
1826        ConditionResult::True
1827    }
1828
1829    /// [527] Hinweis: Dieser Code ist anzugeben, wenn es sich um eine einmalig zu übermittelnde Definition handelt
1830    fn evaluate_527(&self, _ctx: &EvaluationContext) -> ConditionResult {
1831        // Hinweis: Dieser Code ist anzugeben, wenn es sich um eine einmalig zu übermittelnde Definition handelt — informational note, always applies
1832        ConditionResult::True
1833    }
1834
1835    /// [528] Hinweis: Dieser Code ist anzugeben, wenn es sich um eine jährlich zu übermittelnde Definition handelt
1836    fn evaluate_528(&self, _ctx: &EvaluationContext) -> ConditionResult {
1837        // Hinweis: Dieser Code ist anzugeben, wenn es sich um eine jährlich zu übermittelnde Definition handelt — informational note, always applies
1838        ConditionResult::True
1839    }
1840
1841    /// [529] Hinweis: Verwendung der ID der Netzlokation
1842    fn evaluate_529(&self, _ctx: &EvaluationContext) -> ConditionResult {
1843        // Hinweis: Verwendung der ID der Netzlokation — informational note, always applies
1844        ConditionResult::True
1845    }
1846
1847    /// [530] Hinweis: Es darf nur eine Information im DE3148 übermittelt werden
1848    fn evaluate_530(&self, _ctx: &EvaluationContext) -> ConditionResult {
1849        // Hinweis: Es darf nur eine Information im DE3148 übermittelt werden — informational note, always applies
1850        ConditionResult::True
1851    }
1852
1853    /// [531] Hinweis: Für weitere Details siehe Kapitel 4.1 "Übermittlung einer Vielzahl von Berechnungsformeln in einem Vorgang"
1854    fn evaluate_531(&self, _ctx: &EvaluationContext) -> ConditionResult {
1855        // Hinweis: Für weitere Details siehe Kapitel 4.1 "Übermittlung einer Vielzahl von Berechnungsformeln in einem Vorgang" — informational note, always applies
1856        ConditionResult::True
1857    }
1858
1859    /// [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...
1860    // 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)
1861    fn evaluate_532(&self, ctx: &EvaluationContext) -> ConditionResult {
1862        {
1863            // Collect Zeitraum-IDs (DE1156 = elements[0][2]) from SG6 RFF+Z49 and RFF+Z53
1864            let rff_segments = ctx.find_segments("RFF");
1865            let zeitraum_ids: Vec<String> = rff_segments
1866                .iter()
1867                .filter(|s| {
1868                    s.elements
1869                        .first()
1870                        .and_then(|e| e.first())
1871                        .map(|q| q == "Z49" || q == "Z53")
1872                        .unwrap_or(false)
1873                })
1874                .filter_map(|s| {
1875                    s.elements
1876                        .first()
1877                        .and_then(|e| e.get(2))
1878                        .filter(|v| !v.is_empty())
1879                        .cloned()
1880                })
1881                .collect();
1882
1883            if zeitraum_ids.is_empty() {
1884                return ConditionResult::Unknown;
1885            }
1886
1887            // Check SG8 RFF+Z46 references (DE1154 = elements[0][1]) against collected Zeitraum-IDs
1888            let rff_z46_segments = ctx.find_segments_with_qualifier("RFF", 0, "Z46");
1889            if rff_z46_segments.is_empty() {
1890                return ConditionResult::Unknown;
1891            }
1892
1893            let any_match = rff_z46_segments.iter().any(|s| {
1894                s.elements
1895                    .first()
1896                    .and_then(|e| e.get(1))
1897                    .map(|ref_id| zeitraum_ids.iter().any(|zid| zid == ref_id))
1898                    .unwrap_or(false)
1899            });
1900
1901            ConditionResult::from(any_match)
1902        }
1903    }
1904
1905    /// [533] Hinweis: Für jeden übermittelten Zeitraum aus der Übermittlung der Berechnungsformel ist genau einmal das Segement anzugeben
1906    fn evaluate_533(&self, _ctx: &EvaluationContext) -> ConditionResult {
1907        // Hinweis: Für jeden übermittelten Zeitraum aus der Übermittlung der Berechnungsformel ist genau einmal das Segment anzugeben — informational note, always applies
1908        ConditionResult::True
1909    }
1910
1911    /// [534] Hinweis: Wert aus SG5 IDE+24 DE7402 mit der die Übermitt-lung der Berechnungsformel erfolgt ist.
1912    fn evaluate_534(&self, _ctx: &EvaluationContext) -> ConditionResult {
1913        // Hinweis: Wert aus SG5 IDE+24 DE7402 — value comes from the Vorgangsnummer of the Berechnungsformel transmission; informational note, always applies
1914        ConditionResult::True
1915    }
1916
1917    /// [912] Format: Wert kann mit maximal 6 Nachkommastellen angegeben werden
1918    // 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)
1919    fn evaluate_912(&self, ctx: &EvaluationContext) -> ConditionResult {
1920        ctx.format_check("QTY", 0, 1, |val| validate_max_decimal_places(val, 6))
1921    }
1922
1923    /// [913] Format: Mögliche Werte: 1 bis 99999
1924    // 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)
1925    fn evaluate_913(&self, ctx: &EvaluationContext) -> ConditionResult {
1926        // Format: Mögliche Werte: 1 bis 99999
1927        let segs = ctx.find_segments("QTY");
1928        match segs
1929            .first()
1930            .and_then(|s| s.elements.first())
1931            .and_then(|e| e.get(1))
1932        {
1933            Some(val) => {
1934                let ge1 = validate_numeric(val, ">=", 1.0);
1935                let le99999 = validate_numeric(val, "<=", 99999.0);
1936                match (ge1, le99999) {
1937                    (ConditionResult::True, ConditionResult::True) => ConditionResult::True,
1938                    (ConditionResult::False, _) | (_, ConditionResult::False) => {
1939                        ConditionResult::False
1940                    }
1941                    _ => ConditionResult::Unknown,
1942                }
1943            }
1944            None => ConditionResult::False, // segment absent → condition not applicable
1945        }
1946    }
1947
1948    /// [914] Format: Möglicher Wert: &gt; 0
1949    // REVIEW: Format condition: value must be strictly greater than 0. Applied to QTY value element; validate_numeric with '>' operator handles this directly. (medium confidence)
1950    fn evaluate_914(&self, ctx: &EvaluationContext) -> ConditionResult {
1951        ctx.format_check("QTY", 0, 1, |val| validate_numeric(val, ">", 0.0))
1952    }
1953
1954    /// [915] Format: Möglicher Wert: ≠ 1
1955    // 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)
1956    fn evaluate_915(&self, ctx: &EvaluationContext) -> ConditionResult {
1957        ctx.format_check("QTY", 0, 1, |val| validate_numeric(val, "!=", 1.0))
1958    }
1959
1960    /// [930] Format: max. 2 Nachkommastellen
1961    fn evaluate_930(&self, ctx: &EvaluationContext) -> ConditionResult {
1962        ctx.format_check("QTY", 0, 1, |val| validate_max_decimal_places(val, 2))
1963    }
1964
1965    /// [931] Format: ZZZ = +00
1966    fn evaluate_931(&self, ctx: &EvaluationContext) -> ConditionResult {
1967        ctx.format_check("DTM", 0, 1, validate_timezone_utc)
1968    }
1969
1970    /// [932] Format: HHMM = 2200
1971    fn evaluate_932(&self, ctx: &EvaluationContext) -> ConditionResult {
1972        ctx.format_check("DTM", 0, 1, |val| validate_hhmm_equals(val, "2200"))
1973    }
1974
1975    /// [933] Format: HHMM = 2300
1976    fn evaluate_933(&self, ctx: &EvaluationContext) -> ConditionResult {
1977        ctx.format_check("DTM", 0, 1, |val| validate_hhmm_equals(val, "2300"))
1978    }
1979
1980    /// [937] Format: keine Nachkommastelle
1981    fn evaluate_937(&self, ctx: &EvaluationContext) -> ConditionResult {
1982        ctx.format_check("QTY", 0, 1, |val| validate_max_decimal_places(val, 0))
1983    }
1984
1985    /// [939] Format: Die Zeichenkette muss die Zeichen @ und . enthalten
1986    fn evaluate_939(&self, ctx: &EvaluationContext) -> ConditionResult {
1987        ctx.format_check("COM", 0, 0, validate_email)
1988    }
1989
1990    /// [940] Format: Die Zeichenkette muss mit dem Zeichen + beginnen und danach dürfen nur noch Ziffern folgen
1991    fn evaluate_940(&self, ctx: &EvaluationContext) -> ConditionResult {
1992        ctx.format_check("COM", 0, 0, validate_phone)
1993    }
1994
1995    /// [947] Format: MMDDHHMM = 12312300
1996    fn evaluate_947(&self, ctx: &EvaluationContext) -> ConditionResult {
1997        ctx.format_check("DTM", 0, 1, |val| validate_mmddhhmm_equals(val, "12312300"))
1998    }
1999
2000    /// [950] Format: Marktlokations-ID
2001    fn evaluate_950(&self, ctx: &EvaluationContext) -> ConditionResult {
2002        ctx.format_check_qualified("LOC", 0, "Z16", 1, 0, validate_malo_id)
2003    }
2004
2005    /// [951] Format: Zählpunktbezeichnung
2006    // 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)
2007    fn evaluate_951(&self, ctx: &EvaluationContext) -> ConditionResult {
2008        ctx.format_check_qualified("LOC", 0, "Z19", 1, 0, validate_zahlpunkt)
2009    }
2010
2011    /// [960] Format: Netzlokations-ID
2012    // HAND-EDITED: a Netzlokations-ID is "E" + ten alphanumerics (`E1688117482`),
2013    // not MaLo-shaped; `validate_malo_id` rejected every real one (#173).
2014    fn evaluate_960(&self, ctx: &EvaluationContext) -> ConditionResult {
2015        ctx.format_check_qualified("LOC", 0, "Z18", 1, 0, validate_nelo_id)
2016    }
2017
2018    /// [963] Format: Möglicher Wert: ≤ 100
2019    // REVIEW: Format condition: value must be <= 100. Applied to QTY segment value (element 0, component 1). Uses validate_numeric helper. (medium confidence)
2020    fn evaluate_963(&self, ctx: &EvaluationContext) -> ConditionResult {
2021        ctx.format_check("QTY", 0, 1, |val| validate_numeric(val, "<=", 100.0))
2022    }
2023
2024    /// [964] Format: HHMM ≥ 0000
2025    // 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)
2026    fn evaluate_964(&self, ctx: &EvaluationContext) -> ConditionResult {
2027        ctx.format_check("DTM", 0, 1, |val| validate_hhmm_range(val, "0000", "2359"))
2028    }
2029
2030    /// [965] Format: HHMM ≤ 2359
2031    // 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)
2032    fn evaluate_965(&self, ctx: &EvaluationContext) -> ConditionResult {
2033        ctx.format_check("DTM", 0, 1, |val| validate_hhmm_range(val, "0000", "2359"))
2034    }
2035
2036    /// [969] Format: Möglicher Wer: ≤ 1
2037    // 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)
2038    fn evaluate_969(&self, ctx: &EvaluationContext) -> ConditionResult {
2039        ctx.format_check("QTY", 0, 1, |val| validate_numeric(val, "<=", 1.0))
2040    }
2041
2042    /// [2001] Segment bzw. Segmentgruppe ist genau einmal anzugeben
2043    fn evaluate_2001(&self, _ctx: &EvaluationContext) -> ConditionResult {
2044        // Hinweis: Segment bzw. Segmentgruppe ist genau einmal anzugeben — informational cardinality note, always applies
2045        ConditionResult::True
2046    }
2047
2048    /// [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...
2049    // 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)
2050    fn evaluate_2002(&self, ctx: &EvaluationContext) -> ConditionResult {
2051        // HAND-EDITED: "mindestens zwei Register, bei denen … RFF+Z27 mit diesem Code gefüllt ist" — compared with the other instances when the
2052        // scope reaches an SG8; the message as before otherwise.
2053        if let Some(scope) = ctx.enclosing_scope("SG8") {
2054            let codes = |path: &[(&str, usize)]| -> Vec<String> {
2055                ctx.find_segments_at(path, "RFF")
2056                    .iter()
2057                    .filter(|s| s.get_element(0) == "Z27")
2058                    .map(|s| s.get_component(0, 1).to_string())
2059                    .filter(|v| !v.is_empty())
2060                    .collect()
2061            };
2062            let own = codes(scope);
2063            if own.is_empty() {
2064                return ConditionResult::Unknown;
2065            }
2066            let sg8s = ctx.scopes_under("SG5", "SG8").unwrap_or_default();
2067            return ConditionResult::from(
2068                own.iter()
2069                    .all(|code| sg8s.iter().filter(|p| codes(p).contains(code)).count() >= 2),
2070            );
2071        }
2072        // Collect CCI+Z39 codes from SG9 (Code der Zählzeitdefinition) message-wide
2073        let cci_segments = ctx.find_segments("CCI");
2074        let codes: Vec<String> = cci_segments
2075            .iter()
2076            .filter(|s| {
2077                s.elements
2078                    .first()
2079                    .and_then(|e| e.first())
2080                    .is_some_and(|v| v == "Z39")
2081            })
2082            .filter_map(|s| s.elements.get(2).and_then(|e| e.first()).cloned())
2083            .filter(|c| !c.is_empty())
2084            .collect();
2085        if codes.is_empty() {
2086            return ConditionResult::Unknown;
2087        }
2088        // For each code, at least 2 SG8 instances must have RFF+Z27 referencing that code
2089        let rff_z27 = ctx.find_segments_with_qualifier("RFF", 0, "Z27");
2090        for code in &codes {
2091            let count = rff_z27
2092                .iter()
2093                .filter(|s| {
2094                    s.elements
2095                        .first()
2096                        .and_then(|e| e.get(1))
2097                        .is_some_and(|v| v == code)
2098                })
2099                .count();
2100            if count < 2 {
2101                return ConditionResult::False;
2102            }
2103        }
2104        ConditionResult::True
2105    }
2106
2107    /// [2004] Segment ist genau einmal für jede Zeitraum-ID aus dem DE1156 der SG6 RFF+Z49 (Verwendungszeitraum der Daten: "Gültige Daten") anzugeben
2108    // 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)
2109    fn evaluate_2004(&self, ctx: &EvaluationContext) -> ConditionResult {
2110        // Collect Zeitraum-IDs from SG6 RFF+Z49 DE1156 (elements[0][2])
2111        let rff_z49 = ctx.find_segments_with_qualifier("RFF", 0, "Z49");
2112        if rff_z49.is_empty() {
2113            return ConditionResult::Unknown;
2114        }
2115        let zeitraum_ids: Vec<String> = rff_z49
2116            .iter()
2117            .filter_map(|s| s.elements.first().and_then(|e| e.get(2)).cloned())
2118            .filter(|id| !id.is_empty())
2119            .collect();
2120        if zeitraum_ids.is_empty() {
2121            return ConditionResult::Unknown;
2122        }
2123        let sts_segments = ctx.find_segments("STS");
2124        for zid in &zeitraum_ids {
2125            // STS+E01: Zeitraum-ID at elements[2][3] (DE9012)
2126            // STS+Z23: Zeitraum-ID at elements[2][0] (DE9013)
2127            let count = sts_segments
2128                .iter()
2129                .filter(|s| {
2130                    let qual = s
2131                        .elements
2132                        .first()
2133                        .and_then(|e| e.first())
2134                        .map(|v| v.as_str())
2135                        .unwrap_or("");
2136                    match qual {
2137                        "E01" => s
2138                            .elements
2139                            .get(2)
2140                            .and_then(|e| e.get(3))
2141                            .is_some_and(|v| v == zid),
2142                        "Z23" => s
2143                            .elements
2144                            .get(2)
2145                            .and_then(|e| e.first())
2146                            .is_some_and(|v| v == zid),
2147                        _ => false,
2148                    }
2149                })
2150                .count();
2151            if count != 1 {
2152                return ConditionResult::False;
2153            }
2154        }
2155        ConditionResult::True
2156    }
2157
2158    /// [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
2159    // 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)
2160    fn evaluate_2005(&self, ctx: &EvaluationContext) -> ConditionResult {
2161        // Collect Zeitraum-IDs from STS+E01 where DE9013 (elements[2][0]) = A99 (Sonstiges)
2162        let sts_e01 = ctx.find_segments_with_qualifier("STS", 0, "E01");
2163        let zeitraum_ids: Vec<String> = sts_e01
2164            .iter()
2165            .filter(|s| {
2166                s.elements
2167                    .get(2)
2168                    .and_then(|e| e.first())
2169                    .is_some_and(|v| v == "A99")
2170            })
2171            .filter_map(|s| s.elements.get(2).and_then(|e| e.get(3)).cloned())
2172            .filter(|id| !id.is_empty())
2173            .collect();
2174        if zeitraum_ids.is_empty() {
2175            return ConditionResult::Unknown;
2176        }
2177        // For each Zeitraum-ID, exactly one SG8 RFF+Z46 (Referenz auf Zeitraum-ID) must reference it
2178        let rff_z46 = ctx.find_segments_with_qualifier("RFF", 0, "Z46");
2179        for zid in &zeitraum_ids {
2180            let count = rff_z46
2181                .iter()
2182                .filter(|s| {
2183                    s.elements
2184                        .first()
2185                        .and_then(|e| e.get(1))
2186                        .is_some_and(|v| v == zid)
2187                })
2188                .count();
2189            if count != 1 {
2190                return ConditionResult::False;
2191            }
2192        }
2193        ConditionResult::True
2194    }
2195
2196    /// [2006] Segmentgruppe ist mindestens einmal für jede Zeitraum-ID aus dem DE9013 der SG5 STS+Z23+Z33 (Berechnungsformel angefügt) anzugeben
2197    // 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)
2198    fn evaluate_2006(&self, ctx: &EvaluationContext) -> ConditionResult {
2199        // Collect Zeitraum-IDs from STS+Z23 (Berechnungsformel) where status code = Z33 (angefügt)
2200        let sts_z23 = ctx.find_segments_with_qualifier("STS", 0, "Z23");
2201        let zeitraum_ids: Vec<String> = sts_z23
2202            .iter()
2203            .filter(|s| {
2204                s.elements
2205                    .get(1)
2206                    .and_then(|e| e.first())
2207                    .is_some_and(|v| v == "Z33")
2208            })
2209            .filter_map(|s| s.elements.get(2).and_then(|e| e.first()).cloned())
2210            .filter(|id| !id.is_empty())
2211            .collect();
2212        if zeitraum_ids.is_empty() {
2213            return ConditionResult::Unknown;
2214        }
2215        // For each Zeitraum-ID, at least one SG8 RFF+Z46 must reference it
2216        let rff_z46 = ctx.find_segments_with_qualifier("RFF", 0, "Z46");
2217        for zid in &zeitraum_ids {
2218            let count = rff_z46
2219                .iter()
2220                .filter(|s| {
2221                    s.elements
2222                        .first()
2223                        .and_then(|e| e.get(1))
2224                        .is_some_and(|v| v == zid)
2225                })
2226                .count();
2227            if count < 1 {
2228                return ConditionResult::False;
2229            }
2230        }
2231        ConditionResult::True
2232    }
2233
2234    /// [2007] Segmentgruppe ist genau einmal für jede Zeitraum-ID aus dem DE9013 der SG5 STS+Z23+Z33 (Berechnungsformel angefügt) anzugeben
2235    // 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)
2236    fn evaluate_2007(&self, ctx: &EvaluationContext) -> ConditionResult {
2237        // Condition 2007: Segment group required exactly once per Zeitraum-ID from SG5 STS+Z23+Z33
2238        // Evaluates to True when at least one STS in SG5 has Statuskategorie=Z23 and Status=Z33
2239        // (Berechnungsformel angefügt), indicating the group must be present for that Zeitraum-ID.
2240        // STS Z23 structure: elements[0][0]=Z23 (Statuskategorie), elements[1][0]=Z33 (Status), elements[2][0]=Zeitraum-ID
2241        let sts_segments = ctx.find_segments("STS");
2242        let has_z23_z33 = sts_segments.iter().any(|s| {
2243            s.elements
2244                .first()
2245                .and_then(|e| e.first())
2246                .is_some_and(|v| v == "Z23")
2247                && s.elements
2248                    .get(1)
2249                    .and_then(|e| e.first())
2250                    .is_some_and(|v| v == "Z33")
2251        });
2252        ConditionResult::from(has_z23_z33)
2253    }
2254}