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