Skip to main content

automapper_validation/generated/fv2510/
mscons_conditions_fv2510.rs

1// <auto-generated>
2// Generated by automapper-generator generate-conditions
3// AHB: xml-migs-and-ahbs/FV2510/MSCONS_AHB_3_1f_Fehlerkorrektur_20250623.xml
4// Generated: 2026-03-12T10:50:55Z
5// </auto-generated>
6
7// LLM-produced bodies aren't idiomatic Rust — silence stylistic lints at
8// file scope so `cargo clippy -D warnings` stays green. Correctness,
9// suspicious, and perf categories stay on so real bugs still surface.
10#![allow(clippy::style, clippy::complexity)]
11
12#[allow(unused_imports)]
13use crate::eval::format_validators::*;
14use crate::eval::{ConditionEvaluator, ConditionResult, EvaluationContext};
15
16/// Generated condition evaluator for MSCONS FV2510.
17pub struct MsconsConditionEvaluatorFV2510 {
18    // External condition IDs that require runtime context.
19    external_conditions: std::collections::HashSet<u32>,
20}
21
22impl Default for MsconsConditionEvaluatorFV2510 {
23    fn default() -> Self {
24        let mut external_conditions = std::collections::HashSet::new();
25        external_conditions.insert(1);
26        external_conditions.insert(22);
27        external_conditions.insert(32);
28        external_conditions.insert(33);
29        external_conditions.insert(35);
30        external_conditions.insert(36);
31        external_conditions.insert(42);
32        external_conditions.insert(67);
33        external_conditions.insert(77);
34        external_conditions.insert(80);
35        external_conditions.insert(81);
36        external_conditions.insert(97);
37        external_conditions.insert(117);
38        external_conditions.insert(118);
39        external_conditions.insert(126);
40        external_conditions.insert(127);
41        external_conditions.insert(138);
42        external_conditions.insert(141);
43        external_conditions.insert(492);
44        external_conditions.insert(493);
45        Self {
46            external_conditions,
47        }
48    }
49}
50
51impl ConditionEvaluator for MsconsConditionEvaluatorFV2510 {
52    fn message_type(&self) -> &str {
53        "MSCONS"
54    }
55
56    fn format_version(&self) -> &str {
57        "FV2510"
58    }
59
60    fn evaluate(&self, condition: u32, ctx: &EvaluationContext) -> ConditionResult {
61        match condition {
62            1 => self.evaluate_1(ctx),
63            2 => self.evaluate_2(ctx),
64            11 => self.evaluate_11(ctx),
65            12 => self.evaluate_12(ctx),
66            17 => self.evaluate_17(ctx),
67            18 => self.evaluate_18(ctx),
68            22 => self.evaluate_22(ctx),
69            27 => self.evaluate_27(ctx),
70            28 => self.evaluate_28(ctx),
71            32 => self.evaluate_32(ctx),
72            33 => self.evaluate_33(ctx),
73            35 => self.evaluate_35(ctx),
74            36 => self.evaluate_36(ctx),
75            38 => self.evaluate_38(ctx),
76            42 => self.evaluate_42(ctx),
77            45 => self.evaluate_45(ctx),
78            46 => self.evaluate_46(ctx),
79            48 => self.evaluate_48(ctx),
80            49 => self.evaluate_49(ctx),
81            50 => self.evaluate_50(ctx),
82            51 => self.evaluate_51(ctx),
83            62 => self.evaluate_62(ctx),
84            67 => self.evaluate_67(ctx),
85            68 => self.evaluate_68(ctx),
86            69 => self.evaluate_69(ctx),
87            70 => self.evaluate_70(ctx),
88            71 => self.evaluate_71(ctx),
89            72 => self.evaluate_72(ctx),
90            73 => self.evaluate_73(ctx),
91            77 => self.evaluate_77(ctx),
92            78 => self.evaluate_78(ctx),
93            79 => self.evaluate_79(ctx),
94            80 => self.evaluate_80(ctx),
95            81 => self.evaluate_81(ctx),
96            83 => self.evaluate_83(ctx),
97            84 => self.evaluate_84(ctx),
98            85 => self.evaluate_85(ctx),
99            86 => self.evaluate_86(ctx),
100            87 => self.evaluate_87(ctx),
101            88 => self.evaluate_88(ctx),
102            90 => self.evaluate_90(ctx),
103            91 => self.evaluate_91(ctx),
104            92 => self.evaluate_92(ctx),
105            93 => self.evaluate_93(ctx),
106            94 => self.evaluate_94(ctx),
107            95 => self.evaluate_95(ctx),
108            96 => self.evaluate_96(ctx),
109            97 => self.evaluate_97(ctx),
110            98 => self.evaluate_98(ctx),
111            99 => self.evaluate_99(ctx),
112            100 => self.evaluate_100(ctx),
113            101 => self.evaluate_101(ctx),
114            108 => self.evaluate_108(ctx),
115            111 => self.evaluate_111(ctx),
116            113 => self.evaluate_113(ctx),
117            117 => self.evaluate_117(ctx),
118            118 => self.evaluate_118(ctx),
119            119 => self.evaluate_119(ctx),
120            121 => self.evaluate_121(ctx),
121            125 => self.evaluate_125(ctx),
122            126 => self.evaluate_126(ctx),
123            127 => self.evaluate_127(ctx),
124            128 => self.evaluate_128(ctx),
125            129 => self.evaluate_129(ctx),
126            130 => self.evaluate_130(ctx),
127            131 => self.evaluate_131(ctx),
128            132 => self.evaluate_132(ctx),
129            133 => self.evaluate_133(ctx),
130            134 => self.evaluate_134(ctx),
131            135 => self.evaluate_135(ctx),
132            138 => self.evaluate_138(ctx),
133            141 => self.evaluate_141(ctx),
134            142 => self.evaluate_142(ctx),
135            143 => self.evaluate_143(ctx),
136            144 => self.evaluate_144(ctx),
137            145 => self.evaluate_145(ctx),
138            146 => self.evaluate_146(ctx),
139            147 => self.evaluate_147(ctx),
140            148 => self.evaluate_148(ctx),
141            149 => self.evaluate_149(ctx),
142            150 => self.evaluate_150(ctx),
143            490 => self.evaluate_490(ctx),
144            491 => self.evaluate_491(ctx),
145            492 => self.evaluate_492(ctx),
146            493 => self.evaluate_493(ctx),
147            494 => self.evaluate_494(ctx),
148            495 => self.evaluate_495(ctx),
149            501 => self.evaluate_501(ctx),
150            502 => self.evaluate_502(ctx),
151            506 => self.evaluate_506(ctx),
152            509 => self.evaluate_509(ctx),
153            510 => self.evaluate_510(ctx),
154            511 => self.evaluate_511(ctx),
155            512 => self.evaluate_512(ctx),
156            513 => self.evaluate_513(ctx),
157            514 => self.evaluate_514(ctx),
158            515 => self.evaluate_515(ctx),
159            516 => self.evaluate_516(ctx),
160            518 => self.evaluate_518(ctx),
161            519 => self.evaluate_519(ctx),
162            520 => self.evaluate_520(ctx),
163            522 => self.evaluate_522(ctx),
164            523 => self.evaluate_523(ctx),
165            524 => self.evaluate_524(ctx),
166            525 => self.evaluate_525(ctx),
167            526 => self.evaluate_526(ctx),
168            528 => self.evaluate_528(ctx),
169            529 => self.evaluate_529(ctx),
170            530 => self.evaluate_530(ctx),
171            531 => self.evaluate_531(ctx),
172            532 => self.evaluate_532(ctx),
173            535 => self.evaluate_535(ctx),
174            538 => self.evaluate_538(ctx),
175            541 => self.evaluate_541(ctx),
176            543 => self.evaluate_543(ctx),
177            544 => self.evaluate_544(ctx),
178            545 => self.evaluate_545(ctx),
179            546 => self.evaluate_546(ctx),
180            547 => self.evaluate_547(ctx),
181            551 => self.evaluate_551(ctx),
182            553 => self.evaluate_553(ctx),
183            554 => self.evaluate_554(ctx),
184            556 => self.evaluate_556(ctx),
185            557 => self.evaluate_557(ctx),
186            558 => self.evaluate_558(ctx),
187            559 => self.evaluate_559(ctx),
188            560 => self.evaluate_560(ctx),
189            566 => self.evaluate_566(ctx),
190            567 => self.evaluate_567(ctx),
191            568 => self.evaluate_568(ctx),
192            569 => self.evaluate_569(ctx),
193            570 => self.evaluate_570(ctx),
194            571 => self.evaluate_571(ctx),
195            572 => self.evaluate_572(ctx),
196            573 => self.evaluate_573(ctx),
197            574 => self.evaluate_574(ctx),
198            575 => self.evaluate_575(ctx),
199            576 => self.evaluate_576(ctx),
200            577 => self.evaluate_577(ctx),
201            902 => self.evaluate_902(ctx),
202            904 => self.evaluate_904(ctx),
203            905 => self.evaluate_905(ctx),
204            906 => self.evaluate_906(ctx),
205            907 => self.evaluate_907(ctx),
206            908 => self.evaluate_908(ctx),
207            909 => self.evaluate_909(ctx),
208            910 => self.evaluate_910(ctx),
209            917 => self.evaluate_917(ctx),
210            918 => self.evaluate_918(ctx),
211            922 => self.evaluate_922(ctx),
212            925 => self.evaluate_925(ctx),
213            931 => self.evaluate_931(ctx),
214            932 => self.evaluate_932(ctx),
215            933 => self.evaluate_933(ctx),
216            934 => self.evaluate_934(ctx),
217            935 => self.evaluate_935(ctx),
218            937 => self.evaluate_937(ctx),
219            939 => self.evaluate_939(ctx),
220            940 => self.evaluate_940(ctx),
221            950 => self.evaluate_950(ctx),
222            951 => self.evaluate_951(ctx),
223            960 => self.evaluate_960(ctx),
224            2001 => self.evaluate_2001(ctx),
225            2002 => self.evaluate_2002(ctx),
226            2003 => self.evaluate_2003(ctx),
227            _ => ConditionResult::Unknown,
228        }
229    }
230
231    fn is_external(&self, condition: u32) -> bool {
232        self.external_conditions.contains(&condition)
233    }
234    fn is_known(&self, condition: u32) -> bool {
235        matches!(
236            condition,
237            1 | 2
238                | 11
239                | 12
240                | 17
241                | 18
242                | 22
243                | 27
244                | 28
245                | 32
246                | 33
247                | 35
248                | 36
249                | 38
250                | 42
251                | 45
252                | 46
253                | 48
254                | 49
255                | 50
256                | 51
257                | 62
258                | 67
259                | 68
260                | 69
261                | 70
262                | 71
263                | 72
264                | 73
265                | 77
266                | 78
267                | 79
268                | 80
269                | 81
270                | 83
271                | 84
272                | 85
273                | 86
274                | 87
275                | 88
276                | 90
277                | 91
278                | 92
279                | 93
280                | 94
281                | 95
282                | 96
283                | 97
284                | 98
285                | 99
286                | 100
287                | 101
288                | 108
289                | 111
290                | 113
291                | 117
292                | 118
293                | 119
294                | 121
295                | 125
296                | 126
297                | 127
298                | 128
299                | 129
300                | 130
301                | 131
302                | 132
303                | 133
304                | 134
305                | 135
306                | 138
307                | 141
308                | 142
309                | 143
310                | 144
311                | 145
312                | 146
313                | 147
314                | 148
315                | 149
316                | 150
317                | 490
318                | 491
319                | 492
320                | 493
321                | 494
322                | 495
323                | 501
324                | 502
325                | 506
326                | 509
327                | 510
328                | 511
329                | 512
330                | 513
331                | 514
332                | 515
333                | 516
334                | 518
335                | 519
336                | 520
337                | 522
338                | 523
339                | 524
340                | 525
341                | 526
342                | 528
343                | 529
344                | 530
345                | 531
346                | 532
347                | 535
348                | 538
349                | 541
350                | 543
351                | 544
352                | 545
353                | 546
354                | 547
355                | 551
356                | 553
357                | 554
358                | 556
359                | 557
360                | 558
361                | 559
362                | 560
363                | 566
364                | 567
365                | 568
366                | 569
367                | 570
368                | 571
369                | 572
370                | 573
371                | 574
372                | 575
373                | 576
374                | 577
375                | 902
376                | 904
377                | 905
378                | 906
379                | 907
380                | 908
381                | 909
382                | 910
383                | 917
384                | 918
385                | 922
386                | 925
387                | 931
388                | 932
389                | 933
390                | 934
391                | 935
392                | 937
393                | 939
394                | 940
395                | 950
396                | 951
397                | 960
398                | 2001
399                | 2002
400                | 2003
401        )
402    }
403}
404
405impl MsconsConditionEvaluatorFV2510 {
406    /// [81] Wenn MP-ID in SG2 NAD+MS in der Rolle ÜNB
407    /// EXTERNAL: Requires context from outside the message.
408    fn evaluate_81(&self, ctx: &EvaluationContext) -> ConditionResult {
409        ctx.external.evaluate("sender_is_uenb")
410    }
411
412    /// [126] wenn Plausibilisierungshinweise vorliegen
413    /// EXTERNAL: Requires context from outside the message.
414    fn evaluate_126(&self, ctx: &EvaluationContext) -> ConditionResult {
415        ctx.external.evaluate("plausibility_notes_present")
416    }
417
418    /// [127] wenn ein Korrekturgrund anzugeben ist
419    /// EXTERNAL: Requires context from outside the message.
420    fn evaluate_127(&self, ctx: &EvaluationContext) -> ConditionResult {
421        ctx.external.evaluate("correction_reason_required")
422    }
423
424    /// [130] Wenn innerhalb desselben LIN-Segments neben diesem Segment (SG10 DTM+7 Nutzungszeitpunkt) noch das SG10 DTM+60 (Ausführungs- / Änderungszeitpunkt) oder das SG10 DTM+9 (Ablesedatum) vorhanden, dar...
425    // HAND-EDITED: shared with every MSCONS FV in `eval::reading_dates`, so
426    // that `[130] ∨ [133]` asks every partner of the DTM+7 to keep its gap
427    // (a [133] that held without a DTM+9 let any DTM+60 gap pass).
428    fn evaluate_130(&self, ctx: &EvaluationContext) -> ConditionResult {
429        crate::eval::reading_dates::condition_130(ctx)
430    }
431
432    /// [1] Sofern per ORDERS angefordert
433    /// EXTERNAL: Requires context from outside the message.
434    fn evaluate_1(&self, ctx: &EvaluationContext) -> ConditionResult {
435        ctx.external.evaluate("requested_via_orders")
436    }
437
438    /// [2] Wenn das Zeitintervall zwischen ersten SG10 DTM+163 und letzten SG10 DTM+164 mindestens einen Monat umfasst
439    // REVIEW: Parses year+month from DTM+163 (first) and DTM+164 (last) in format 303 (YYYYMMDDHHmm). Computes calendar-month difference. 'Mindestens einen Monat' interpreted as end month > start month (calendar boundary). Medium confidence due to ambiguity between calendar-month vs. 30-day interpretation. (medium confidence)
440    fn evaluate_2(&self, ctx: &EvaluationContext) -> ConditionResult {
441        let dtm_163 = ctx.find_segments_with_qualifier("DTM", 0, "163");
442        let dtm_164 = ctx.find_segments_with_qualifier("DTM", 0, "164");
443        let first_start = dtm_163
444            .first()
445            .and_then(|s| s.elements.first())
446            .and_then(|e| e.get(1))
447            .map(|s| s.as_str());
448        let last_end = dtm_164
449            .last()
450            .and_then(|s| s.elements.first())
451            .and_then(|e| e.get(1))
452            .map(|s| s.as_str());
453        match (first_start, last_end) {
454            (Some(start), Some(end)) if start.len() >= 6 && end.len() >= 6 => {
455                let start_year: u32 = start[0..4].parse().unwrap_or(0);
456                let start_month: u32 = start[4..6].parse().unwrap_or(0);
457                let end_year: u32 = end[0..4].parse().unwrap_or(0);
458                let end_month: u32 = end[4..6].parse().unwrap_or(0);
459                let start_total = start_year * 12 + start_month;
460                let end_total = end_year * 12 + end_month;
461                ConditionResult::from(end_total >= start_total + 1)
462            }
463            _ => ConditionResult::Unknown,
464        }
465    }
466
467    /// [11] Wenn SG9 PIA+5+7-0?:52.0.22/7-0?:54.0.16/7-0?:54.0.20/7-0?:54.0.22
468    fn evaluate_11(&self, ctx: &EvaluationContext) -> ConditionResult {
469        let pia_segments = ctx.find_segments_with_qualifier("PIA", 0, "5");
470        let obis_values = ["7-0:52.0.22", "7-0:54.0.16", "7-0:54.0.20", "7-0:54.0.22"];
471        ConditionResult::from(pia_segments.iter().any(|s| {
472            s.elements
473                .get(1)
474                .and_then(|e| e.first())
475                .is_some_and(|v| obis_values.contains(&v.as_str()))
476        }))
477    }
478
479    /// [12] Wenn nicht SG9 PIA+5+7-0?:52.0.22/7-0?:54.0.16/7-0?:54.0.20/7-0?:54.0.22
480    fn evaluate_12(&self, ctx: &EvaluationContext) -> ConditionResult {
481        let pia_segments = ctx.find_segments_with_qualifier("PIA", 0, "5");
482        let obis_values = ["7-0:52.0.22", "7-0:54.0.16", "7-0:54.0.20", "7-0:54.0.22"];
483        ConditionResult::from(!pia_segments.iter().any(|s| {
484            s.elements
485                .get(1)
486                .and_then(|e| e.first())
487                .is_some_and(|v| obis_values.contains(&v.as_str()))
488        }))
489    }
490
491    /// [17] Wenn nicht SG9 PIA+5+1-b?:9.99.0 (b= Kanal: Wert gemäß Codeliste der OBIS-Kennzahlen und Medien)
492    // REVIEW: Checks absence of PIA+5 with OBIS matching pattern '1-{channel}:9.99.0'. The 'b' is a variable channel, so pattern-matching on prefix '1-' and suffix ':9.99.0' is used. Negated because condition is 'Wenn NICHT'. Medium confidence due to pattern-based matching of OBIS code. (medium confidence)
493    fn evaluate_17(&self, ctx: &EvaluationContext) -> ConditionResult {
494        let pia_segments = ctx.find_segments_with_qualifier("PIA", 0, "5");
495        let has_match = pia_segments.iter().any(|s| {
496            s.elements
497                .get(1)
498                .and_then(|e| e.first())
499                .is_some_and(|v| v.starts_with("1-") && v.ends_with(":9.99.0"))
500        });
501        ConditionResult::from(!has_match)
502    }
503
504    /// [18] Wenn SG9 PIA+5+1-b?:9.99.0 (b= Kanal: Wert gemäß Codeliste der OBIS-Kennzahlen und Medien)
505    // REVIEW: Affirmative version of condition 17. True when any PIA+5 has OBIS code matching the '1-{channel}:9.99.0' pattern (combined heat/power or special metering). Medium confidence due to pattern matching. (medium confidence)
506    fn evaluate_18(&self, ctx: &EvaluationContext) -> ConditionResult {
507        let pia_segments = ctx.find_segments_with_qualifier("PIA", 0, "5");
508        ConditionResult::from(pia_segments.iter().any(|s| {
509            s.elements
510                .get(1)
511                .and_then(|e| e.first())
512                .is_some_and(|v| v.starts_with("1-") && v.ends_with(":9.99.0"))
513        }))
514    }
515
516    /// [22] Wenn Aufteilung vorhanden
517    /// EXTERNAL: Requires context from outside the message.
518    fn evaluate_22(&self, ctx: &EvaluationContext) -> ConditionResult {
519        ctx.external.evaluate("message_splitting")
520    }
521
522    /// [27] Wenn SG9 PIA+5+1-1?:1.9.0 vorhanden
523    fn evaluate_27(&self, ctx: &EvaluationContext) -> ConditionResult {
524        let pia_segments = ctx.find_segments_with_qualifier("PIA", 0, "5");
525        ConditionResult::from(pia_segments.iter().any(|s| {
526            s.elements
527                .get(1)
528                .and_then(|e| e.first())
529                .is_some_and(|v| v == "1-1:1.9.0")
530        }))
531    }
532
533    /// [28] Wenn SG9 PIA+5+1-1?:1.9.0 nicht vorhanden
534    fn evaluate_28(&self, ctx: &EvaluationContext) -> ConditionResult {
535        let pia_segments = ctx.find_segments_with_qualifier("PIA", 0, "5");
536        ConditionResult::from(!pia_segments.iter().any(|s| {
537            s.elements
538                .get(1)
539                .and_then(|e| e.first())
540                .is_some_and(|v| v == "1-1:1.9.0")
541        }))
542    }
543
544    /// [32] wenn MP-ID in SG2 NAD+MS in der Rolle NB
545    /// EXTERNAL: Requires context from outside the message.
546    fn evaluate_32(&self, ctx: &EvaluationContext) -> ConditionResult {
547        ctx.external.evaluate("sender_is_nb")
548    }
549
550    /// [33] wenn MP-ID in SG2 NAD+MR in der Rolle LF
551    /// EXTERNAL: Requires context from outside the message.
552    fn evaluate_33(&self, ctx: &EvaluationContext) -> ConditionResult {
553        ctx.external.evaluate("recipient_is_lf")
554    }
555
556    /// [35] wenn MP-ID in SG2 NAD+MS in der Rolle MSB
557    /// EXTERNAL: Requires context from outside the message.
558    fn evaluate_35(&self, ctx: &EvaluationContext) -> ConditionResult {
559        ctx.external.evaluate("sender_is_msb")
560    }
561
562    /// [36] wenn MP-ID in SG2 NAD+MR in der Rolle NB
563    /// EXTERNAL: Requires context from outside the message.
564    fn evaluate_36(&self, ctx: &EvaluationContext) -> ConditionResult {
565        ctx.external.evaluate("recipient_is_nb")
566    }
567
568    /// [38] wenn in SG6 LOC+172 DE3225 die ID der Messlokation angegeben ist
569    fn evaluate_38(&self, ctx: &EvaluationContext) -> ConditionResult {
570        // Messlokation = 33-char alphanumeric Zählpunktbezeichnung. A non-empty
571        // value that doesn't match (e.g. an 11-digit MaLo-ID) must be False so
572        // the AHB's XOR branches against condition [119] stay mutually exclusive.
573        let loc_segments = ctx.find_segments_with_qualifier("LOC", 0, "172");
574        match loc_segments.first() {
575            Some(loc) => match loc.elements.get(1).and_then(|e| e.first()) {
576                Some(v) if !v.is_empty() => validate_zahlpunkt(v),
577                _ => ConditionResult::False,
578            },
579            None => ConditionResult::False,
580        }
581    }
582
583    /// [42] Wenn MP-ID in SG2 NAD+MR in der Rolle MSB
584    /// EXTERNAL: Requires context from outside the message.
585    fn evaluate_42(&self, ctx: &EvaluationContext) -> ConditionResult {
586        ctx.external.evaluate("recipient_is_msb")
587    }
588
589    /// [45] Wenn SG9 PIA+5+7-b:99.41.16 (b=Kanal: Wert gemäß Codeliste der OBIS-Kennzahlen und Medien) vorhanden
590    fn evaluate_45(&self, ctx: &EvaluationContext) -> ConditionResult {
591        let pias = ctx.find_segments_with_qualifier("PIA", 0, "5");
592        ConditionResult::from(pias.iter().any(|s| {
593            s.elements
594                .get(1)
595                .and_then(|e| e.first())
596                .is_some_and(|v| v.starts_with("7-") && v.ends_with(":99.41.16"))
597        }))
598    }
599
600    /// [46] Wenn Wert in SG6 LOC+172 DE3225 genau 11 Stellen
601    fn evaluate_46(&self, ctx: &EvaluationContext) -> ConditionResult {
602        let locs = ctx.find_segments_with_qualifier("LOC", 0, "172");
603        if locs.is_empty() {
604            return ConditionResult::Unknown;
605        }
606        ConditionResult::from(locs.iter().any(|s| {
607            s.elements
608                .get(1)
609                .and_then(|e| e.first())
610                .is_some_and(|v| v.len() == 11)
611        }))
612    }
613
614    /// [48] Wenn SG9 PIA+5+7-0?:52.0.22
615    fn evaluate_48(&self, ctx: &EvaluationContext) -> ConditionResult {
616        let pias = ctx.find_segments_with_qualifier("PIA", 0, "5");
617        ConditionResult::from(pias.iter().any(|s| {
618            s.elements
619                .get(1)
620                .and_then(|e| e.first())
621                .is_some_and(|v| v == "7-0:52.0.22")
622        }))
623    }
624
625    /// [49] Wenn SG9 PIA+5+7-b?:70.16.16/7-b?:70.16.20/7-b?:70.16.22 (b=Kanal: Wert gemäß Codeliste der OBIS-Kennzahlen und Medien) vorhanden
626    fn evaluate_49(&self, ctx: &EvaluationContext) -> ConditionResult {
627        let pias = ctx.find_segments_with_qualifier("PIA", 0, "5");
628        ConditionResult::from(pias.iter().any(|s| {
629            s.elements.get(1).and_then(|e| e.first()).is_some_and(|v| {
630                v.starts_with("7-")
631                    && (v.ends_with(":70.16.16")
632                        || v.ends_with(":70.16.20")
633                        || v.ends_with(":70.16.22"))
634            })
635        }))
636    }
637
638    /// [50] Wenn SG9 PIA+5+7-b?:70.18.16/7-b?:70.18.20/7-b?:70.18.22 (b=Kanal: Wert gemäß Codeliste der OBIS-Kennzahlen und Medien) vorhanden
639    fn evaluate_50(&self, ctx: &EvaluationContext) -> ConditionResult {
640        let pias = ctx.find_segments_with_qualifier("PIA", 0, "5");
641        ConditionResult::from(pias.iter().any(|s| {
642            s.elements.get(1).and_then(|e| e.first()).is_some_and(|v| {
643                v.starts_with("7-")
644                    && (v.ends_with(":70.18.16")
645                        || v.ends_with(":70.18.20")
646                        || v.ends_with(":70.18.22"))
647            })
648        }))
649    }
650
651    /// [51] Wenn SG9 PIA+5+7-0?:33.86.0 vorhanden ist, darf mittels Wiederholung SG9 LIN in derselben Nachricht das SG9 PIA+5+7-0?:52.0.22/7-0?:54.0.16/7-0?:54.0.20/7-0?:54.0.22 nicht mehr angegeben werden
652    // REVIEW: Mutual-exclusion constraint: if 7-0:33.86.0 is present, none of the listed 52/54.x codes may appear. Returns True when constraint is satisfied (i.e. 33.86.0 absent, or 33.86.0 present but excluded codes absent). Returns True when 33.86.0 is not present because the constraint simply does not apply. (medium confidence)
653    fn evaluate_51(&self, ctx: &EvaluationContext) -> ConditionResult {
654        let pias = ctx.find_segments_with_qualifier("PIA", 0, "5");
655        let has_3386 = pias.iter().any(|s| {
656            s.elements
657                .get(1)
658                .and_then(|e| e.first())
659                .is_some_and(|v| v == "7-0:33.86.0")
660        });
661        if !has_3386 {
662            return ConditionResult::True;
663        }
664        let excluded = ["7-0:52.0.22", "7-0:54.0.16", "7-0:54.0.20", "7-0:54.0.22"];
665        ConditionResult::from(!pias.iter().any(|s| {
666            s.elements
667                .get(1)
668                .and_then(|e| e.first())
669                .is_some_and(|v| excluded.contains(&v.as_str()))
670        }))
671    }
672
673    /// [62] Wenn Wert in SG6 LOC+172 DE3225 genau 33 Stellen
674    fn evaluate_62(&self, ctx: &EvaluationContext) -> ConditionResult {
675        let locs = ctx.find_segments_with_qualifier("LOC", 0, "172");
676        if locs.is_empty() {
677            return ConditionResult::Unknown;
678        }
679        ConditionResult::from(locs.iter().any(|s| {
680            s.elements
681                .get(1)
682                .and_then(|e| e.first())
683                .is_some_and(|v| v.len() == 33)
684        }))
685    }
686
687    /// [67] Wenn es sich um die Referenz auf eine ORDERS handelt
688    /// EXTERNAL: Requires context from outside the message.
689    // REVIEW: Checks whether the current MSCONS transaction references an ORDERS message. While RFF segments can carry cross-message references, the specific qualifier that marks an ORDERS reference is not provided in the segment structure reference, and determining whether a reference points to an ORDERS vs. another message type typically requires external business context about the originating transaction. (medium confidence)
690    fn evaluate_67(&self, ctx: &EvaluationContext) -> ConditionResult {
691        ctx.external.evaluate("is_orders_reference")
692    }
693
694    /// [68] Wenn BGM+7 vorhanden
695    fn evaluate_68(&self, ctx: &EvaluationContext) -> ConditionResult {
696        ctx.has_qualifier("BGM", 0, "7")
697    }
698
699    /// [69] Wenn BGM+Z28 vorhanden
700    fn evaluate_69(&self, ctx: &EvaluationContext) -> ConditionResult {
701        ctx.has_qualifier("BGM", 0, "Z28")
702    }
703
704    /// [70] Wenn BGM+BK vorhanden
705    fn evaluate_70(&self, ctx: &EvaluationContext) -> ConditionResult {
706        ctx.has_qualifier("BGM", 0, "BK")
707    }
708
709    /// [71] Wenn BGM+Z39 vorhanden
710    fn evaluate_71(&self, ctx: &EvaluationContext) -> ConditionResult {
711        ctx.has_qualifier("BGM", 0, "Z39")
712    }
713
714    /// [72] Wenn SG9 PIA+5+1-b?:1.6.0/1-b?:3.6.0/1-b?:4.6.0/1-66?:13.6.0/1-66?:14.6.0 (b=Kanal: Wert gemäß Codeliste der OBIS-Kennzahlen und Medien) vorhanden
715    fn evaluate_72(&self, ctx: &EvaluationContext) -> ConditionResult {
716        let pias = ctx.find_segments_with_qualifier("PIA", 0, "5");
717        ConditionResult::from(pias.iter().any(|s| {
718            s.elements.get(1).and_then(|e| e.first()).is_some_and(|v| {
719                (v.starts_with("1-")
720                    && (v.ends_with(":1.6.0") || v.ends_with(":3.6.0") || v.ends_with(":4.6.0")))
721                    || v == "1-66:13.6.0"
722                    || v == "1-66:14.6.0"
723            })
724        }))
725    }
726
727    /// [73] Wenn SG9 PIA+5+1-b?:1.9.e/1-b?:3.9.0/1-b?:4.9.0/1-66?:13.9.0/1-66?:14.9.0 (b=Kanal: Wert gemäß Codeliste der OBIS-Kennzahlen und Medien, e=Tarif: Wert gemäß Codeliste der OBIS-Kennzahlen und Me...
728    fn evaluate_73(&self, ctx: &EvaluationContext) -> ConditionResult {
729        let pias = ctx.find_segments_with_qualifier("PIA", 0, "5");
730        ConditionResult::from(pias.iter().any(|s| {
731            s.elements.get(1).and_then(|e| e.first()).is_some_and(|v| {
732                (v.starts_with("1-") && v.contains(":1.9."))
733                    || (v.starts_with("1-") && v.ends_with(":3.9.0"))
734                    || (v.starts_with("1-") && v.ends_with(":4.9.0"))
735                    || v == "1-66:13.9.0"
736                    || v == "1-66:14.9.0"
737            })
738        }))
739    }
740
741    /// [77] Wenn MP-ID in SG2 NAD+MR der RB HKN-R
742    /// EXTERNAL: Requires context from outside the message.
743    fn evaluate_77(&self, ctx: &EvaluationContext) -> ConditionResult {
744        ctx.external
745            .evaluate("recipient_is_hkn_registration_authority")
746    }
747
748    /// [78] Wenn SG9 PIA+5+1-66?:13.6.0/1-66?:14.6.0/1-66?:13.9.0/1-66?:14.9.0 vorhanden
749    // REVIEW: PIA+5 means elements[0][0]='5'. DE7140 (OBIS code) is elements[1][0]. EDIFACT '?:' escape sequences are resolved by the parser to literal ':', so '1-66?:13.6.0' becomes '1-66:13.6.0'. Checks any SG9 group instance for PIA+5 with one of the four OBIS codes. Group path SG6->SG9 is approximate based on MSCONS structure. (medium confidence)
750    fn evaluate_78(&self, ctx: &EvaluationContext) -> ConditionResult {
751        ctx.any_group_has_qualified_value(
752            "PIA",
753            0,
754            "5",
755            1,
756            0,
757            &["1-66:13.6.0", "1-66:14.6.0", "1-66:13.9.0", "1-66:14.9.0"],
758            &["SG6", "SG9"],
759        )
760    }
761
762    /// [79] Wenn SG9 PIA+5+1-66?:13.6.0/1-66?:14.6.0/1-66?:13.9.0/1-66?:14.9.0 nicht vorhanden
763    // REVIEW: Logical negation of condition 78. True when none of the specified OBIS codes appears in any SG9 PIA+5 segment. (medium confidence)
764    fn evaluate_79(&self, ctx: &EvaluationContext) -> ConditionResult {
765        match ctx.any_group_has_qualified_value(
766            "PIA",
767            0,
768            "5",
769            1,
770            0,
771            &["1-66:13.6.0", "1-66:14.6.0", "1-66:13.9.0", "1-66:14.9.0"],
772            &["SG6", "SG9"],
773        ) {
774            ConditionResult::True => ConditionResult::False,
775            ConditionResult::False => ConditionResult::True,
776            ConditionResult::Unknown => ConditionResult::Unknown,
777        }
778    }
779
780    /// [80] Wenn MP-ID in SG2 NAD+MR in der Rolle ÜNB
781    /// EXTERNAL: Requires context from outside the message.
782    fn evaluate_80(&self, ctx: &EvaluationContext) -> ConditionResult {
783        ctx.external.evaluate("recipient_is_uenb")
784    }
785
786    /// [83] Wenn in derselben SG9 LIN die Angabe STS+10+Z38 nicht vorhanden
787    // REVIEW: STS 'Grundlage der Energiemenge' has elements[0][0]='10' (Statuskategorie) and elements[1][0]=status code. STS lives in SG10 (child of SG9). Condition is True when NO STS with category 10 and status Z38 exists within SG9/SG10 scope. Using any_group_has_qualified_value on SG9->SG10 path then negating. Group scope is approximate — a precise check would traverse SG9 instances and their SG10 children. (medium confidence)
788    fn evaluate_83(&self, ctx: &EvaluationContext) -> ConditionResult {
789        // HAND-EDITED: "in derselben SG9 LIN die Angabe STS+10+Z38 nicht vorhanden; the STS sits in its SG10" — the enclosing SG9 only, when the scope
790        // reaches one; the message as before otherwise.
791        if let Some(scope) = ctx.enclosing_scope("SG9") {
792            return ConditionResult::from(
793                !ctx.find_segments_at(scope, "STS")
794                    .iter()
795                    .any(|s| s.get_element(0) == "10" && s.get_component(1, 0) == "Z38"),
796            );
797        }
798        match ctx.any_group_has_qualified_value("STS", 0, "10", 1, 0, &["Z38"], &["SG9", "SG10"]) {
799            ConditionResult::True => ConditionResult::False,
800            ConditionResult::False => ConditionResult::True,
801            ConditionResult::Unknown => ConditionResult::Unknown,
802        }
803    }
804
805    /// [84] Wenn in derselben SG9 LIN die Angabe STS+10+Z39 nicht vorhanden
806    // REVIEW: Same pattern as condition 83 but for status code Z39. True when no STS+10+Z39 is present in any SG10 within the SG9 scope. (medium confidence)
807    fn evaluate_84(&self, ctx: &EvaluationContext) -> ConditionResult {
808        // HAND-EDITED: "in derselben SG9 LIN die Angabe STS+10+Z39 nicht vorhanden; the STS sits in its SG10" — the enclosing SG9 only, when the scope
809        // reaches one; the message as before otherwise.
810        if let Some(scope) = ctx.enclosing_scope("SG9") {
811            return ConditionResult::from(
812                !ctx.find_segments_at(scope, "STS")
813                    .iter()
814                    .any(|s| s.get_element(0) == "10" && s.get_component(1, 0) == "Z39"),
815            );
816        }
817        match ctx.any_group_has_qualified_value("STS", 0, "10", 1, 0, &["Z39"], &["SG9", "SG10"]) {
818            ConditionResult::True => ConditionResult::False,
819            ConditionResult::False => ConditionResult::True,
820            ConditionResult::Unknown => ConditionResult::Unknown,
821        }
822    }
823
824    /// [85] Wenn in derselben SG9 LIN die Angabe STS+10+Z36 nicht vorhanden
825    // REVIEW: Same pattern as condition 83 but for status code Z36. True when no STS+10+Z36 is present in any SG10 within the SG9 scope. (medium confidence)
826    fn evaluate_85(&self, ctx: &EvaluationContext) -> ConditionResult {
827        // HAND-EDITED: "in derselben SG9 LIN die Angabe STS+10+Z36 nicht vorhanden; the STS sits in its SG10" — the enclosing SG9 only, when the scope
828        // reaches one; the message as before otherwise.
829        if let Some(scope) = ctx.enclosing_scope("SG9") {
830            return ConditionResult::from(
831                !ctx.find_segments_at(scope, "STS")
832                    .iter()
833                    .any(|s| s.get_element(0) == "10" && s.get_component(1, 0) == "Z36"),
834            );
835        }
836        match ctx.any_group_has_qualified_value("STS", 0, "10", 1, 0, &["Z36"], &["SG9", "SG10"]) {
837            ConditionResult::True => ConditionResult::False,
838            ConditionResult::False => ConditionResult::True,
839            ConditionResult::Unknown => ConditionResult::Unknown,
840        }
841    }
842
843    /// [86] Wenn in derselben SG9 LIN die Angabe STS+10+Z37 nicht vorhanden
844    // REVIEW: Same pattern as condition 83 but for status code Z37. True when no STS+10+Z37 is present in any SG10 within the SG9 scope. (medium confidence)
845    fn evaluate_86(&self, ctx: &EvaluationContext) -> ConditionResult {
846        // HAND-EDITED: "in derselben SG9 LIN die Angabe STS+10+Z37 nicht vorhanden; the STS sits in its SG10" — the enclosing SG9 only, when the scope
847        // reaches one; the message as before otherwise.
848        if let Some(scope) = ctx.enclosing_scope("SG9") {
849            return ConditionResult::from(
850                !ctx.find_segments_at(scope, "STS")
851                    .iter()
852                    .any(|s| s.get_element(0) == "10" && s.get_component(1, 0) == "Z37"),
853            );
854        }
855        match ctx.any_group_has_qualified_value("STS", 0, "10", 1, 0, &["Z37"], &["SG9", "SG10"]) {
856            ConditionResult::True => ConditionResult::False,
857            ConditionResult::False => ConditionResult::True,
858            ConditionResult::Unknown => ConditionResult::Unknown,
859        }
860    }
861
862    /// [87] Wenn der Wert in DTM+163 DE2380 derselben SG6 LOC+172 mit demselben Wert in SG9 PIA+5 DE7140 der früheste angegebene Zeitpunkt ist
863    // REVIEW: Checks that DTM+163 in SG6 (with LOC+172) equals the minimum DTM+163 across all SG10 instances beneath that SG6's SG9 children. Medium confidence because the SG9→SG10 child navigation uses a local child instance index (si) passed to the grandchild API, which may behave differently depending on navigator implementation details for two-level child traversal. (medium confidence)
864    fn evaluate_87(&self, ctx: &EvaluationContext) -> ConditionResult {
865        // HAND-EDITED: "DTM+163 … derselben SG6 LOC+172 mit demselben Wert in SG9 PIA+5 … der früheste Zeitpunkt" — compared with the other instances when the
866        // scope reaches an SG10; the message as before otherwise.
867        if let Some(scope) = ctx.enclosing_scope("SG10") {
868            let dtm = |path: &[(&str, usize)]| -> Vec<String> {
869                ctx.find_segments_at(path, "DTM")
870                    .iter()
871                    .filter(|s| s.get_element(0) == "163")
872                    .map(|s| s.get_component(0, 1).to_string())
873                    .filter(|v| !v.is_empty())
874                    .collect()
875            };
876            let pia = |path: &[(&str, usize)]| -> Vec<String> {
877                ctx.find_segments_at(path, "PIA")
878                    .iter()
879                    .filter(|s| s.get_element(0) == "5")
880                    .map(|s| s.get_component(1, 0).to_string())
881                    .filter(|v| !v.is_empty())
882                    .collect()
883            };
884            let (Some(own), Some(sg9)) =
885                (dtm(scope).into_iter().next(), ctx.enclosing_scope("SG9"))
886            else {
887                return ConditionResult::Unknown;
888            };
889            let products = pia(sg9);
890            // Every SG10 (the DTMs sit there) of the SG9s of this SG6 with the
891            // same PIA+5 product.
892            let all: Vec<String> = ctx
893                .scopes_under("SG6", "SG9")
894                .unwrap_or_default()
895                .iter()
896                .filter(|p| pia(p).iter().any(|v| products.contains(v)))
897                .flat_map(|p| dtm(p))
898                .collect();
899            return ConditionResult::from(all.iter().min() == Some(&own));
900        }
901        let nav = match ctx.navigator() {
902            Some(n) => n,
903            None => return ConditionResult::Unknown,
904        };
905        let sg6_count = nav.group_instance_count(&["SG5", "SG6"]);
906        for gi in 0..sg6_count {
907            let locs = nav.find_segments_in_group("LOC", &["SG5", "SG6"], gi);
908            let has_loc_172 = locs.iter().any(|s| {
909                s.elements
910                    .first()
911                    .and_then(|e| e.first())
912                    .is_some_and(|v| v == "172")
913            });
914            if !has_loc_172 {
915                continue;
916            }
917            let sg6_dtms = nav.find_segments_in_group("DTM", &["SG5", "SG6"], gi);
918            let sg6_val = match sg6_dtms
919                .iter()
920                .find(|s| {
921                    s.elements
922                        .first()
923                        .and_then(|e| e.first())
924                        .is_some_and(|v| v == "163")
925                })
926                .and_then(|s| s.elements.first().and_then(|e| e.get(1)))
927                .filter(|v| !v.is_empty())
928                .cloned()
929            {
930                Some(v) => v,
931                None => continue,
932            };
933            let sg9_count = nav.child_group_instance_count(&["SG5", "SG6"], gi, "SG9");
934            let mut sg10_values: Vec<String> = Vec::new();
935            for si in 0..sg9_count {
936                let sg10_count = nav.child_group_instance_count(&["SG5", "SG6", "SG9"], si, "SG10");
937                for ti in 0..sg10_count {
938                    let dtms = nav.find_segments_in_child_group(
939                        "DTM",
940                        &["SG5", "SG6", "SG9"],
941                        si,
942                        "SG10",
943                        ti,
944                    );
945                    for dtm in &dtms {
946                        if dtm
947                            .elements
948                            .first()
949                            .and_then(|e| e.first())
950                            .is_some_and(|v| v == "163")
951                        {
952                            if let Some(val) = dtm.elements.first().and_then(|e| e.get(1)) {
953                                if !val.is_empty() {
954                                    sg10_values.push(val.clone());
955                                }
956                            }
957                        }
958                    }
959                }
960            }
961            if let Some(min_val) = sg10_values.iter().min() {
962                if &sg6_val != min_val {
963                    return ConditionResult::False;
964                }
965            }
966        }
967        ConditionResult::True
968    }
969
970    /// [88] Wenn der Wert in DTM+164 DE2380 derselben SG6 LOC+172 mit demselben Wert in SG9 PIA+5 DE7140 der späteste angegebene Zeitpunkt ist
971    // REVIEW: Mirror of condition 87 but for DTM+164 (Ende Messperiode) — checks that the SG6 transmission period end equals the maximum DTM+164 across all SG10 measurement instances under that SG6. Same navigation caveat applies for two-level SG9→SG10 child traversal. (medium confidence)
972    fn evaluate_88(&self, ctx: &EvaluationContext) -> ConditionResult {
973        // HAND-EDITED: "DTM+164 … derselben SG6 LOC+172 mit demselben Wert in SG9 PIA+5 … der späteste Zeitpunkt" — compared with the other instances when the
974        // scope reaches an SG10; the message as before otherwise.
975        if let Some(scope) = ctx.enclosing_scope("SG10") {
976            let dtm = |path: &[(&str, usize)]| -> Vec<String> {
977                ctx.find_segments_at(path, "DTM")
978                    .iter()
979                    .filter(|s| s.get_element(0) == "164")
980                    .map(|s| s.get_component(0, 1).to_string())
981                    .filter(|v| !v.is_empty())
982                    .collect()
983            };
984            let pia = |path: &[(&str, usize)]| -> Vec<String> {
985                ctx.find_segments_at(path, "PIA")
986                    .iter()
987                    .filter(|s| s.get_element(0) == "5")
988                    .map(|s| s.get_component(1, 0).to_string())
989                    .filter(|v| !v.is_empty())
990                    .collect()
991            };
992            let (Some(own), Some(sg9)) =
993                (dtm(scope).into_iter().next(), ctx.enclosing_scope("SG9"))
994            else {
995                return ConditionResult::Unknown;
996            };
997            let products = pia(sg9);
998            // Every SG10 (the DTMs sit there) of the SG9s of this SG6 with the
999            // same PIA+5 product.
1000            let all: Vec<String> = ctx
1001                .scopes_under("SG6", "SG9")
1002                .unwrap_or_default()
1003                .iter()
1004                .filter(|p| pia(p).iter().any(|v| products.contains(v)))
1005                .flat_map(|p| dtm(p))
1006                .collect();
1007            return ConditionResult::from(all.iter().max() == Some(&own));
1008        }
1009        let nav = match ctx.navigator() {
1010            Some(n) => n,
1011            None => return ConditionResult::Unknown,
1012        };
1013        let sg6_count = nav.group_instance_count(&["SG5", "SG6"]);
1014        for gi in 0..sg6_count {
1015            let locs = nav.find_segments_in_group("LOC", &["SG5", "SG6"], gi);
1016            let has_loc_172 = locs.iter().any(|s| {
1017                s.elements
1018                    .first()
1019                    .and_then(|e| e.first())
1020                    .is_some_and(|v| v == "172")
1021            });
1022            if !has_loc_172 {
1023                continue;
1024            }
1025            let sg6_dtms = nav.find_segments_in_group("DTM", &["SG5", "SG6"], gi);
1026            let sg6_val = match sg6_dtms
1027                .iter()
1028                .find(|s| {
1029                    s.elements
1030                        .first()
1031                        .and_then(|e| e.first())
1032                        .is_some_and(|v| v == "164")
1033                })
1034                .and_then(|s| s.elements.first().and_then(|e| e.get(1)))
1035                .filter(|v| !v.is_empty())
1036                .cloned()
1037            {
1038                Some(v) => v,
1039                None => continue,
1040            };
1041            let sg9_count = nav.child_group_instance_count(&["SG5", "SG6"], gi, "SG9");
1042            let mut sg10_values: Vec<String> = Vec::new();
1043            for si in 0..sg9_count {
1044                let sg10_count = nav.child_group_instance_count(&["SG5", "SG6", "SG9"], si, "SG10");
1045                for ti in 0..sg10_count {
1046                    let dtms = nav.find_segments_in_child_group(
1047                        "DTM",
1048                        &["SG5", "SG6", "SG9"],
1049                        si,
1050                        "SG10",
1051                        ti,
1052                    );
1053                    for dtm in &dtms {
1054                        if dtm
1055                            .elements
1056                            .first()
1057                            .and_then(|e| e.first())
1058                            .is_some_and(|v| v == "164")
1059                        {
1060                            if let Some(val) = dtm.elements.first().and_then(|e| e.get(1)) {
1061                                if !val.is_empty() {
1062                                    sg10_values.push(val.clone());
1063                                }
1064                            }
1065                        }
1066                    }
1067                }
1068            }
1069            if let Some(max_val) = sg10_values.iter().max() {
1070                if &sg6_val != max_val {
1071                    return ConditionResult::False;
1072                }
1073            }
1074        }
1075        ConditionResult::True
1076    }
1077
1078    /// [90] Wenn BGM+Z41 vorhanden
1079    fn evaluate_90(&self, ctx: &EvaluationContext) -> ConditionResult {
1080        ctx.has_qualifier("BGM", 0, "Z41")
1081    }
1082
1083    /// [91] Wenn BGM+Z42 vorhanden
1084    fn evaluate_91(&self, ctx: &EvaluationContext) -> ConditionResult {
1085        ctx.has_qualifier("BGM", 0, "Z42")
1086    }
1087
1088    /// [92] Wenn SG10 QTY DE6063 mit Wert 67 vorhanden
1089    fn evaluate_92(&self, ctx: &EvaluationContext) -> ConditionResult {
1090        // HAND-EDITED (#334): the QTY of this SG10 — STS combines [92]/[93]/[94]
1091        // with `⊻`, which holds only per SG10.
1092        ctx.this_group_has_qualifier("QTY", 0, "67", &["SG5", "SG6", "SG9", "SG10"])
1093    }
1094
1095    /// [93] Wenn SG10 QTY DE6063 mit Wert 220 vorhanden
1096    fn evaluate_93(&self, ctx: &EvaluationContext) -> ConditionResult {
1097        // HAND-EDITED (#334): the QTY of this SG10 — STS combines [92]/[93]/[94]
1098        // with `⊻`, which holds only per SG10.
1099        ctx.this_group_has_qualifier("QTY", 0, "220", &["SG5", "SG6", "SG9", "SG10"])
1100    }
1101
1102    /// [94] Wenn SG10 QTY DE6063 mit Wert 201 vorhanden
1103    fn evaluate_94(&self, ctx: &EvaluationContext) -> ConditionResult {
1104        // HAND-EDITED (#334): the QTY of this SG10 — STS combines [92]/[93]/[94]
1105        // with `⊻`, which holds only per SG10.
1106        ctx.this_group_has_qualifier("QTY", 0, "201", &["SG5", "SG6", "SG9", "SG10"])
1107    }
1108
1109    /// [95] Wenn SG10 QTY DE6063 mit Wert 20 vorhanden
1110    fn evaluate_95(&self, ctx: &EvaluationContext) -> ConditionResult {
1111        ctx.any_group_has_qualifier("QTY", 0, "20", &["SG5", "SG10"])
1112    }
1113
1114    /// [96] Wenn SG10 QTY DE6063 mit Wert Z18 vorhanden
1115    fn evaluate_96(&self, ctx: &EvaluationContext) -> ConditionResult {
1116        ctx.any_group_has_qualifier("QTY", 0, "Z18", &["SG5", "SG10"])
1117    }
1118
1119    /// [97] Wenn es sich um die Übermittlung eines Wertes aufgrund der Umstellung der Gasqualität handelt
1120    /// EXTERNAL: Requires context from outside the message.
1121    // REVIEW: Whether this transmission is caused by a gas quality changeover (H-Gas/L-Gas conversion) cannot be determined from EDIFACT message content alone — it is a business context condition. (medium confidence)
1122    fn evaluate_97(&self, ctx: &EvaluationContext) -> ConditionResult {
1123        ctx.external.evaluate("gas_quality_change_transmission")
1124    }
1125
1126    /// [98] Wenn SG9 PIA+5+SOL:Z08 vorhanden
1127    fn evaluate_98(&self, ctx: &EvaluationContext) -> ConditionResult {
1128        let pias = ctx.find_segments_with_qualifier("PIA", 0, "5");
1129        ConditionResult::from(pias.iter().any(|s| {
1130            s.elements
1131                .get(1)
1132                .and_then(|e| e.first())
1133                .is_some_and(|v| v == "SOL")
1134                && s.elements
1135                    .get(1)
1136                    .and_then(|e| e.get(1))
1137                    .is_some_and(|v| v == "Z08")
1138        }))
1139    }
1140
1141    /// [99] Wenn SG9 PIA+5+WID:Z08 vorhanden
1142    fn evaluate_99(&self, ctx: &EvaluationContext) -> ConditionResult {
1143        let pias = ctx.find_segments_with_qualifier("PIA", 0, "5");
1144        ConditionResult::from(pias.iter().any(|s| {
1145            s.elements
1146                .get(1)
1147                .and_then(|e| e.first())
1148                .is_some_and(|v| v == "WID")
1149                && s.elements
1150                    .get(1)
1151                    .and_then(|e| e.get(1))
1152                    .is_some_and(|v| v == "Z08")
1153        }))
1154    }
1155
1156    /// [100] Wenn in derselben SG9 LIN das PIA+5+AUA:Z08 vorhanden
1157    fn evaluate_100(&self, ctx: &EvaluationContext) -> ConditionResult {
1158        // HAND-EDITED: "in dieser/derselben SG9" — scoped to the enclosing group.
1159        ctx.this_group_has_qualified_value("PIA", 0, "5", 1, 0, &["AUA"], &["SG5", "SG9"])
1160    }
1161
1162    /// [101] Wenn in derselben SG9 LIN das PIA+5+FPA:Z08 vorhanden
1163    fn evaluate_101(&self, ctx: &EvaluationContext) -> ConditionResult {
1164        // HAND-EDITED: "in dieser/derselben SG9" — scoped to the enclosing group.
1165        ctx.this_group_has_qualified_value("PIA", 0, "5", 1, 0, &["FPA"], &["SG5", "SG9"])
1166    }
1167
1168    /// [108] wenn SG9 PIA+5+7-b?:99.41.16/7-b?:99.42.16 (b=Kanal: Wert gemäß Codeliste der OBIS-Kennzahlen und Medien) vorhanden
1169    // REVIEW: Checks PIA+5 for OBIS codes matching pattern '7-{channel}:99.41.16' or '7-{channel}:99.42.16' (Wirkenergie-Kennzahlen). The '?' in AHB notation is the EDIFACT release character escaping the colon; parsed segments will contain literal colon. Channel variable 'b' matched via starts_with("7-"). (medium confidence)
1170    fn evaluate_108(&self, ctx: &EvaluationContext) -> ConditionResult {
1171        let pias = ctx.find_segments_with_qualifier("PIA", 0, "5");
1172        ConditionResult::from(pias.iter().any(|s| {
1173            let obis = match s.elements.get(1).and_then(|e| e.first()) {
1174                Some(v) => v.as_str(),
1175                None => return false,
1176            };
1177            let code_list = s
1178                .elements
1179                .get(1)
1180                .and_then(|e| e.get(1))
1181                .map(|v| v.as_str())
1182                .unwrap_or("");
1183            code_list == "Z08"
1184                && obis.starts_with("7-")
1185                && (obis.contains(":99.41.16") || obis.contains(":99.42.16"))
1186        }))
1187    }
1188
1189    /// [111] Wenn SG10 DTM+9 DE2379 in demselben Segment mit Wert 303 vorhanden
1190    fn evaluate_111(&self, ctx: &EvaluationContext) -> ConditionResult {
1191        ConditionResult::from(ctx.self_segment_value_equals("DTM", 0, 2, "303"))
1192    }
1193
1194    /// [113] wenn SG7 RFF+AGK (Konfigurations-ID) vorhanden
1195    fn evaluate_113(&self, ctx: &EvaluationContext) -> ConditionResult {
1196        ctx.has_qualifier("RFF", 0, "AGK")
1197    }
1198
1199    /// [117] Nur MP-ID aus Sparte Strom
1200    /// EXTERNAL: Requires context from outside the message.
1201    fn evaluate_117(&self, ctx: &EvaluationContext) -> ConditionResult {
1202        ctx.external.evaluate("marktpartner_is_strom")
1203    }
1204
1205    /// [118] Nur MP-ID aus Sparte Gas
1206    /// EXTERNAL: Requires context from outside the message.
1207    fn evaluate_118(&self, ctx: &EvaluationContext) -> ConditionResult {
1208        ctx.external.evaluate("marktpartner_is_gas")
1209    }
1210
1211    /// [119] wenn in SG6 LOC+172 DE3225 die ID der Marktlokation angegeben ist
1212    fn evaluate_119(&self, ctx: &EvaluationContext) -> ConditionResult {
1213        // Marktlokation = 11-digit MaLo-ID with BDEW check digit. A non-empty
1214        // value that doesn't match (e.g. a 33-char Zählpunkt) must be False so
1215        // the AHB's XOR branches against condition [38] stay mutually exclusive.
1216        let locs = ctx.find_segments_with_qualifier("LOC", 0, "172");
1217        match locs.first() {
1218            Some(loc) => match loc.elements.get(1).and_then(|e| e.first()) {
1219                Some(v) if !v.is_empty() => validate_malo_id(v),
1220                _ => ConditionResult::False,
1221            },
1222            None => ConditionResult::False,
1223        }
1224    }
1225
1226    /// [121] wenn BGM+Z43 (Redispatch Ausfallarbeitüberführungszeitreihe) vorhanden
1227    fn evaluate_121(&self, ctx: &EvaluationContext) -> ConditionResult {
1228        ctx.has_qualifier("BGM", 0, "Z43")
1229    }
1230
1231    /// [125] wenn SG9 PIA+5+7-0?:52.0.22/7-b?:53.0.16/7-b?:55.0.16/7-b?:55.0.20/7-b?:55.0.22 (b=Kanal: Wert gemäß Codeliste der OBIS-Kennzahlen und Medien) vorhanden
1232    // REVIEW: Checks PIA+5 for specific OBIS code patterns: '7-0:52.0.22' (fixed channel 0), '7-{b}:53.0.16', '7-{b}:55.0.16', '7-{b}:55.0.20', '7-{b}:55.0.22' (variable channel). These are Blindenergie/Leistungsmessungs-Kennzahlen. Uses contains() to match the OBIS suffix after the channel variable. (medium confidence)
1233    fn evaluate_125(&self, ctx: &EvaluationContext) -> ConditionResult {
1234        let pias = ctx.find_segments_with_qualifier("PIA", 0, "5");
1235        ConditionResult::from(pias.iter().any(|s| {
1236            let obis = match s.elements.get(1).and_then(|e| e.first()) {
1237                Some(v) => v.as_str(),
1238                None => return false,
1239            };
1240            let code_list = s
1241                .elements
1242                .get(1)
1243                .and_then(|e| e.get(1))
1244                .map(|v| v.as_str())
1245                .unwrap_or("");
1246            code_list == "Z08"
1247                && obis.starts_with("7-")
1248                && (obis.contains(":52.0.22")
1249                    || obis.contains(":53.0.16")
1250                    || obis.contains(":55.0.16")
1251                    || obis.contains(":55.0.20")
1252                    || obis.contains(":55.0.22"))
1253        }))
1254    }
1255
1256    /// [128] Wenn es sich um eine Ablesung handelt, welche keine Ablesung aufgrund der Änderung an der Messtechnik oder deren Konfiguration ist (z.B. Kundenablesung).
1257    // REVIEW: A regular reading (e.g. Kundenablesung) is distinguished from a meter-change reading by the absence of DTM+60 (Ausführungs-/Änderungszeitpunkt) in the same SG10. DTM+9 presence confirms it is a reading at all; DTM+60 absence confirms it is not due to a meter/config change. Group path SG9→SG10 matches MSCONS LIN-group context; fallback to message-wide if no navigator. (medium confidence)
1258    fn evaluate_128(&self, ctx: &EvaluationContext) -> ConditionResult {
1259        // True when SG10 has a reading date (DTM+9, Ablesedatum) but no device change timestamp (DTM+60),
1260        // indicating a regular reading not caused by meter change or configuration change
1261        ctx.any_group_has_qualifier_without("DTM", 0, "9", "DTM", 0, "60", &["SG9", "SG10"])
1262    }
1263
1264    /// [129] Wenn es sich um eine Ablesung aufgrund der Änderung an der Messtechnik oder deren Konfiguration handelt (z.B. Gerätewechsel).
1265    // REVIEW: A meter-change reading is identified by the co-occurrence of DTM+9 (Ablesedatum) and DTM+60 (Ausführungs-/Änderungszeitpunkt) within the same SG10 instance. Uses direct navigator iteration to check co-occurrence within a single group instance (any_group_has_co_occurrence would have ambiguity for same-tag different qualifiers). Falls back to message-wide check without navigator. (medium confidence)
1266    fn evaluate_129(&self, ctx: &EvaluationContext) -> ConditionResult {
1267        // True when SG10 contains both DTM+9 (Ablesedatum) and DTM+60 (Ausführungs-/Änderungszeitpunkt),
1268        // indicating a reading caused by a meter change or configuration change (e.g. Gerätewechsel)
1269        let nav = match ctx.navigator() {
1270            Some(n) => n,
1271            None => {
1272                let has9 = ctx.has_qualifier("DTM", 0, "9");
1273                let has60 = ctx.has_qualifier("DTM", 0, "60");
1274                return match (has9, has60) {
1275                    (ConditionResult::True, ConditionResult::True) => ConditionResult::True,
1276                    (ConditionResult::Unknown, _) | (_, ConditionResult::Unknown) => {
1277                        ConditionResult::Unknown
1278                    }
1279                    _ => ConditionResult::False,
1280                };
1281            }
1282        };
1283        let sg10_count = nav.group_instance_count(&["SG9", "SG10"]);
1284        for i in 0..sg10_count {
1285            let dtms = nav.find_segments_in_group("DTM", &["SG9", "SG10"], i);
1286            let has9 = dtms.iter().any(|s| {
1287                s.elements
1288                    .first()
1289                    .and_then(|e| e.first())
1290                    .is_some_and(|v| v == "9")
1291            });
1292            let has60 = dtms.iter().any(|s| {
1293                s.elements
1294                    .first()
1295                    .and_then(|e| e.first())
1296                    .is_some_and(|v| v == "60")
1297            });
1298            if has9 && has60 {
1299                return ConditionResult::True;
1300            }
1301        }
1302        ConditionResult::False
1303    }
1304
1305    /// [131] wenn RFF+AGK (Konfigurations-ID) nicht vorhanden
1306    fn evaluate_131(&self, ctx: &EvaluationContext) -> ConditionResult {
1307        ctx.lacks_qualifier("RFF", 0, "AGK")
1308    }
1309
1310    /// [132] wenn LOC+172 (Identifikationsangabe) DE3225 nicht vorhanden
1311    // REVIEW: Checks if LOC+172 is absent entirely, or if DE3225 (elements[1][0], the Bezeichnung) is empty/missing within the LOC+172 segment. Returns True in either case as the condition is 'DE3225 nicht vorhanden'. (medium confidence)
1312    fn evaluate_132(&self, ctx: &EvaluationContext) -> ConditionResult {
1313        let locs = ctx.find_segments_with_qualifier("LOC", 0, "172");
1314        match locs.first() {
1315            None => ConditionResult::True,
1316            Some(loc) => {
1317                let de3225_absent = loc
1318                    .elements
1319                    .get(1)
1320                    .and_then(|e| e.first())
1321                    .map(|v| v.is_empty())
1322                    .unwrap_or(true);
1323                ConditionResult::from(de3225_absent)
1324            }
1325        }
1326    }
1327
1328    /// [133] Wenn innerhalb desselben LIN-Segments neben diesem Segment (SG10 DTM+7 Nutzungszeitpunkt) noch das SG10 DTM+9 (Ablesedatum) mit dem Code 102 im DE2379 vorhanden ist, darf der Wert der Differenz zwi...
1329    // HAND-EDITED: shared with every MSCONS FV in `eval::reading_dates`, so
1330    // that `[130] ∨ [133]` asks every partner of the DTM+7 to keep its gap
1331    // (a [133] that held without a DTM+9 let any DTM+60 gap pass).
1332    fn evaluate_133(&self, ctx: &EvaluationContext) -> ConditionResult {
1333        crate::eval::reading_dates::condition_133(ctx)
1334    }
1335
1336    /// [134] Wenn SG10 DTM+9 DE2379 in demselben Segment mit Wert 102 vorhanden
1337    fn evaluate_134(&self, ctx: &EvaluationContext) -> ConditionResult {
1338        ConditionResult::from(ctx.self_segment_value_equals("DTM", 0, 2, "102"))
1339    }
1340
1341    /// [135] Der Wert an der Stelle CCYYMMDD muss ≤ dem Wert an der Stelle CCYYMMDD im DE2380 des DTM+137 sein
1342    // HAND-EDITED: subject is the current DTM's DE2380 (via ctx.resolved_value), not a
1343    // guessed DTM+9. Compares the CCYYMMDD prefix (first 8 chars) lexicographically,
1344    // which matches both format 303 (CCYYMMDDhhmm+ZZ) and 102 (CCYYMMDD) prefixes.
1345    fn evaluate_135(&self, ctx: &EvaluationContext) -> ConditionResult {
1346        let current = match ctx.resolved_value {
1347            Some(v) if v.len() >= 8 => &v[..8],
1348            _ => return ConditionResult::Unknown,
1349        };
1350        let threshold: String = match ctx
1351            .find_segments_with_qualifier("DTM", 0, "137")
1352            .first()
1353            .and_then(|s| s.elements.first())
1354            .and_then(|e| e.get(1))
1355            .filter(|v| v.len() >= 8)
1356        {
1357            Some(v) => v.chars().take(8).collect(),
1358            None => return ConditionResult::Unknown,
1359        };
1360        ConditionResult::from(current <= threshold.as_str())
1361    }
1362
1363    /// [138] Wenn es sich um eine Korrekturenergiemenge auf einen Wert aus einem iMS handelt
1364    /// EXTERNAL: Requires context from outside the message.
1365    fn evaluate_138(&self, ctx: &EvaluationContext) -> ConditionResult {
1366        ctx.external.evaluate("is_correction_energy_from_ims")
1367    }
1368
1369    /// [141] Wenn MP-ID in SG2 NAD+MR in der Rolle MGV
1370    /// EXTERNAL: Requires context from outside the message.
1371    fn evaluate_141(&self, ctx: &EvaluationContext) -> ConditionResult {
1372        ctx.external.evaluate("recipient_is_mgv")
1373    }
1374
1375    /// [142] wenn im DE3155 in demselben COM der Code EM vorhanden ist
1376    fn evaluate_142(&self, ctx: &EvaluationContext) -> ConditionResult {
1377        let coms = ctx.find_segments("COM");
1378        ConditionResult::from(coms.iter().any(|s| {
1379            s.elements
1380                .first()
1381                .and_then(|e| e.get(1))
1382                .is_some_and(|v| v == "EM")
1383        }))
1384    }
1385
1386    /// [143] wenn im DE3155 in demselben COM der Code TE / FX / AJ / AL vorhanden ist
1387    fn evaluate_143(&self, ctx: &EvaluationContext) -> ConditionResult {
1388        let coms = ctx.find_segments("COM");
1389        ConditionResult::from(coms.iter().any(|s| {
1390            s.elements
1391                .first()
1392                .and_then(|e| e.get(1))
1393                .is_some_and(|v| matches!(v.as_str(), "TE" | "FX" | "AJ" | "AL"))
1394        }))
1395    }
1396
1397    /// [144] Wenn Wert in SG7 RFF+AGK DE1154 (Konfigurations-ID) vorhanden
1398    fn evaluate_144(&self, ctx: &EvaluationContext) -> ConditionResult {
1399        let rffs = ctx.find_segments_with_qualifier("RFF", 0, "AGK");
1400        match rffs.first() {
1401            None => ConditionResult::False,
1402            Some(rff) => {
1403                let has_value = rff
1404                    .elements
1405                    .first()
1406                    .and_then(|e| e.get(1))
1407                    .map(|v| !v.is_empty())
1408                    .unwrap_or(false);
1409                ConditionResult::from(has_value)
1410            }
1411        }
1412    }
1413
1414    /// [145] Wenn in derselben S9 LIN das SG10 DTM+163 (Beginn Messperiode) nicht vorhanden ist.
1415    // REVIEW: Checks absence of DTM+163 (Beginn Messperiode) in the message. The condition references the same SG10 context within the enclosing LIN group; without the exact MSCONS group path this is a message-wide approximation. Note: DTM+163 also appears in SG6 (Übertragungszeitraum), so this may yield false-negatives for the SG10-scoped intent. (medium confidence)
1416    fn evaluate_145(&self, ctx: &EvaluationContext) -> ConditionResult {
1417        ctx.lacks_qualifier("DTM", 0, "163")
1418    }
1419
1420    /// [146] Wenn es bei dem zu übermittelnden Wert um einen Wert zu einem Zeitpunkt handelt.
1421    // REVIEW: A point-in-time value (Wert zu einem Zeitpunkt) is signalled by the presence of DTM+7 (Nutzungszeitpunkt) in SG10. If this DTM qualifier is present, the transmitted value represents a single point in time rather than an interval. (medium confidence)
1422    fn evaluate_146(&self, ctx: &EvaluationContext) -> ConditionResult {
1423        ctx.has_qualifier("DTM", 0, "7")
1424    }
1425
1426    /// [147] Wenn in derselben S9 LIN das SG10 DTM+7 (Nutzungszeitpunkt) nicht vorhanden ist.
1427    // REVIEW: Checks absence of DTM+7 (Nutzungszeitpunkt) in the message, approximating the SG10-scoped check within the enclosing LIN group. Absence means the value is not a point-in-time value. (medium confidence)
1428    fn evaluate_147(&self, ctx: &EvaluationContext) -> ConditionResult {
1429        ctx.lacks_qualifier("DTM", 0, "7")
1430    }
1431
1432    /// [148] Wenn es bei dem zu übermittelnden Wert um einen Wert in einem Zeitintervall handelt.
1433    // REVIEW: An interval value (Wert in einem Zeitintervall) is signalled by the presence of DTM+163 (Beginn Messperiode) in SG10. If Beginn Messperiode is present, the value spans a time interval rather than being a single point. (medium confidence)
1434    fn evaluate_148(&self, ctx: &EvaluationContext) -> ConditionResult {
1435        ctx.has_qualifier("DTM", 0, "163")
1436    }
1437
1438    /// [149] Wenn in derselben S9 LIN das SG10 DTM+163 (Beginn Messperiode) vorhanden ist.
1439    // REVIEW: Checks presence of DTM+163 (Beginn Messperiode) — the direct inverse of condition 145. Message-wide approximation of the SG10-scoped intent within the enclosing LIN group. (medium confidence)
1440    fn evaluate_149(&self, ctx: &EvaluationContext) -> ConditionResult {
1441        ctx.has_qualifier("DTM", 0, "163")
1442    }
1443
1444    /// [150] Wenn BGM+Z69 (Redispatch tägliche Ausfallarbeitsüberführungszeitreihe) vorhanden.
1445    fn evaluate_150(&self, ctx: &EvaluationContext) -> ConditionResult {
1446        ctx.has_qualifier("BGM", 0, "Z69")
1447    }
1448
1449    /// [490] wenn Wert in diesem DE, an der Stelle CCYYMMDDHHMM ein Zeitpunkt aus dem angegeben Zeitraum der Tabelle Kapitel 3.5 „Übersicht gesetzliche deutsche Sommerzeit (MESZ)“ der Spalten: „Sommerzei...
1450    fn evaluate_490(&self, ctx: &EvaluationContext) -> ConditionResult {
1451        match ctx.resolved_value {
1452            Some(val) => is_mesz_utc(val),
1453            None => ConditionResult::False,
1454        }
1455    }
1456
1457    /// [491] wenn Wert in diesem DE, an der Stelle CCYYMMDDHHMM ein Zeitpunkt aus dem angegeben Zeitraum der Tabelle Kapitel 3.6 „Übersicht gesetzliche deutsche Zeit (MEZ)“ der Spalten: „Winterzeit (MEZ)...
1458    // REVIEW: Checks whether the datetime value falls within German winter time (MEZ, UTC+1) using the is_mez_utc helper. Medium confidence because 'diesem DE' is context-dependent — without knowing the exact field the condition is applied to in the AHB table, we check the first DTM value available. The is_mez_utc helper applies the EU DST rule to determine MEZ vs MESZ. (medium confidence)
1459    fn evaluate_491(&self, ctx: &EvaluationContext) -> ConditionResult {
1460        match ctx.resolved_value {
1461            Some(val) => is_mez_utc(val),
1462            None => ConditionResult::False,
1463        }
1464    }
1465
1466    /// [492] wenn MP-ID in NAD+MR aus Sparte Strom
1467    /// EXTERNAL: Requires context from outside the message.
1468    fn evaluate_492(&self, ctx: &EvaluationContext) -> ConditionResult {
1469        ctx.external.evaluate("recipient_is_electricity_sector")
1470    }
1471
1472    /// [493] wenn MP-ID in NAD+MR aus Sparte Gas
1473    /// EXTERNAL: Requires context from outside the message.
1474    fn evaluate_493(&self, ctx: &EvaluationContext) -> ConditionResult {
1475        ctx.external.evaluate("recipient_is_gas_sector")
1476    }
1477
1478    /// [494] Das hier genannte Datum muss der Zeitpunkt sein, zu dem das Dokument erstellt wurde, oder ein Zeitpunkt, der davor liegt.
1479    // HAND-EDITED: subject is the current DTM's DE2380 (via ctx.resolved_value), not a
1480    // guessed other qualifier. Lexicographic compare is valid for fixed-width
1481    // CCYYMMDDHHMM+ZZ (format 303) and CCYYMMDD (format 102) when compared prefix-wise.
1482    fn evaluate_494(&self, ctx: &EvaluationContext) -> ConditionResult {
1483        let current = match ctx.resolved_value {
1484            Some(v) if !v.is_empty() => v,
1485            _ => return ConditionResult::Unknown,
1486        };
1487        let threshold = match ctx
1488            .find_segments_with_qualifier("DTM", 0, "137")
1489            .first()
1490            .and_then(|s| s.elements.first())
1491            .and_then(|e| e.get(1))
1492        {
1493            Some(v) if !v.is_empty() => v.clone(),
1494            _ => return ConditionResult::Unknown,
1495        };
1496        ConditionResult::from(current <= threshold.as_str())
1497    }
1498
1499    /// [495] Der Zeitpunkt muss ≤ dem Wert im DE2380 des DTM+137 sein
1500    // HAND-EDITED: subject is the current DTM's DE2380 (via ctx.resolved_value), not a
1501    // guessed other qualifier. Matches the invoic_conditions pattern.
1502    fn evaluate_495(&self, ctx: &EvaluationContext) -> ConditionResult {
1503        let current = match ctx.resolved_value {
1504            Some(v) if !v.is_empty() => v,
1505            _ => return ConditionResult::Unknown,
1506        };
1507        // HAND-EDITED (#437): the first DTM+137 only; collecting every DTM
1508        // per SG10 value made a Lastgang quadratic.
1509        let threshold = match ctx
1510            .find_segment_with_qualifier("DTM", 0, "137")
1511            .and_then(|s| s.elements.first())
1512            .and_then(|e| e.get(1))
1513        {
1514            Some(v) if !v.is_empty() => v.clone(),
1515            _ => return ConditionResult::Unknown,
1516        };
1517        ConditionResult::from(current <= threshold.as_str())
1518    }
1519
1520    /// [501] Hinweis: Es sind nur die Werte erlaubt, die in der EDI@Energy Codeliste der OBIS-Kennzahlen und Medien mit dem entsprechenden Prüfidentifikator versehen sind.
1521    fn evaluate_501(&self, _ctx: &EvaluationContext) -> ConditionResult {
1522        // Hinweis: Informational note about allowed OBIS codes — always applies unconditionally
1523        ConditionResult::True
1524    }
1525
1526    /// [502] Hinweis: Einmal für die Energiemenge von Beginn des Kalenderjahres bis zum Lieferbeginn und bis zu zweimal für die zwei höchsten Monatsleistungswerte (wegen KAV) von Beginn des Kalenderjahres bi...
1527    fn evaluate_502(&self, _ctx: &EvaluationContext) -> ConditionResult {
1528        // Hinweis: Informational note about cardinality (once for energy from start of calendar year,
1529        // up to twice for two highest monthly peak values due to KAV) — always applies unconditionally
1530        ConditionResult::True
1531    }
1532
1533    /// [506] Hinweis: Nur bei Einspeisemengen und bei Gas zur stündlichen Energiedatenübermittlung
1534    fn evaluate_506(&self, _ctx: &EvaluationContext) -> ConditionResult {
1535        // Hinweis: Informational note — only for feed-in quantities and gas hourly energy data transmission
1536        // This is an informational annotation, not a boolean predicate — always applies
1537        ConditionResult::True
1538    }
1539
1540    /// [509] Hinweis: Falls es sich um eine Korrekturenergiemenge handelt, ist hier die Referenz auf die MSCONS anzugeben, in der der Zählerstand vorab übermittelt wurde.
1541    fn evaluate_509(&self, _ctx: &EvaluationContext) -> ConditionResult {
1542        // Hinweis: Informational note — for correction energy amounts, reference the prior MSCONS
1543        // in which the meter reading was transmitted. Informational annotation, always applies.
1544        ConditionResult::True
1545    }
1546
1547    /// [510] Hinweis: Verwendung der ID der Messlokation
1548    fn evaluate_510(&self, _ctx: &EvaluationContext) -> ConditionResult {
1549        // Hinweis: Verwendung der ID der Messlokation — informational note, always applies
1550        ConditionResult::True
1551    }
1552
1553    /// [511] Hinweis: Verwendung der ID des MaBiS-ZP
1554    fn evaluate_511(&self, _ctx: &EvaluationContext) -> ConditionResult {
1555        // Hinweis: Verwendung der ID des MaBiS-ZP — informational note, always applies
1556        ConditionResult::True
1557    }
1558
1559    /// [512] Hinweis: Verwendung der Bilanzkreisbezeichnung
1560    fn evaluate_512(&self, _ctx: &EvaluationContext) -> ConditionResult {
1561        // Hinweis: Verwendung der Bilanzkreisbezeichnung — informational note, always applies
1562        ConditionResult::True
1563    }
1564
1565    /// [513] Hinweis: Verwendung der Bezeichnung des Bilanzierungsgebietes
1566    fn evaluate_513(&self, _ctx: &EvaluationContext) -> ConditionResult {
1567        // Hinweis: Verwendung der Bezeichnung des Bilanzierungsgebietes — informational note, always applies
1568        ConditionResult::True
1569    }
1570
1571    /// [514] Hinweis: Verwendung der ID der Marktlokation
1572    fn evaluate_514(&self, _ctx: &EvaluationContext) -> ConditionResult {
1573        // Hinweis: Verwendung der ID der Marktlokation — informational note, always applies
1574        ConditionResult::True
1575    }
1576
1577    /// [515] Hinweis: Verwendung der Profilbezeichnung
1578    fn evaluate_515(&self, _ctx: &EvaluationContext) -> ConditionResult {
1579        // Hinweis: Verwendung der Profilbezeichnung — informational note, always applies
1580        ConditionResult::True
1581    }
1582
1583    /// [516] Hinweis: Verwendung der Bezeichnung der Profilschar
1584    fn evaluate_516(&self, _ctx: &EvaluationContext) -> ConditionResult {
1585        // Hinweis: Verwendung der Bezeichnung der Profilschar — informational note, always applies
1586        ConditionResult::True
1587    }
1588
1589    /// [518] Hinweis: Verwendung der ID der Tranche
1590    fn evaluate_518(&self, _ctx: &EvaluationContext) -> ConditionResult {
1591        // Hinweis: Verwendung der ID der Tranche — informational note, always applies
1592        ConditionResult::True
1593    }
1594
1595    /// [519] Hinweis: Nur wenn der gemessene Lastgang der Messlokation nicht dem Lastgang der Marktlokation 1:1 entspricht.
1596    fn evaluate_519(&self, _ctx: &EvaluationContext) -> ConditionResult {
1597        // Hinweis: Nur wenn der gemessene Lastgang der Messlokation nicht dem Lastgang der Marktlokation 1:1 entspricht — informational note, always applies
1598        ConditionResult::True
1599    }
1600
1601    /// [520] Hinweis: Wenn es sich um eine 1:1 Beziehung zwischen Messlokation und Marktlokation handelt und der gemessene Lastgang der Messlokation dem Lastgang der Marktlokation 1:1 entspricht, oder wenn der ...
1602    fn evaluate_520(&self, _ctx: &EvaluationContext) -> ConditionResult {
1603        // Hinweis: Wenn es sich um eine 1:1 Beziehung zwischen Messlokation und Marktlokation handelt... — informational note, always applies
1604        ConditionResult::True
1605    }
1606
1607    /// [522] Hinweis: Nur für die Übermittlung der Korrekturenergiemengen im Zeitintervall zwischen zwei Messwerten.
1608    fn evaluate_522(&self, _ctx: &EvaluationContext) -> ConditionResult {
1609        // Hinweis: Nur für die Übermittlung der Korrekturenergiemengen im Zeitintervall zwischen zwei Messwerten.
1610        ConditionResult::True
1611    }
1612
1613    /// [523] Hinweis: Nur für die Übermittlung der Energiemenge im Zeitintervall zwischen zwei Messwerten vor der Netznutzungsabrechnung.
1614    fn evaluate_523(&self, _ctx: &EvaluationContext) -> ConditionResult {
1615        // Hinweis: Nur für die Übermittlung der Energiemenge im Zeitintervall zwischen zwei Messwerten vor der Netznutzungsabrechnung.
1616        ConditionResult::True
1617    }
1618
1619    /// [524] Hinweis: Nur, wenn es sich um die Übermittlung von Abrechnungsbrennwert und Z-Zahl für den vom Lieferanten über eine Geschäftsdatenanfrage angeforderten Zeitraum handelt.
1620    fn evaluate_524(&self, _ctx: &EvaluationContext) -> ConditionResult {
1621        // Hinweis: Nur, wenn es sich um die Übermittlung von Abrechnungsbrennwert und Z-Zahl für den vom Lieferanten über eine Geschäftsdatenanfrage angeforderten Zeitraum handelt.
1622        ConditionResult::True
1623    }
1624
1625    /// [525] Hinweis: Nur für die Übermittlung der Energiemenge im Zeitintervall für eine Marktlokation ohne Messlokation (Pauschalanlage) wenn eines der Ereignisse aus Kapitel 4.2 eingetreten ist.
1626    fn evaluate_525(&self, _ctx: &EvaluationContext) -> ConditionResult {
1627        // Hinweis: Nur für die Übermittlung der Energiemenge im Zeitintervall für eine Marktlokation ohne Messlokation (Pauschalanlage) wenn eines der Ereignisse aus Kapitel 4.2 eingetreten ist.
1628        ConditionResult::True
1629    }
1630
1631    /// [526] Hinweis: Wert aus BGM+Z24 DE1004 der ORDERS mit der die Allokationsliste bestellt wurde.
1632    fn evaluate_526(&self, _ctx: &EvaluationContext) -> ConditionResult {
1633        // Hinweis: Wert aus BGM+Z24 DE1004 der ORDERS mit der die Allokationsliste bestellt wurde.
1634        ConditionResult::True
1635    }
1636
1637    /// [528] Hinweis: Wert aus BGM+Z28 DE1004 der ORDERS mit der die Anforderung von Messwerten erfolgt ist.
1638    fn evaluate_528(&self, _ctx: &EvaluationContext) -> ConditionResult {
1639        // Hinweis: Wert aus BGM+Z28 DE1004 der ORDERS mit der die Anforderung von Messwerten erfolgt ist.
1640        // Informational note, always applies
1641        ConditionResult::True
1642    }
1643
1644    /// [529] Hinweis: Wert aus BGM+7 DE1004 der ORDERS mit der die Anforderung von Messwerten erfolgt ist.
1645    fn evaluate_529(&self, _ctx: &EvaluationContext) -> ConditionResult {
1646        // Hinweis: Wert aus BGM+7 DE1004 der ORDERS mit der die Anforderung von Messwerten erfolgt ist.
1647        // Informational note, always applies
1648        ConditionResult::True
1649    }
1650
1651    /// [530] Hinweis: Wert aus SG4 IDE+24 DE7402 der UTILMD mit dem der Sender der MSCONS die vorherigen Stammdaten mittels UTILMD übermittelt hat.
1652    fn evaluate_530(&self, _ctx: &EvaluationContext) -> ConditionResult {
1653        // Hinweis: Wert aus SG4 IDE+24 DE7402 der UTILMD mit dem der Sender der MSCONS die vorherigen Stammdaten mittels UTILMD übermittelt hat.
1654        // Informational note, always applies
1655        ConditionResult::True
1656    }
1657
1658    /// [531] Hinweis: Wert aus BGM+7 DE1004 der MSCONS mit der der Zählerstand übermittelt wurde.
1659    fn evaluate_531(&self, _ctx: &EvaluationContext) -> ConditionResult {
1660        // Hinweis: Wert aus BGM+7 DE1004 der MSCONS mit der der Zählerstand übermittelt wurde.
1661        // Informational note, always applies
1662        ConditionResult::True
1663    }
1664
1665    /// [532] Hinweis: Wert aus BGM+7/Z27/Z28/270/Z41/Z42/Z85 DE1004 der MSCONS Nachricht, die storniert wird
1666    fn evaluate_532(&self, _ctx: &EvaluationContext) -> ConditionResult {
1667        // Hinweis: Wert aus BGM+7/Z27/Z28/270/Z41/Z42/Z85 DE1004 der MSCONS Nachricht, die storniert wird
1668        // Informational note, always applies
1669        ConditionResult::True
1670    }
1671
1672    /// [535] Hinweis: Verwendung der ID des Netzkoppelpunktes Strom/Gas
1673    fn evaluate_535(&self, _ctx: &EvaluationContext) -> ConditionResult {
1674        // Hinweis: Verwendung der ID des Netzkoppelpunktes Strom/Gas — informational note, always applies
1675        ConditionResult::True
1676    }
1677
1678    /// [538] Hinweis: Die Referenz auf die ORDERS ist nur dann anzugeben, wenn diese Werte vom Empfänger auch ursprünglich mittels ORDERS angefragt wurden.
1679    fn evaluate_538(&self, _ctx: &EvaluationContext) -> ConditionResult {
1680        // Hinweis: Die Referenz auf die ORDERS ist nur dann anzugeben, wenn diese Werte vom Empfänger auch ursprünglich mittels ORDERS angefragt wurden — informational note, always applies
1681        ConditionResult::True
1682    }
1683
1684    /// [541] Hinweis: Ein Korrekturgrund ist anzugeben, wenn: 1. ein bereits an den MP übermittelter vorläufiger Wert nach Stornierung durch einen Ersatzwert ersetzt wird, oder  2. ein bereits an den MP über...
1685    fn evaluate_541(&self, _ctx: &EvaluationContext) -> ConditionResult {
1686        // Hinweis: Ein Korrekturgrund ist anzugeben in bestimmten Ersatzwert/Stornierung-Szenarien — informational note, always applies
1687        ConditionResult::True
1688    }
1689
1690    /// [543] Hinweis: Wert aus BGM+Z23 DE1004 der ORDERS mit der die bilanzierte Menge bestellt wurde.
1691    fn evaluate_543(&self, _ctx: &EvaluationContext) -> ConditionResult {
1692        // Hinweis: Wert aus BGM+Z23 DE1004 der ORDERS mit der die bilanzierte Menge bestellt wurde — informational note, always applies
1693        ConditionResult::True
1694    }
1695
1696    /// [544] Hinweis: Bei einer Mengenaufteilung (z. B. Aufgrund einer Abgrenzung) für SG6 LOC+172 muss für den frühesten angegebenen Zeitpunkt zum Beginn des Zeitintervalls  (über alle Wiederholungen der L...
1697    fn evaluate_544(&self, _ctx: &EvaluationContext) -> ConditionResult {
1698        // Hinweis: Bei einer Mengenaufteilung für SG6 LOC+172 muss für den frühesten Zeitpunkt zu jeder OBIS-Kennziffer ein Zählerstand vorhanden sein — informational note, always applies
1699        ConditionResult::True
1700    }
1701
1702    /// [545] Hinweis: Bei einer Mengenaufteilung (z. B. Aufgrund einer Abgrenzung) für SG6 LOC+172 muss für den spätesten angegebenen Zeitpunkt zum Ende des Zeitintervalls (über alle Wiederholungen der LIN-...
1703    fn evaluate_545(&self, _ctx: &EvaluationContext) -> ConditionResult {
1704        // Hinweis: informational note about Mengenaufteilung and Zählerstand requirement — always applies
1705        ConditionResult::True
1706    }
1707
1708    /// [546] Hinweis: Eine Referenz auf die Stammdatenänderung des Gerätewechsels ist immer anzugeben, wenn diese dem Sender vorliegt.
1709    fn evaluate_546(&self, _ctx: &EvaluationContext) -> ConditionResult {
1710        // Hinweis: informational note about referencing master data changes for device replacements — always applies
1711        ConditionResult::True
1712    }
1713
1714    /// [547] Hinweis: Der Code 270 ist nur zu nutzen, wenn ein Lieferschein, der vor dem 1.4.2021 erstellt wurde, storniert wird.
1715    fn evaluate_547(&self, _ctx: &EvaluationContext) -> ConditionResult {
1716        // Hinweis: informational note — code 270 only for cancelling delivery notes created before 1.4.2021 — always applies
1717        ConditionResult::True
1718    }
1719
1720    /// [551] Hinweis: Ein Korrekturgrund ist anzugeben, wenn: 1. ein bereits an den MP übermittelter vorläufiger Wert durch einen Ersatzwert ersetzt wird, oder  2. ein bereits an den MP übermittelter Ersatzw...
1721    fn evaluate_551(&self, _ctx: &EvaluationContext) -> ConditionResult {
1722        // Hinweis: informational note describing when a Korrekturgrund must be provided — always applies
1723        ConditionResult::True
1724    }
1725
1726    /// [553] Hinweis: Wert aus BGM+Z34 DE1004 der ORDERS mit der die Reklamation von Werten erfolgt ist
1727    fn evaluate_553(&self, _ctx: &EvaluationContext) -> ConditionResult {
1728        // Hinweis: informational note — value from BGM+Z34 DE1004 of the ORDERS used for value complaint — always applies
1729        ConditionResult::True
1730    }
1731
1732    /// [554] Hinweis: Verwendung der ID der Technischen Ressource
1733    fn evaluate_554(&self, _ctx: &EvaluationContext) -> ConditionResult {
1734        // Hinweis: Verwendung der ID der Technischen Ressource — informational note, always applies
1735        ConditionResult::True
1736    }
1737
1738    /// [556] Hinweis: Wert aus BGM+Z45 DE1004 der ORDERS mit der die Anforderung der Ausfallarbeit durch den anfNB erfolgt ist.
1739    fn evaluate_556(&self, _ctx: &EvaluationContext) -> ConditionResult {
1740        // Hinweis: Wert aus BGM+Z45 DE1004 der ORDERS — informational note, always applies
1741        ConditionResult::True
1742    }
1743
1744    /// [557] Hinweis: Die Referenz auf die ursprüngliche MSCONS ist anzugeben, wenn es sich um die Übermittlung eines Gegenvorschlags durch den BTR handelt.
1745    fn evaluate_557(&self, _ctx: &EvaluationContext) -> ConditionResult {
1746        // Hinweis: Referenz auf ursprüngliche MSCONS bei Gegenvorschlag — informational note, always applies
1747        ConditionResult::True
1748    }
1749
1750    /// [558] Hinweis: Wert aus BGM+Z45 DE1004 der MSCONS auf die sich die Übermittlung des Gegenvorschlags durch den BTR bezieht.
1751    fn evaluate_558(&self, _ctx: &EvaluationContext) -> ConditionResult {
1752        // Hinweis: Wert aus BGM+Z45 DE1004 der MSCONS bei Gegenvorschlag — informational note, always applies
1753        ConditionResult::True
1754    }
1755
1756    /// [559] Hinweis: Ein Korrekturgrund ist anzugeben, wenn: 1. ein bereits an den MP übermittelter vorläufiger Wert nach Stornierung durch einen Ersatzwert ersetzt wird, oder  2. ein bereits an den MP über...
1757    fn evaluate_559(&self, _ctx: &EvaluationContext) -> ConditionResult {
1758        // Hinweis: Korrekturgrund bei Ersatz/Stornierung von Werten — informational note, always applies
1759        ConditionResult::True
1760    }
1761
1762    /// [560] Hinweis: Ein Korrekturgrund ist anzugeben, wenn: 1. ein bereits an den MP übermittelter vorläufiger Wert durch einen Ersatzwert ersetzt wird, oder  2. ein bereits an den MP übermittelter Ersatzw...
1763    fn evaluate_560(&self, _ctx: &EvaluationContext) -> ConditionResult {
1764        // Hinweis: Ein Korrekturgrund ist anzugeben, wenn ein vorläufiger/Ersatz/wahrer Wert ersetzt wird — informational note, always applies
1765        ConditionResult::True
1766    }
1767
1768    /// [566] Hinweis: Es sind nur die Werte erlaubt, die im vorherigen Stammdatenaustausch zu diesem Meldepunkt vom MSB zum Zeitpunkt übermittelt wurden.
1769    fn evaluate_566(&self, _ctx: &EvaluationContext) -> ConditionResult {
1770        // Hinweis: Es sind nur die Werte erlaubt, die im vorherigen Stammdatenaustausch übermittelt wurden — informational note, always applies
1771        ConditionResult::True
1772    }
1773
1774    /// [567] Hinweis: Es ist die Konfigurations-ID anzugeben, die im vorherigen Stammdatenaustausch kommuniziert wurde.
1775    fn evaluate_567(&self, _ctx: &EvaluationContext) -> ConditionResult {
1776        // Hinweis: Es ist die Konfigurations-ID anzugeben, die im vorherigen Stammdatenaustausch kommuniziert wurde — informational note, always applies
1777        ConditionResult::True
1778    }
1779
1780    /// [568] Hinweis: Verwendung ist nur zulässig, wenn es sich um 1:n Beziehung zwischen Markt- und Messlokation handelt und auf Ebene der Messlokation unterschiedliche Ersatzwertbildungsverfahren verwendet u...
1781    fn evaluate_568(&self, _ctx: &EvaluationContext) -> ConditionResult {
1782        // Hinweis: Verwendung nur zulässig bei 1:n Beziehung zwischen Markt- und Messlokation mit unterschiedlichen Ersatzwertbildungsverfahren — informational note, always applies
1783        ConditionResult::True
1784    }
1785
1786    /// [569] Hinweis: Bei mehreren Zählerständen einer Messlokation (z. B. HT/NT) ist diese Zeitangabe zu nutzen und eine Wiederholung das SG9 LIN durchzuführen.
1787    fn evaluate_569(&self, _ctx: &EvaluationContext) -> ConditionResult {
1788        // Hinweis: Bei mehreren Zählerständen einer Messlokation (z. B. HT/NT) ist diese Zeitangabe zu nutzen und SG9 LIN zu wiederholen — informational note, always applies
1789        ConditionResult::True
1790    }
1791
1792    /// [570] Hinweis: Verwendung ist nur zulässig, wenn es sich um 1:n Beziehung zwischen Markt- und Messlokation handelt und auf Ebene der Messlokation unterschiedliche Gründe für die Ersatzwertbildung vorl...
1793    fn evaluate_570(&self, _ctx: &EvaluationContext) -> ConditionResult {
1794        // Hinweis: Verwendung ist nur zulässig, wenn es sich um 1:n Beziehung zwischen Markt- und Messlokation handelt und auf Ebene der Messlokation unterschiedliche Gründe für die Ersatzwertbildung vorliegen und kommuniziert wurden.
1795        ConditionResult::True
1796    }
1797
1798    /// [571] Hinweis: Verwendung ist nur zulässig, wenn es sich um 1:n Beziehung handelt und auf Ebene der Netzkopplungspunkte unterschiedliche Gründe für die Ersatzwertbildung vorliegen und kommuniziert wur...
1799    fn evaluate_571(&self, _ctx: &EvaluationContext) -> ConditionResult {
1800        // Hinweis: Verwendung ist nur zulässig, wenn es sich um 1:n Beziehung handelt und auf Ebene der Netzkopplungspunkte unterschiedliche Gründe für die Ersatzwertbildung vorliegen und kommuniziert wurden.
1801        ConditionResult::True
1802    }
1803
1804    /// [572] Hinweis: Verwendung ist nur zulässig, wenn es sich um 1:n Beziehung handelt und auf Ebene der Netzkopplungspunkte unterschiedliche Ersatzwertbildungsverfahren vorliegen und kommuniziert wurden.
1805    fn evaluate_572(&self, _ctx: &EvaluationContext) -> ConditionResult {
1806        // Hinweis: Verwendung ist nur zulässig, wenn es sich um 1:n Beziehung handelt und auf Ebene der Netzkopplungspunkte unterschiedliche Ersatzwertbildungsverfahren vorliegen und kommuniziert wurden.
1807        ConditionResult::True
1808    }
1809
1810    /// [573] Hinweis: Eine Energiemenge in der Sparte Gas ist gemäß DVGW G685 Arbeitsblatt 4 Kapitel 5.3 auf ganze Kilowattstunden zu runden.
1811    fn evaluate_573(&self, _ctx: &EvaluationContext) -> ConditionResult {
1812        // Hinweis: Eine Energiemenge in der Sparte Gas ist gemäß DVGW G685 Arbeitsblatt 4 Kapitel 5.3 auf ganze Kilowattstunden zu runden.
1813        ConditionResult::True
1814    }
1815
1816    /// [574] Hinweis: Wert aus BGM DE1004 der ORDERS mit der die Bestellung der Werte nach Typ 2 erfolg ist
1817    fn evaluate_574(&self, _ctx: &EvaluationContext) -> ConditionResult {
1818        // Hinweis: Wert aus BGM DE1004 der ORDERS mit der die Bestellung der Werte nach Typ 2 erfolg ist
1819        ConditionResult::True
1820    }
1821
1822    /// [575] Hinweis: Verwendung der ID der Netzlokation
1823    fn evaluate_575(&self, _ctx: &EvaluationContext) -> ConditionResult {
1824        // Hinweis: Verwendung der ID der Netzlokation — informational note, always applies
1825        ConditionResult::True
1826    }
1827
1828    /// [576] Hinweis: Es darf nur eine Information im DE3148 übermittelt werden
1829    fn evaluate_576(&self, _ctx: &EvaluationContext) -> ConditionResult {
1830        // Hinweis: Es darf nur eine Information im DE3148 übermittelt werden — informational note, always applies
1831        ConditionResult::True
1832    }
1833
1834    /// [577] Hinweis: Dieser Code ist auch zu verwenden, wenn aufgrund der Beendigung einer Messlokation (Stilllegung) die Beendigung der Marktlokation (Stilllegung) zu unterschiedlichen Zeitpunkten erfolgt, da...
1835    fn evaluate_577(&self, _ctx: &EvaluationContext) -> ConditionResult {
1836        // Hinweis: Informational note about Messlokation/Marktlokation termination timing — always applies
1837        ConditionResult::True
1838    }
1839
1840    /// [902] Format: Möglicher Wert: ≥ 0
1841    fn evaluate_902(&self, ctx: &EvaluationContext) -> ConditionResult {
1842        ctx.format_check("QTY", 0, 1, |val| validate_numeric(val, ">=", 0.0))
1843    }
1844
1845    /// [904] Format: genau 16 Stellen
1846    // REVIEW: Format condition requiring exactly 16 characters. Applied to QTY value (most common numeric format condition target in MSCONS). Confidence is medium because the exact target segment/element is not specified in the condition text — QTY is the most likely candidate in MSCONS context. (medium confidence)
1847    fn evaluate_904(&self, ctx: &EvaluationContext) -> ConditionResult {
1848        ctx.format_check("QTY", 0, 1, |val| validate_exact_length(val, 16))
1849    }
1850
1851    /// [905] Format: max. 3 Stellen
1852    fn evaluate_905(&self, ctx: &EvaluationContext) -> ConditionResult {
1853        ctx.format_check("QTY", 0, 1, |val| validate_max_integer_digits(val, 3))
1854    }
1855
1856    /// [906] Format: max. 3 Nachkommastellen
1857    fn evaluate_906(&self, ctx: &EvaluationContext) -> ConditionResult {
1858        ctx.format_check("QTY", 0, 1, |val| validate_max_decimal_places(val, 3))
1859    }
1860
1861    /// [907] Format: max. 4 Nachkommastellen
1862    fn evaluate_907(&self, ctx: &EvaluationContext) -> ConditionResult {
1863        ctx.format_check("QTY", 0, 1, |val| validate_max_decimal_places(val, 4))
1864    }
1865
1866    /// [908] Format: Mögliche Werte: 1 bis n
1867    fn evaluate_908(&self, ctx: &EvaluationContext) -> ConditionResult {
1868        ctx.format_check("QTY", 0, 1, |val| validate_numeric(val, ">=", 1.0))
1869    }
1870
1871    /// [909] Format: Mögliche Werte: 0 bis n
1872    fn evaluate_909(&self, ctx: &EvaluationContext) -> ConditionResult {
1873        ctx.format_check("QTY", 0, 1, |val| validate_numeric(val, ">=", 0.0))
1874    }
1875
1876    /// [910] Format: Möglicher Wert: &lt; 0 oder ≥ 0
1877    fn evaluate_910(&self, ctx: &EvaluationContext) -> ConditionResult {
1878        let qty_segments = ctx.find_segments("QTY");
1879        let val = qty_segments
1880            .first()
1881            .and_then(|s| s.elements.first())
1882            .and_then(|e| e.get(1))
1883            .map(|s| s.as_str())
1884            .unwrap_or("");
1885        if val.is_empty() {
1886            return ConditionResult::Unknown;
1887        }
1888        match val.parse::<f64>() {
1889            Ok(_) => ConditionResult::True,
1890            Err(_) => ConditionResult::False,
1891        }
1892    }
1893
1894    /// [917] Format: max. 4 Vorkommastellen
1895    fn evaluate_917(&self, ctx: &EvaluationContext) -> ConditionResult {
1896        ctx.format_check("QTY", 0, 1, |val| validate_max_integer_digits(val, 4))
1897    }
1898
1899    /// [918] Format: Zeichen aus dem über UNOC definierten Zeichensatz, wobei von den Buchstaben nur Großbuchstaben erlaubt sind.
1900    // REVIEW: UNOC charset requires uppercase-only alphabetics. Checks all alphabetic characters in FTX text fields (C108, elements[3], components 0..4) are uppercase. Non-alphabetic characters (digits, punctuation) are permitted. Medium confidence — UNOC validation approximated as uppercase-only alphabetic check without full ISO 8859-1 codepoint enumeration. (medium confidence)
1901    fn evaluate_918(&self, ctx: &EvaluationContext) -> ConditionResult {
1902        let ftx_segments = ctx.find_segments("FTX");
1903        for seg in &ftx_segments {
1904            if let Some(c108) = seg.elements.get(3) {
1905                for component in c108 {
1906                    let s = component.as_str();
1907                    if s.is_empty() {
1908                        continue;
1909                    }
1910                    if !s.chars().all(|c| !c.is_alphabetic() || c.is_uppercase()) {
1911                        return ConditionResult::False;
1912                    }
1913                }
1914            }
1915        }
1916        ConditionResult::True
1917    }
1918
1919    /// [922] Format: TR-ID
1920    // REVIEW: Format: TR-ID — validates that the value is a valid Transaktionsreferenz-ID (1–35 alphanumeric characters) using validate_tr_id. In MSCONS, the document/transaction ID is typically found in BGM elements[1][0] (the document number composite). Medium confidence because the exact DE this format condition is applied to depends on the AHB field context, which could alternatively be RFF or another reference segment. (medium confidence)
1921    fn evaluate_922(&self, ctx: &EvaluationContext) -> ConditionResult {
1922        ctx.format_check("BGM", 1, 0, validate_tr_id)
1923    }
1924
1925    /// [925] Format: max. 5 Nachkommastellen
1926    fn evaluate_925(&self, ctx: &EvaluationContext) -> ConditionResult {
1927        ctx.format_check("QTY", 0, 1, |val| validate_max_decimal_places(val, 5))
1928    }
1929
1930    /// [931] Format: ZZZ = +00
1931    fn evaluate_931(&self, ctx: &EvaluationContext) -> ConditionResult {
1932        ctx.format_check("DTM", 0, 1, validate_timezone_utc)
1933    }
1934
1935    /// [932] Format: HHMM = 2200
1936    fn evaluate_932(&self, ctx: &EvaluationContext) -> ConditionResult {
1937        ctx.format_check("DTM", 0, 1, |val| validate_hhmm_equals(val, "2200"))
1938    }
1939
1940    /// [933] Format: HHMM = 2300
1941    fn evaluate_933(&self, ctx: &EvaluationContext) -> ConditionResult {
1942        ctx.format_check("DTM", 0, 1, |val| validate_hhmm_equals(val, "2300"))
1943    }
1944
1945    /// [934] Format: HHMM = 0400
1946    fn evaluate_934(&self, ctx: &EvaluationContext) -> ConditionResult {
1947        ctx.format_check("DTM", 0, 1, |val| validate_hhmm_equals(val, "0400"))
1948    }
1949
1950    /// [935] Format: HHMM = 0500
1951    fn evaluate_935(&self, ctx: &EvaluationContext) -> ConditionResult {
1952        ctx.format_check("DTM", 0, 1, |val| validate_hhmm_equals(val, "0500"))
1953    }
1954
1955    /// [937] Format: keine Nachkommastelle
1956    fn evaluate_937(&self, ctx: &EvaluationContext) -> ConditionResult {
1957        ctx.format_check("QTY", 0, 1, |val| validate_max_decimal_places(val, 0))
1958    }
1959
1960    /// [939] Format: Die Zeichenkette muss die Zeichen @ und . enthalten
1961    fn evaluate_939(&self, ctx: &EvaluationContext) -> ConditionResult {
1962        ctx.format_check("COM", 0, 0, validate_email)
1963    }
1964
1965    /// [940] Format: Die Zeichenkette muss mit dem Zeichen + beginnen und danach dürfen nur noch Ziffern folgen
1966    fn evaluate_940(&self, ctx: &EvaluationContext) -> ConditionResult {
1967        ctx.format_check("COM", 0, 0, validate_phone)
1968    }
1969
1970    /// [950] Format: Marktlokations-ID
1971    // HAND-EDITED (#373): the condition sits on LOC+172 and RFF C506 1154, not on
1972    // LOC+Z16; judge the value it is attached to.
1973    fn evaluate_950(&self, ctx: &EvaluationContext) -> ConditionResult {
1974        ctx.format_check_resolved(validate_malo_id)
1975    }
1976
1977    /// [951] Format: Zählpunktbezeichnung
1978    // REVIEW: Format: Zählpunktbezeichnung — 33 alphanumeric characters. In MSCONS, Zählpunktbezeichnung typically appears in LOC+Z19. Using validate_zahlpunkt helper. (medium confidence)
1979    // HAND-EDITED (#373): the condition sits on LOC+172 and RFF C506 1154, not on
1980    // LOC+Z19; judge the value it is attached to.
1981    fn evaluate_951(&self, ctx: &EvaluationContext) -> ConditionResult {
1982        ctx.format_check_resolved(validate_zahlpunkt)
1983    }
1984
1985    /// [960] Format: Netzlokations-ID
1986    // HAND-EDITED: a Netzlokations-ID is "E" + ten alphanumerics (`E1688117482`),
1987    // not MaLo-shaped; `validate_malo_id` rejected every real one (#173).
1988    // HAND-EDITED (#373): the condition sits on LOC+172 and RFF C506 1154, not on
1989    // LOC+Z17; judge the value it is attached to.
1990    fn evaluate_960(&self, ctx: &EvaluationContext) -> ConditionResult {
1991        ctx.format_check_resolved(validate_nelo_id)
1992    }
1993
1994    /// [2001] Segmentgruppe ist nur einmal je UNH anzugeben
1995    fn evaluate_2001(&self, _ctx: &EvaluationContext) -> ConditionResult {
1996        // Hinweis: Segmentgruppe ist nur einmal je UNH anzugeben — cardinality annotation, always applies
1997        ConditionResult::True
1998    }
1999
2000    /// [2002] Segmentgruppe ist bis zu drei Mal je SG5 NAD+DP anzugeben
2001    fn evaluate_2002(&self, _ctx: &EvaluationContext) -> ConditionResult {
2002        // Hinweis: Segmentgruppe ist bis zu drei Mal je SG5 NAD+DP anzugeben — cardinality annotation, always applies
2003        ConditionResult::True
2004    }
2005
2006    /// [2003] Segmentgruppe ist genau zwei Mal je SG9 LIN anzugeben
2007    fn evaluate_2003(&self, _ctx: &EvaluationContext) -> ConditionResult {
2008        // Hinweis: Segmentgruppe ist genau zwei Mal je SG9 LIN anzugeben — cardinality annotation, always applies
2009        ConditionResult::True
2010    }
2011}